AOP를 통한 로깅 클래스 분리


기존에는 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

예외 구분, 마커 인터페이스