How to Log All Requests and Responses and Exceptions in a Single Place

1. 소개

로깅은 웹 애플리케이션을 구축하는 데 중요한 역할을 합니다. 이는 효율적인 디버깅, 성능 모니터링 및 오류 추적을 가능하게 합니다. 그러나 모든 요청, 응답 및 예외를 중앙 집중식으로 캡처할 때, 깔끔하고 조직적인 방식으로 로깅을 구현하는 것은 일반적인 도전 과제입니다.

이 튜토리얼에서는 Spring Boot 애플리케이션에서 중앙 집중식 로깅을 구현할 것입니다. 필요한 모든 구성에 대한 자세한 단계별 가이드를 제공하며, 실제 코드 예제로 프로세스를 보여줍니다.

2. Maven 의존성

먼저, pom.xml에 필요한 의존성이 있는지 확인하십시오. 우리는 Spring Web와, 선택적으로 더 나은 모니터링을 위한 Spring Boot Actuator가 필요합니다:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.4.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <version>3.4.1</version>
</dependency>

의존성이 설정되면 로깅 논리를 구현할 준비가 됩니다.

3. 요청 로깅을 위한 Spring Boot Actuator 사용하기

커스텀 로직을 만들기 전에, HTTP 요청을 기본적으로 로깅하는 Spring Boot Actuator를 사용하는 것을 고려하십시오. Actuator 모듈에는 애플리케이션에 대한 마지막 100개의 HTTP 요청을 보여주는 /actuator/httpexchanges 엔드포인트가 포함되어 있습니다 (Spring Boot 2.0 이상). spring-boot-starter-actuator 의존성을 추가하는 것 외에도 httpexchanges 엔드포인트를 노출하도록 애플리케이션 속성을 구성할 것입니다:

management:
  endpoints:
    web:
      exposure:
        include: httpexchanges

또한, 트레이스 데이터를 저장할 인메모리 리포지토리를 추가합니다. 이를 통해 기본 애플리케이션 논리에 영향을 주지 않고 트레이스 데이터를 일시적으로 저장할 수 있습니다:

@Configuration
public class HttpTraceActuatorConfiguration {
    @Bean
    public InMemoryHttpExchangeRepository createTraceRepository() {
        return new InMemoryHttpExchangeRepository();
    }
}

이제 애플리케이션을 실행하고 /actuator/httpexchanges에 접근하여 로그를 확인할 수 있습니다:

HTTP Exchanges

4. 커스텀 로깅 필터 만들기

커스텀 로깅 필터를 만들면 우리의 필요에 맞게 프로세스를 조정할 수 있습니다. Spring Boot Actuator가 HTTP 요청 및 응답을 로깅하는 편리한 방법을 제공하지만, 세부적이거나 커스텀 로깅 요구 사항을 모두 충족하지는 않을 수 있습니다. 커스텀 필터를 사용하면 추가 세부정보를 로깅하거나 로그를 특정 방식으로 형식화하거나 로깅을 다른 모니터링 도구와 통합할 수 있습니다. 또한, 기본적으로 Actuator와 같은 도구에 의해 캡처되지 않는 민감한 데이터를 로깅하는 데 유용합니다. 예를 들어, 우리는 요청 헤더, 본문 내용, 응답 세부정보를 어떤 형식으로든 로깅할 수 있습니다.

4.1. 커스텀 필터 구현하기

이 필터는 모든 들어오는 HTTP 요청과 나가는 HTTP 응답에 대한 중앙 집중식 인터셉터가 될 것입니다. Filter 인터페이스를 구현함으로써 우리는 애플리케이션을 통과하는 모든 요청 및 응답의 세부정보를 로깅할 수 있으며, 이를 통해 디버깅 및 모니터링이 더 효율적이 됩니다:

@Override
public void doFilter(jakarta.servlet.ServletRequest request, jakarta.servlet.ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        logRequest(httpRequest);

        ResponseWrapper responseWrapper = new ResponseWrapper(httpResponse);

        chain.doFilter(request, responseWrapper);

        logResponse(httpRequest, responseWrapper);
    } else {
        chain.doFilter(request, response);
    }
}

커스텀 필터 내에서 요청 및 응답을 로깅하는 두 가지 추가 메소드를 사용합니다:

private void logRequest(HttpServletRequest request) {
    logger.info("Incoming Request: [{}] {}", request.getMethod(), request.getRequestURI());
    request.getHeaderNames().asIterator().forEachRemaining(header ->
      logger.info("Header: {} = {}", header, request.getHeader(header))
    );
}

private void logResponse(HttpServletRequest request, ResponseWrapper responseWrapper) throws IOException {
    logger.info("Outgoing Response for [{}] {}: Status = {}",
      request.getMethod(), request.getRequestURI(), responseWrapper.getStatus());
    logger.info("Response Body: {}", responseWrapper.getBodyAsString());
}

4.2. 커스텀 응답 래퍼

HTTP 응답의 응답 본문을 캡처하고 조작할 수 있는 커스텀 ResponseWrapper를 구현할 것입니다. 이 래퍼는 기본 HttpServletResponse가 본문이 작성된 후 직접적으로 접근할 수 없기 때문에 유용합니다. 응답 콘텐츠를 가로채고 저장하여 클라이언트에게 전송하기 전에 로깅하거나 수정할 수 있습니다:

public class ResponseWrapper extends HttpServletResponseWrapper {

    private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    private final PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream));

    public ResponseWrapper(HttpServletResponse response) {
        super(response);
    }

    @Override
    public ServletOutputStream getOutputStream() {
        return new ServletOutputStream() {
            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setWriteListener(WriteListener writeListener) {
            }

            @Override
            public void write(int b) {
                outputStream.write(b);
            }
        };
    }

    @Override
    public PrintWriter getWriter() {
        return writer;
    }

    @Override
    public void flushBuffer() throws IOException {
        super.flushBuffer();
        writer.flush();
    }

    public String getBodyAsString() {
        writer.flush();
        return outputStream.toString();
    }
}

4.3. 전역적으로 예외 처리하기

Spring Boot는 @ControllerAdvice 주석을 통해 예외를 관리할 수 있는 편리한 방법을 제공합니다. 이 핸들러는 요청 처리 중 발생한 모든 예외를 잡아 유용한 정보를 로깅합니다:

@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(Exception ex) {
        logger.error("Exception caught: {}", ex.getMessage(), ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred");
    }
}

우리는 ExceptionHandler 주석을 사용하여 특정 유형의 예외를 처리하는 메서드를 지정합니다. 이 경우, Exception.class입니다. 이는 이 핸들러가 모든 예외를 잡는다는 것을 의미합니다(애플리케이션의 다른 곳에서 처리되지 않는 한). 모든 예외를 포착하고 로깅하여 클라이언트에게 일반적인 오류 응답을 반환합니다. 스택 추적을 로깅함으로써 세부 사항이 누락되지 않도록 보장합니다.

5. 구현 테스트하기

로깅 설정을 테스트하기 위해 간단한 REST 컨트롤러를 생성할 수 있습니다:

@RestController
@RequestMapping("/api")
public class TestController {
    @GetMapping("/hello")
    public String hello() {
        return "Hello, World!";
    }

    @GetMapping("/error")
    public String error() {
        throw new RuntimeException("This is a test exception");
    }
}

/api/hello에 접근하면 요청 및 응답이 로깅됩니다:

INFO 19561 --- [log-all-requests] [nio-8080-exec-3] c.baeldung.logallrequests.LoggingFilter  : Incoming Request: [GET] /api/hello
INFO 19561 --- [log-all-requests] [nio-8080-exec-3] c.baeldung.logallrequests.LoggingFilter  : Header: host = localhost:8080
INFO 19561 --- [log-all-requests] [nio-8080-exec-3] c.baeldung.logallrequests.LoggingFilter  : Header: connection = keep-alive
…
INFO 19561 --- [log-all-requests] [nio-8080-exec-3] c.baeldung.logallrequests.LoggingFilter  : Outgoing Response for [GET] /api/hello: Status = 200
INFO 19561 --- [log-all-requests] [nio-8080-exec-3] c.baeldung.logallrequests.LoggingFilter  : Response Body: Hello, World!

/api/error에 접근하면 예외가 발생하고 그 과정에서 로깅됩니다:

INFO 19561 --- [log-all-requests] [nio-8080-exec-7] c.baeldung.logallrequests.LoggingFilter  : Outgoing Response for [GET] /api/error: Status = 500

6. 결론

이 기사에서는 요청, 응답 및 예외에 대한 중앙 집중식 로깅 메커니즘을 성공적으로 구현하였습니다. Spring Boot Actuator를 활용하거나 FilterControllerAdvice를 사용하여 커스텀 로깅 논리를 생성함으로써, 우리의 애플리케이션이 깔끔하고 유지 관리 가능하도록 보장했습니다.

이를 통해 애플리케이션을 모니터링하고 발생하는 문제를 신속하게 해결할 수 있도록 하였습니다. 전체 소스 코드는 GitHub에서 확인할 수 있습니다.

원본 출처

You may also like...

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다