프로젝트 진행 중에 Exception 이 발생하면 로그테이블에 저장해주는 로직을 개발해야 하는 상황이었다. 그래서 예외 처리를 @ExceptionHandler 를 통해 관리하고 @Aspect 어노테이션을 붙인 클래스에서 로그테이블을 저장 하는 흐름으로 개발 했는데 문제가 발생하였다. 😭
바로 @RequsetBody 를 통해 들어온 파라미터를 받아서 로그에 저장해야 하는데 현재 개발되어 있는 파라미터로는 요청 파라미터를 받을 수 없어서 찾아보던 중 HttpServletRequestWrapper 를 통해 값을 읽어오는 방법을 알게 되었음! (유레카😲)
상황
Controller
@PostMapping("/approval")
public CardResponse paymentApproval(
@Parameter(hidden = true) @LoginUserId Long userId
,@RequestBody PaymentApprovalRequest request
) {
return paymentService.prepaymentContract(userId, request, "결제");
}
LoggingAspect
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class LoggingAspect {
@Before(value = "execution(* [프로젝트명].exception.handler.GlobalExceptionHandler.*(..)) && args(exception, webRequest)", argNames = "exception,webRequest")
public void logExceptionBeforeMethod(Exception exception, WebRequest webRequest) {
saveToDatabaseCardLog(exception.getMessage().trim(), webRequest);
}
private void saveToDatabaseCardLog(String exceptionMessage, WebRequest webRequest){
ServletWebRequest servletWebRequest = (ServletWebRequest) webRequest;
[오류가 난 컨트롤러에서의 RequestBody 파라미터];
✨ String parameter = getRequestBody(servletWebRequest);
CardLog cardLog = CardLog.builder()
.requestNo(parameter)
.build();
CardLogRepository.save(cardLog);
}
}
어떤 데이터가 오류가 난 건지 Controller 의 @RequestBody 에 넘어온 파라미터를 로그 테이블에 저장하고 싶은 상황인데 검색해보니, HttpServletRequest 클래스의 getInputStream() 을 활용하여 body 값을 조회 해 올 수 있다고 함!
방법1-1. HttpServletRequest 를 통해 body 값 가져오기 - getInputStream()
public String getRequestBody(ServletWebRequest servletWebRequest){
BufferedReader br = null;
StringBuilder stringBuilder = new StringBuilder();
try{
HttpServletRequest request = servletWebRequest.getNativeRequest(HttpServletRequest.class);
InputStream inputStream = request.getInputStream();
String line = "";
if(inputStream != null){
br = new BufferedReader(new InputStreamReader(inputStream));
while ((line = br.readLine()) != null){
stringBuilder.append(line);
}
}
log.info("###### body {}" ,stringBuilder);
}catch (Exception e){
throw new RuntimeException(e);
}
return null;
}
HttpServletRequest request = servletWebRequest.getNativeRequest(HttpServletRequest.class);
-> ServletWebRequest 를 HttpServletRequest 로 변환
request.getInputSreamReader()
-> 읽기
결과 : 빈값으로 나오는 오류 발생

원인

요청 -> 필터 -> 디스패처서블릿 -> 인터셉터 -> 컨트롤러의 객체로 값을 바인딩을 하는 흐름인데, 지금 상황 같은 경우 body data를 Controller 단에서 소비하고 AOP 에서 getInputStream() 을 하니 비워져 있는 것이다.
왜 AOP가 아닌 "Controller 단에서 소비" 했다고 말하는 이유?
-> 예외가 발생하게 되면 @RestControllerAdvice 어노테이션이 붙은 클래스의 @ExeptionHandler 에서 예외처리를 받아 주는 상황이다. 그런데 그 전에 @Aspect 어노테이션을 붙은 클래스에서 @Before 로 포인트컷을 주어서
ExceptionHandler 클래스의 하위 메소드가 실행되기 전에 Aspect 클래스가 실행되고 거기서 Controller 단에서 받은 body 파라미터를 로그테이블에 저장하고 싶은 상황이다.
즉, Controller ----- throw 예외처리 ----> AOP -> ExceptionHandler 흐름이기 때문에 Controller 에서 이미 getInputStream()이 소비 되어서 AOP 에서 읽으려고 하니 비어있는 상황인것이다.
방법1-2. HttpServletRequest 를 통해 body 값 가져오기 - getReader()
public String getRequestBody(ServletWebRequest servletWebRequest){
BufferedReader br = null;
StringBuilder stringBuilder = new StringBuilder();
try{
HttpServletRequest request = servletWebRequest.getNativeRequest(HttpServletRequest.class);
String line = "";
br = request.getReader();
while ((line = br.readLine()) != null){
stringBuilder.append(line);
}
log.info("###### body {}" ,stringBuilder);
}catch (Exception e){
throw new RuntimeException(e);
}
return null;
}
br = request.getReader();
-> getInputStream() 이 아닌, getReader() 로 파라미터 값을 받아옴.
결과 : getInputStream() has already been called for this request

원인
getReader()에서 InputStream을 생성하는데, tomcat 에서 한번만 사용할 수 있도록 막아두었기 때문에 한번 read한 body 값은 다시 읽을 수 없다.
그래서 이 부분을 해결하기 위해서 HttpServletRequestWrapper 를 만들어서, getReader() 메서드를 오버라이드 하고 새로운 InputStreamReader를 만들어서 반환하도록 한 뒤, Filter 를 통해 들어오는 request들을 새로 만든 wrapper로 변경하여 받아올 수 있다.
방법2-1. HttpServletRequestWrapper 를 상속받는 클래스 생성
- 한번 읽고 사라지는 httpBody가 아닌 여러번 계속해서 읽을 수 있도록 구현
- getInputSream()에서 요청 httpBody를 복사하여 값을 저장하고 반환하는 기능을 재정의.
@Slf4j
public class RequestWrapper extends HttpServletRequestWrapper {
private final String body;
/**
* Constructs a request object wrapping the given request.
*
* @param request The request to wrap
* @throws IllegalArgumentException if the request is null
*/
public RequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder stringBuilder = new StringBuilder();
try (BufferedReader bufferedReader = request.getReader()) {
char[] charBuffer = new char[128];
int bytesRead;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
body = stringBuilder.toString();
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
public int read() {
return byteArrayInputStream.read();
}
};
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
방법2-2 : RequestFilter 생성
- 인터셉터에서 httpBody 가 소모되기 전에 필터 단계에서 httpBody를 옮겨 담는 기능을 수행.
@Component
@WebFilter(urlPatterns="/api/payment/*")
public class RequestFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// 다시 읽을 수 있는 클래스로 래핑 해주는 코드..
⭐request = new RequestWrapper(httpServletRequest);⭐
chain.doFilter(request, response);
}
}
@WebFilter 어노테이션은 해당 url 으로 들어오는 경우에만 해당 로직을 처리하고 싶어서 추가한 것이다.
완성
public String getRequestBody(ServletWebRequest servletWebRequest){
HttpServletRequest httpServletRequest = servletWebRequest.getNativeRequest(HttpServletRequest.class);
String body;
StringBuilder stringBuilder = new StringBuilder();
⭐try(BufferedReader bufferedReader = Objects.requireNonNull(httpServletRequest).getReader()){⭐
log.info("##### LoggingAspect body");
while ((body = bufferedReader.readLine()) != null){
log.info(body);
stringBuilder.append(body);
}
JSONParser parser = new JSONParser();
JSONObject jsonObject = (JSONObject) parser.parse(stringBuilder.toString());
return jsonObject.get("orderId") == null ? stringBuilder.toString() : jsonObject.get("orderId").toString();
}catch (Exception e){
throw new RuntimeException(e);
}
}
https://sjh9708.tistory.com/168
[Spring Boot] 사용자 정의 예외처리 : Exception Handler
이번 포스팅에서는 Spring Boot에서 예외가 발생했을 때, 이를 처리하기 위한 계층을 정의해서 만들어보려고 한다. Exception Handler 정의의 필요성 @Override @Transactional public Long addMember(MemberJoinRequestDto i
sjh9708.tistory.com
https://hirlawldo.tistory.com/44
[Spring 프로젝트] Interceptor로 request, response body json 값 로깅하기
Spring Logging (Interceptor로 Request, Response body json 값 로깅하기) 스프링 프로젝트를 하면서 기존에는 LoggingAspect를 만들어서 Aspect파일에서 parameter값과 body값을 찍어주고 있었다. response 값도 찍어주기
hirlawldo.tistory.com
https://hirlawldo.tistory.com/42
[Spring 프로젝트] AOP Logging (Post 메서드의 Json Body 값 로깅하기)
Logging AOP - Json으로 들어온 Request Body값 로깅하기 테스트 하다보니 기존에 추가했었던 로깅 Aspect 에서는 Parameter값을 로깅하는 것만 추가했어서 Post 메서드에서 들어오는 json body값을 로깅하고 있
hirlawldo.tistory.com
HttpRequest에서 body값 가져오기
Form Data를 어느 method에 넣느냐에 따라 쿼리파라미터의 위치가 다르다get: urlpost: body요청 -> 필터 -> 디스패처 서블릿 -> 인터셉터 -> 컨트롤러로 값을 바인딩 하는 과정에서 Interceptor에서 getInputStream
velog.io
'Java > AOP' 카테고리의 다른 글
| AOP의 @Aspect 에 대해서 (0) | 2024.08.21 |
|---|---|
| ExceptionHandler (with. AOP) 에 대해서 (0) | 2024.08.21 |