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 모듈은 * /actuator/httpexchanges *라는 엔드포인트를 포함하고 있으며 (Spring Boot 2.0+ 용), 이는 애플리케이션으로의 마지막 100개의 HTTP 요청을 보여줍니다. *spring-boot-starter-actuator* 의존성을 추가하는 것 외에도, httpexchanges 엔드포인트를 노출하기 위해 애플리케이션 속성을 구성하겠습니다:
management:
endpoints:
web:
exposure:
include: httpexchanges
추적 데이터를 저장할 수 있는 인메모리 리포지토리도 추가하겠습니다. 이렇게 하면 기본 애플리케이션 로직에 영향을 주지 않고 임시로 추적 데이터를 저장할 수 있습니다:
@Configuration
public class HttpTraceActuatorConfiguration {
@Bean
public InMemoryHttpExchangeRepository createTraceRepository() {
return new InMemoryHttpExchangeRepository();
}
}
이제 애플리케이션을 실행하고 /actuator/httpexchanges에 접근하여 기록된 로그를 확인할 수 있습니다.
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 : 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를 활용하거나 Filter와 ControllerAdvice를 사용하여 커스텀 로깅 논리를 생성함으로써, 애플리케이션이 깨끗하고 유지 관리 가능하게 유지되도록 했습니다.
이 기사의 코드 기반은 GitHub에서 사용할 수 있습니다. 로그인을 한 후 Baeldung Pro Member로 프로젝트에서 학습하고 코딩을 시작하세요.