Continuous Challenge

[항해플러스 7기 백엔드] 5주차 회고 - 서버 구축하기 (Filter, Interceptor, Exception) (feat. 추천인코드) 본문

Study/항해플러스 7기

[항해플러스 7기 백엔드] 5주차 회고 - 서버 구축하기 (Filter, Interceptor, Exception) (feat. 추천인코드)

응굥 2025. 1. 18. 17:00
728x90
728x90

4주차가 기능 구현이었다면, 5주차는 4주차에 구현한 코드를 고도화하는 것 이었다.

 

ㅇ 4주차에 구현하지 못한 기능들 구현하기

  - 잔액 히스토리 남기기

  - 결제 완료 시 주문 상태 업데이트 하기

  - 아키텍처에 맞게 파일 다시 정리하기 (DataPlatformService 이동, application -> infrastructure)

 

ㅇ Filter 로 Logging 처리하기

ㅇ Interceptor 로 사용자 검증하기

ㅇ ExceptionHandler 로 예외 처리하기

 

ㅇ 동시성 통합 테스트 보완하기

 

이번 주차에는 3군데에서 동시성 제어에 대한 처리를 했다.

1. 잔액 사용

2. 재고 차감

3. (선착순) 쿠폰 발급

모두 비관적 락을 사용해서 처리를 하였다.

 

앞으로는 지금까지 구현한 코드를 기반으로 계속해서 리팩토링해가며 고도화하는 시간을 가진다.

6주차에는 드디어 redis를 사용한 분산 락을 적용해본다 ! (너무너무 기대가 된다.)

그리고 각각의 기능에서 적합한 락을 판단하여 기능에 맞는 락을 사용하도록 리팩토링할 예정이다.

 


공통 프로세스에 대한 처리

공통 프로세스에 대한 로직을 모든 API 마다 작성하게 된다면 중복 코드가 많아지게 되고, 소스 코드의 관리가 어려워 진다.

그렇기 때문에 공통 프로세스에 대한 부분은 별도로 관리하는 것이 좋다.

이러한 공통 코드를 처리할 수 있는 방법은 3가지가 있다.

1. Filter

2. Interceptor

3. AOP

출처 https://velog.io/@soyeon207/Spring-Filter-Interceptor-AOP

 

  • Interceptor와 Filter는 Servlet 단위에서 실행된다. 반면 AOP는 메소드 앞에 Proxy패턴의 형태로 실행된다.
  • 요청이 들어오면 Filter → Interceptor → AOP → Interceptor → Filter 순으로 거치게 된다.
1. 서버를 실행시켜 서블릿이 올라오는 동안에 init이 실행되고, 그 후 doFilter가 실행된다. 

2. 컨트롤러에 들어가기 전 preHandler가 실행된다

3. 컨트롤러에서 나와 postHandler, after Completion, doFilter 순으로 진행이 된다.

4. 서블릿 종료 시 destroy가 실행된다.

Filter

  • 요청과 응답을 거른 뒤 정제하는 역할.
  • 서블릿 필터는 DispatcherServlet 이전에 실행이 되는데 필터가 동작하도록 지정된 자원의 앞단에서 요청 내용을 변경할 수 있으며 응답에 대해서도 변경 가능하다.
  • 스프링 컨텍스트 외부에 존재하여 스프링과 무관한 자원에 대해 동작한다.

필터의 메서드

  • init() : 필터 인스턴스 초기화
  • doFilter() : 전/후 처리
  • destroy() : 필터 인스턴스 종료

사용 예시

  • 공통된 보안 및 인증/인가 관련 작업
  • 이미지/데이터 압축 및 문자열 인코딩
  • 모든 요청과 응답에 대한 로깅

Interceptor

  • 요청에 대한 작업 전/후로 가로채는 역할.
  • 스프링의 DispatcherServlet이 컨트롤러를 호출하기 전/후로 끼어들기 때문에 스프링 컨텍스트 내부에서 Controller(Handler)에 관한 요청과 응답에 대해 처리한다.
  • 스프링의 모든 빈 객체에 접근할 수 있다.

인터셉터의 메서드

  • preHandler() : 컨트롤러 메서드가 실행되기 전
  • postHandler() : 컨트롤러 메서드 실행 직후 view 페이지가 랜더링 되기 전
  • afterCompletion() : view 페이지가 랜더링 되고 난 후

사용 예시

  • 세부적인 보안 및 인증/인가 작업
  • API 호출에 대한 로깅 또는 검증

이번 과제에서는 Filter 를 사용해 모든 요청과 응답에 대한 로깅을 처리하기로 했다.

Request, Response 값은 한 번 읽으면 다시 사용할 수 없다 ?!

Request, Response 값을 확인하는 과정에서 getParameter(), getInputStream(), getReader() 등의 메소드를 사용할 경우 내부의 값을 stream 방식으로 가져오게 되고, 이 단계가 종료되면 stream close 되기 때문에 더 이상 값을 가져올 수 없다.

 

이를 해결하기 위해 스프링에서 구현해놓은 Wrapper 클래스를 사용했다. Wrapper 클래스를 사용해 데이터를 감싸준 후 캐싱하여 여러 번 읽기가 가능하도록 하였다.

 

MDC 란?

MDC(Mapped Diagnostic Context)는 스레드 단위로 데이터를 읽고 쓸 수 있는 기능.

MDC를 사용하면 멀티 스레드 환경에서 각 스레드의 실행 컨텍스트를 구분할 수 있어서 모니터링 시 추적에 용이하다.

 

아래는 구현한 소스 코드의 일부이다. 

traceId 를 로그에 남겨 분산 시스템에서 추적이 용이하도록 하였다.

   @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        ContentCachingRequestWrapper cachingRequest = new ContentCachingRequestWrapper(httpRequest);
        ContentCachingResponseWrapper cachingResponse = new ContentCachingResponseWrapper(httpResponse);
        // Trace ID 생성 및 추가
        String traceId = UUID.randomUUID().toString();
        MDC.put("traceId", traceId);
        try {
            chain.doFilter(cachingRequest, cachingResponse);
        } finally {
            MDC.clear();  // MDC 내용 제거
        }
        // request
        String uri = cachingRequest.getRequestURI();
        String requestBody = new String(cachingRequest.getContentAsByteArray());
        log.info("Request -> TraceId: {}, URI: {}, Method: {}, Body: {}",
                traceId,
                uri,
                httpRequest.getMethod(),
                requestBody.isEmpty() ? "No Content" : requestBody);
        // response
        int status = cachingResponse.getStatus();
        String responseBody = new String(cachingResponse.getContentAsByteArray());
        log.info("Response -> TraceId: {}, Status: {}, Body: {}",
                traceId,
                status,
                responseBody.length() > 500 ? responseBody.substring(0, 500) + "..." : responseBody); // 500자 이상이면 생략
        // 응답 본문 복사
        cachingResponse.copyBodyToResponse();
    }
}

 


추가로,

 

지금 항해에서는 8기 백엔드 플러스 과정을 모집하고 있습니다 ~

https://hanghae99.spartacodingclub.kr/hhplus-hub

 

항해 플러스

10주 후 무엇이든 해내는 개발자로

hanghae99.spartacodingclub.kr

 

추천인 코드를 입력하면 20만원의 할인 혜택이 있다고 하니

추천인 코드가 필요하시다면 ! 

추천인 코드 : 0uoLDT 사용하세요 🫶🏻

728x90
728x90
Comments