SpringBoot 사용하여 Request요청을 로깅하기 Logging HTTP Requests in SpringBoot
Request 요청 로깅을 할 경우, 모든 REST API 컨트롤러에 로그를 남기는것도 하나의 방법이다. 하지만 모든 API 컨트롤러에 로깅을 작성하게 된다면 비효율적으로 작업이 될 수 있다.
Spring Interceptor 라는 것을 사용해서, 컨트롤러의 Handler로 도착하기 전에 가로채어 따로 작업을 해 주는 방법을 정리하려고 한다
When logging request information, one approach is to add logs to every REST API controller. However, writing logging code in every API controller can be inefficient.
I’m going to explain how to use Spring Interceptor to intercept requests before they reach the controller’s Handler and perform separate processing.
Interceptor란
Interceptor란, 단어에서 느낄 수 있듯이 “낚아채다” 라는 의미를 가지고있다. Client 이 Server로 요청을 보낼 때, Request 객체는 가장먼저 DispatcherServelet이라는 곳을 통과하여 Controller로 전달이 된다.
이 떄, DispatcherServelet과 Controller사이에 Interceptor를 두어 미리 Request객체를 가져올 수 있다.
Interceptor를 사용하면 생기는 장점?
- 공통 코드 사용으로 중복된 코드를 제거함으로써 코드 재사용성을 증가시킨다
- 반복되는 작업을 일일히 하지 않아도 되어 코드 누락에 대한 위험성이 감소한다
What is an Interceptor?
As the word suggests, Interceptor means “to catch” or “intercept.” When a Client sends a request to a Server, the Request object first passes through the DispatcherServlet before being delivered to the Controller.
At this point, by placing an Interceptor between the DispatcherServlet and Controller, you can retrieve the Request object in advance.
Advantages of Using Interceptor?
- Increases code reusability by eliminating duplicate code through common code usage
- Reduces the risk of code omission since you don’t have to perform repetitive tasks individually
Handler Interceptor Method 종류
- preHandle()
request가Controller에 진입하기 전에 동작하는 함수. – return 값이 true인 경우에만Controller가 정상적으로 진행이 되고, false인 경우에는 실행이 종료된다.
- postHandle()
request가Controller에 진입 한 후,View가Rendering되기 전 수행
- afterCompletion()
request가Controller에 진입 한 후,View가 정상적으로 실행 된 후에 수행
- afterConcurrentHandlingStarted()
- 비동기요청시 사용하는 함수– postHandle(), afterCompletion() 메서드를 대체
Types of Handler Interceptor Methods
- preHandle()
- A function that runs before the
requestenters theController. TheControllerproceeds normally only when the return value is true; if false, execution is terminated.
- A function that runs before the
- postHandle()
- Executes after the
requestenters theControllerbut before theViewis rendered
- Executes after the
- afterCompletion()
- Executes after the
requestenters theControllerand after theViewhas been successfully rendered
- Executes after the
- afterConcurrentHandlingStarted()
- A function used for asynchronous requests - replaces postHandle() and afterCompletion() methods
Handler Interceptor 사용하기
Handler Interceptor는 위에서 설명 한 것 처럼 이미 생성되어있는 인터페이스이다. 때문에 메서드 오버라이딩을 사용해서 preHandle을 구현하려고한다.
총 4단계로 구성을 하려고 한다
- Handler Interceptor 구현체를 만들고 모든 요청을 기록
- SpringBoot가 인식 할 수 있도록 구현체 등록
- HttpServletRequest를 래핑하여 한번만 읽을 수 있는 요청을 여러번 읽어서 사용 할 수 있도록 래핑
- HttpServlet 요청을 필터링 하는 서블릿 필터 생성
아래는 나의 프로젝트 구조이다
Using Handler Interceptor
As explained above, Handler Interceptor is an already existing interface. Therefore, I’m going to implement preHandle using method overriding.
I’ll set this up in 4 steps:
- Create a Handler Interceptor implementation and log all requests
- Register the implementation so SpringBoot can recognize it
- Wrap HttpServletRequest so that requests that can only be read once can be read multiple times
- Create a servlet filter that filters HttpServlet requests
Below is my project structure.
.
├── HELP.md
├── README.md
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── springboothandlerinterceptorsimple
│ │ ├── SpringBootHandlerInterceptorSimpleApplication.java
│ │ ├── common
│ │ │ ├── MyLoggingInterceptor.java
│ │ │ ├── RequestServletFilter.java
│ │ │ └── RequestServletWrapper.java
│ │ ├── config
│ │ │ └── Configuration.java
│ │ └── controller
│ │ └── MyTestController.java
│ └── resources
│ ├── application.properties
│ ├── static
│ └── templates
└── test
└── java
└── com
└── example
└── springboothandlerinterceptorsimple
└── SpringBootHandlerInterceptorSimpleApplicationTests.java
Handler Interceptor 생성
Creating Handler Interceptor
package com.example.springboothandlerinterceptorsimple.common;
import java.util.Map;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import com.fasterxml.jackson.databind.ObjectMapper;
@Component
public class MyLoggingInterceptor implements HandlerInterceptor {
Logger logger = LoggerFactory.getLogger(MyLoggingInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (Objects.equals(request.getMethod(), "POST")) {
Map<String, Object> inputMap = new ObjectMapper().readValue(request.getInputStream(), Map.class);
logger.info("요청 정보: " + inputMap);
logger.info("요청 URL: " + request.getRequestURL());
return true;
} else {
logger.info("요청 정보: " + request.getQueryString());
logger.info("요청 URL: " + request.getRequestURL());
return true;
}
}
}
Handler Interceptor 등록
Config 패키지를 만들어 아래와 같이 생성한다
Registering Handler Interceptor
Create a Config package and set it up as follows.
package com.example.springboothandlerinterceptorsimple.config;
import com.example.springboothandlerinterceptorsimple.common.MyLoggingInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Component
public class Configuration implements WebMvcConfigurer {
@Autowired
private MyLoggingInterceptor myLoggingInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myLoggingInterceptor);
}
}
Servlet Wrapper 생성
request는 spring에서 한번만 읽을 수 있다
이 request객체를 래핑하여 여러곳에서 읽을 수 있도록 처리 해 주자
Creating Servlet Wrapper
In Spring, a request can only be read once.
Let’s wrap this request object so it can be read in multiple places.
package com.example.springboothandlerinterceptorsimple.common;
import java.io.IOException;
import java.io.StringReader;
import java.util.Scanner;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class RequestServletWrapper extends HttpServletRequestWrapper {
private String requestData = null;
public RequestServletWrapper(HttpServletRequest request) {
super(request);
try (Scanner s = new Scanner(request.getInputStream()).useDelimiter("\\A")) {
requestData = s.hasNext() ? s.next() : "";
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
StringReader reader = new StringReader(requestData);
return new ServletInputStream() {
private ReadListener readListener = null;
@Override
public int read() throws IOException {
return reader.read();
}
@Override
public void setReadListener(ReadListener listener) {
this.readListener = listener;
try {
if (!isFinished()) {
readListener.onDataAvailable();
} else {
readListener.onAllDataRead();
}
} catch (IOException io) {
io.printStackTrace();
}
}
@Override
public boolean isReady() {
return isFinished();
}
@Override
public boolean isFinished() {
try {
return reader.read() < 0;
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
};
}
}
- HttpServletRequest객체를 받아서 문자열로 추출하는 생성자를 만든다
StringReader reader = new StringReader(requestData);
- read(), setReadListener(), isFinished(), isReady()가 구현된 InputStream을 재정의
- Create a constructor that receives HttpServletRequest object and extracts it as a string
StringReader reader = new StringReader(requestData);
- Redefine InputStream with read(), setReadListener(), isFinished(), isReady() implemented
Servlet Filter 생성
Creating Servlet Filter
package com.example.springboothandlerinterceptorsimple.common;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
@Component
public class RequestServletFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest wrappedRequest = new RequestServletWrapper((HttpServletRequest) request);
chain.doFilter(wrappedRequest, response);
}
}
- 요청은 래퍼 개체를 사용하여 래핑되고 이 래퍼 개체는 필터 체인으로 전달 됨
- HttpServletRequest 객체를 읽을 때 구체적으로 언급하지 않더라도 래퍼 객체를 읽음(MyLoggingInterceptor 클래스)
- Requests are wrapped using the wrapper object and this wrapper object is passed to the filter chain
- When reading the HttpServletRequest object, it reads the wrapper object even without specific mention (MyLoggingInterceptor class)
Controller 생성
api endpoint를 생성 해서 request를 받아 줄 수 있는 controller 클래스를 생성한다
Creating Controller
Create a controller class that can receive requests by creating API endpoints.
package com.example.springboothandlerinterceptorsimple.controller;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
public class MyTestController {
@GetMapping("/test-one")
public Map<String, Object> firstAPI(@RequestParam Map<String, Object> request) {
return request;
}
@PostMapping("/test-two")
public Map<String, Object> secondAPI(@RequestBody Map<String, Object> request) {
return request;
}
}
요청 테스트하기
우리가 로그를 잘 남기고 있는지 확인하기 위해 서버를 실행 한 후 요청을 실행 해 보자
컨트롤러에 endpoint를 총 두개 생성했다
test-one: GETtest-two: POST
- GET -
localhost:8080/test-one?id=1&name=wool- 결과
INFO 14244 --- [nio-8080-exec-7] c.e.s.common.MyLoggingInterceptor : 요청 정보: id=1&name=wool INFO 14244 --- [nio-8080-exec-7] c.e.s.common.MyLoggingInterceptor : 요청 URL: http://localhost:8080/test-one
- 결과
- POST -
localhost:8080/test-two- body :
application/json'{ "id":1, "name":"wool", "phone":"01012341234", "mail":"hello@mail.com" } - 결과
INFO 14244 --- [nio-8080-exec-9] c.e.s.common.MyLoggingInterceptor : 요청 정보: {id=1, name=wool, phone=01012341234, mail=hello@mail.com} INFO 14244 --- [nio-8080-exec-9] c.e.s.common.MyLoggingInterceptor : 요청 URL: http://localhost:8080/test-two
- body :
Testing Requests
Let’s run the server and execute some requests to verify that logging is working correctly.
I created two endpoints in the controller:
test-one: GETtest-two: POST
- GET -
localhost:8080/test-one?id=1&name=wool- Result
INFO 14244 --- [nio-8080-exec-7] c.e.s.common.MyLoggingInterceptor : Request Info: id=1&name=wool INFO 14244 --- [nio-8080-exec-7] c.e.s.common.MyLoggingInterceptor : Request URL: http://localhost:8080/test-one
- Result
- POST -
localhost:8080/test-two- body :
application/json'{ "id":1, "name":"wool", "phone":"01012341234", "mail":"hello@mail.com" } - Result
INFO 14244 --- [nio-8080-exec-9] c.e.s.common.MyLoggingInterceptor : Request Info: {id=1, name=wool, phone=01012341234, mail=hello@mail.com} INFO 14244 --- [nio-8080-exec-9] c.e.s.common.MyLoggingInterceptor : Request URL: http://localhost:8080/test-two
- body :
댓글남기기