기존에는 ExceptionHandler를 사용해서 로깅과 응답을 같이 처리하고 있었다.
@ExceptionHandler(value = DuplicateException.class)
public ResponseEntity<ResponseDto> memberDuplicate(DuplicateException e) {
log.warn(...);
ResponseDto errorResponse = new ResponseDto(HttpStatus.CONFLICT.value(),HttpStatus.CONFLICT, e.getMessage());
return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT);
}
문제는 없으나 거의 모든 메서드에서 로그를 작성하고 있기 때문에 로깅을 위한 클래스를 따로 작성하고 싶었다. 로그를 찍은 클래스에서 예외를 넘겨주면 ExceptionHandler는 그에 맞는 응답만 처리하기를 원한다. 그러려면 AOP를 사용하면 될 것 같다는 생각이 들었다. 처리 과정은 다음과 같아질 것이다.
AOP 프록시 객체가 COntroller 대상 호출 -> Controller 이하에서 예외 발생 -> AOP 프록시 객체에서 예외 로깅 -> ExceptionHandler
예외가 발생한 경우만 로깅할 것이기 때문에 @AfterThrowing을 사용해 작성했다.
@AfterThrowing(pointcut = "execution(* guckflix.backend.controller.*.*(..))", throwing = "e")
public void exceptionLogging(JoinPoint joinPoint, Exception e) {
// 사용자 정보
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
StringBuffer userInfo = new StringBuffer();
if(authentication.getPrincipal() == "anonymousUser") {
userInfo.append("anonymous");
} else {
Member member = ((PrincipalDetails) authentication.getPrincipal()).getMember();
String username = member.getUsername();
String userRole = member.getRole().toString();
userInfo.append(username).append(" ").append(userRole);
}
// 파라미터 출력
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
Enumeration<String> parameterNames = request.getParameterNames();
StringBuffer params = new StringBuffer();
parameterNames.asIterator().forEachRemaining(paramName ->
params.append(paramName).append(" = ").append(request.getParameter(paramName)).append(" "));
String requestURL = request.getMethod() + " " + request.getRequestURL();
String IP = request.getRemoteAddr();
String occurredClass = joinPoint.getSignature().toString();
/* 로깅 형식
요청 URL : GET <http://localhost:8081/test>
요청 IP : 0:0:0:0:0:0:0:1
사용자 정보 : anonymous
실행 클래스 : String guckflix.backend.controller.TestController.test()
리퀘스트 파라미터 : page = 2255 page4 = 336
예외 클래스 : class guckflix.backend.exception.NotAllowedIdException
다음 예외 발생
guckflix.backend.exception.NotAllowedIdException: Not allowed ID
이하 스택트레이스...
*/
log.warn("\\n 요청 URL : {} \\n 요청 IP : {} \\n 사용자 정보 : {} \\n 실행 클래스 : {} \\n 리퀘스트 파라미터 : {} \\n 예외 클래스 : {} \\n 다음 예외 발생 \\n",
requestURL,
IP,
userInfo,
occurredClass,
params,
e.getClass(),
e);
남은 로그는 다음과 같다.
2025-08-08 20:40:13.177 [http-nio-8081-exec-2] WARN [a4357b20-91f8-11ef-8a7e-c59ad3b711dd] guckflix.backend.log.LogAspect -
요청 URL : GET <http://localhost:8081/test>
요청 IP : 0:0:0:0:0:0:0:1
사용자 정보 : anonymous
실행 클래스 : String guckflix.backend.controller.TestController.test()
리퀘스트 파라미터 :
예외 클래스 : class guckflix.backend.exception.NotAllowedIdException
다음 예외 발생
guckflix.backend.exception.NotAllowedIdException: Not allowed ID
at guckflix.backend.controller.TestController.test(TestController.java:40)
at guckflix.backend.controller.TestController$$FastClassBySpringCGLIB$$a90f55c3.invoke(<generated>)
...
Caused by: java.lang.RuntimeException: 런타임 익셉션
at guckflix.backend.controller.TestController.causeException(TestController.java:58)
at guckflix.backend.controller.TestController.test(TestController.java:38)
... 125 common frames omitted