[Spring] Interceptor와 AOP를 활용한 Logging 처리

Tak Jeon·2025년 2월 25일
0

Spring

목록 보기
5/8
post-thumbnail

이번 포스팅에서는 Logging 처리 중 Interceptor와 AOP를 활용한 Logging 처리에 대하여 작성해보고자 합니다.

1. Interceptor을 활용한 Logging 처리

Spring InterceptorHandlerInterceptor를 구현하는 별도의 클래스를 작성해서 구현한다.
HandlerInterceptorAdapter는 Spring Boot 3.x에서는 삭제되었으므로, 따라서 HandlerInterceptor만 활용한다.

HandlerInterceptor는 다음과 같이 구성된다.

  • preHandle() : 실제 핸들러가 실행되기 이전
  • postHandle() : 핸들러가 실행 된 이후
  • afterCompletion() : 요청이 모두 완료된 이후

preHandle() : boolean 타입의 반환값을 가짐. true인 경우에는 요청을 실행하고, false인 경우 요청을 중단.
postHandle() : 핸들러가 실행된 이후 실행됨.
afterCompletion() : 요청이 모두 완료된 이후, 예외 발생 여부와 관계 없이 클라이언트에 응답이 전달될 때 실행됨.

로그를 언제 기록할것인지에 따라, 3가지 메서드를 각각 재정의 하면 된다.

이번 과제에서 나의 목표는 deleteComment()Interceptor를 활용하여 어드민 권한 여부를 확인하여 인증되지 않은 사용자의 접근을 차단하고, 인증 성공시 요청 시각과 URL을 로깅하도록 구현하는 것이다.

따라서 preHandle()에서는 요청 시각과 URL에 대한 Log 를 기록하고 truereturn 하도록 구현한다.
그 이유는 Filter에서 사용자가 ADMIN이 아닐 경우 예외 처리를 하고 있으므로, Interceptor까지 들어온 상태라면 사용자는 ADMIN이기 때문이다.

LoggingInterceptor.class

//LoggingInterceptor 생성
public class LoggingInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object Handler) {
        LocalDateTime time = LocalDateTime.now();
        logger.info("===Admin Access: {}===", request.getMethod());
        logger.info("Time: {}, URL: {}", time, request.getRequestURI());
        return true;
    }
}

//LoggingInterceptor 등록 (WebConfig 클래스에 등록한다)

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoggingInterceptor()).addPathPatterns("/admin/comments/**");
    }

인터셉터를 WebConfig에 등록한다. /admin/comments/에 해당하는 URL일 경우 해당 LoggingInterceptor를 지나게 된다.
Filter에서 ADMIN 판별을 했으므로, Interceptor에서는 시간값과 URL 값을 로그로 기록한 뒤, truereturn 해준다.


로그가 잘 찍히는 것을 확인할 수 있다.

2. AOP를 활용한 Logging 처리

AOP(Aspect Oriented Programming)이란 관점 지향 프로그래밍이다.
비즈니스 로직의 기준을 정하고 그 기준에 따라 나눈 부분을 관점이라고 한다면, 해당 관점을 모듈화 하는것이라 할 수 있다.
예를 들어, 로깅, 트랜잭션 관리, 보안 등은 여러 모듈에서 공통적으로 가지고 있는 관점이라 할 수 있다.
해당 관점을 하나의 모듈로 모듈화 하여 가독성을 높이고 중복을 줄이는 것이 목표이다.
Spring에서의 AOP 흐름은 다음과 같다.
1. Request -> Filter
2. Filter -> DispatcherServlet
3. DispatcherServlet -> Interceptor
4. Interceptor -> AOP
5. AOP -> Controller

이후 역순으로 진행된다.

따라서 AOP는 인터셉터 이후, 컨트롤러 이전에 처리된다.
즉, Logging에 효과적으로 사용될 수 있다.

이번 과제에서 나의 목표는 changeUserRole()AOP를 활용하여 요청/응답 데이터를 Logging하는 것이다.
UserAdminServicechangeUserRole()를 처리하기 위해 @Around를 사용하여 UserControllerchangeUserRole 메서드를 AOP 적용 대상으로 설정한다.
LoggingAspect 클래스를 만들어 해당 클래스에서 Logging 처리를 구현한다.
Logging에 들어갈 정보

  • 요청한 사용자의 ID
  • API 요청 시각
  • API 요청 URL
  • 요청 본문(RequestBody)
  • 응답 본문(ResponseBody)

해당 정보를 log에 기록하고 메서드를 종료한다.

LoggingAspect.class

@Aspect
@Component
@RequiredArgsConstructor
public class LoggingAspect {

    private final HttpServletRequest request;
    private final ObjectMapper objectMapper = new ObjectMapper();
    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

    @Pointcut("execution(* org.example.expert.domain.user.controller.UserAdminController.changeUserRole(..))")
    public void pointCut() {
    }


    @Around("pointCut()")
    public Object loggingMethodCall(ProceedingJoinPoint joinPoint) {
        Method method = getMethod(joinPoint);
        logger.info("===Admin Access: {}===)", method.getName());
        return logReturn(joinPoint);
    }
    
    //로그 기록 위한 메서드
    private Object logReturn(ProceedingJoinPoint joinPoint) {
        String userId = String.valueOf(request.getAttribute("userId"));
        String requestUrl = request.getRequestURI();
        LocalDateTime requestTime = LocalDateTime.now();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();

        //requestBody JSON -> String
        String requestBody = Arrays.stream(args)
                .map(arg -> {
                    try {
                        return objectMapper.writeValueAsString(arg);
                    } catch (Exception e) {
                        return "Failed to convert request body to JSON";
                    }
                })
                .toList()
                .toString();

        //실행
        Object result;
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            logger.error("[ERROR] {} - Message: {}", methodName, e.getMessage(), e);
            throw new RuntimeException(e);
        }

        //실행 후 결과 값인 responseBody JSON -> String
        String responseBody;
        try {
            responseBody = objectMapper.writeValueAsString(result);
        } catch (Exception e) {
            responseBody = "Failed to convert response body to JSON";
        }

        logger.info("User Id: {}, Request Time: {}, Request URL: {}, RequestBody: {}, ResponseBody: {}", userId,
                requestTime,
                requestUrl, requestBody, responseBody);

        return result;
    }

    private Method getMethod(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        return signature.getMethod();
    }
}

pointCut을 통해 특정 메서드에서만 실행 될 수 있도록 한다.
@Around를 사용하여 특정 메서드가 들어올 경우 로그를 남길 수 있도록 한다.
logReturn 메서드는 RequestBody 본문과 ResponseBody 본문을 로그로 출력하기 위하여 따로 메서드를 분리하여 구현하였다.


로그가 잘 찍히는 것을 확인할 수 있다.

profile
문제 해결을 좋아하는 개발자 입니다 :)

0개의 댓글