[Spring] AOP 사용해서 API 로깅하기

NCOOKIE·2025년 4월 18일

내일배움캠프

목록 보기
2/3

개요

어드민 사용자만 접근할 수 있는 특정 API에 접근할 때마다 접근 로그를 기록하려고 한다.

이를 위해 AOP를 사용해서 로깅 기능을 구현했다.

구현

로깅 시 아래 내용들을 포함시키려고 한다.

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

@LogAdmin이라는 커스텀 어노테이션이 붙어있는 컨트롤러의 메서드에서만 로그가 찍히도록 하기 위해서 @Around("@annotation(org.example.expert.aop.LogAdmin)")와 같이 사용했다.

유저 정보는 JwtFileter에서 인증 성공 시 httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject()));로 값을 넣어주고 있기 때문에 request.getAttributes("userId")로 가져올 수 있다.

AdminApiLoggingAspect.java

@Aspect
@Component
@RequiredArgsConstructor
public class AdminApiLoggingAspect {

    private final ObjectMapper objectMapper;
    private static final Logger logger = LogManager.getLogger(AdminApiLoggingAspect.class);

    @Around("@annotation(org.example.expert.aop.LogAdmin)")
    public Object logAdminApi(ProceedingJoinPoint joinPoint) throws Throwable {

        HttpServletRequest httpRequest = getHttpRequest();

        if (httpRequest == null) {
            // request 또는 response 값이 null 이면 중간에 종료
            // @Around는 메서드 실행을 감싸는 래퍼이므로 proceed()를 호출해야 원래 메서드가 실행됨
            logger.warn("HttpServletRequest 객체를 가져올 수 없음");
            return joinPoint.proceed();
        }

        String methodName = joinPoint.getSignature().getName();

        Long userId = (Long) httpRequest.getAttribute("userId");            // 요청한 사용자의 ID
        LocalDateTime requestTime = LocalDateTime.now();                    // API 요청 시각
        String requestURI = httpRequest.getRequestURI();                    // API 요청 URL
        String requestBody = getRequestBody(joinPoint);                     // 요청 본문

        logger.info("[Admin API Logging]");
        logger.info("Request 정보: Method: {}, URI: {}, time: {}, userId: {}, RequestBody: {}",
                methodName,
                requestURI,
                requestTime,
                userId,
                requestBody
        );

        Object result;
        try {
            result = joinPoint.proceed();     // 실제 API 실행
        } catch (Exception e) {
            logger.error("에러 발생!! : Method: {}, URI: {}, message: {}",
                    methodName,
                    requestURI,
                    e.getMessage()
            );
            throw e;    // 처리는 ControllerAdvice 에게 위임
        }

        String responseString = convertObjectToJson(result);
        String responseBody = extractBodyFromJson(responseString);

        logger.info("Response 정보: Method: {}, URI: {}, ResponseBody: {}",
                methodName,
                requestURI,
                responseBody
        );

        return result;
    }

    private HttpServletRequest getHttpRequest() {
        ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return attr != null ? attr.getRequest() : null;
    }

    private String getRequestBody(ProceedingJoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        if (args.length > 0) {
            try {
                return Arrays.stream(args)
                        .map(this::convertObjectToJson)
                        .reduce((arg1, arg2) -> arg1 + ", " + arg2)
                        .orElse("");
            } catch (Exception e) {
                logger.error("Error serializing request body", e);
            }
        }
        return "";
    }

    private String convertObjectToJson(Object object) {
        if (object == null)
            return "";
        try {
            return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
        } catch (JsonProcessingException e) {
            logger.error("Error serializing object to JSON", e);
            return "Error serializing object to JSON";
        }
    }

    private String extractBodyFromJson(String json) {
        if (json == null || json.isBlank()) return "";
        try {
            JsonNode root = objectMapper.readTree(json);
            JsonNode body = root.path("body");
            return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(body);
        } catch (JsonProcessingException e) {
            logger.error("Error extracting body from JSON", e);
            return "Error extracting body from JSON";
        }
    }

}

LogAdmin.java

/**
 * Logging 작업을 수행할 Admin API 메서드에 붙여서 사용
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAdmin {
}

적용 예시

@LogAdmin
@DeleteMapping("/admin/comments/{commentId}")
public void deleteComment(@PathVariable long commentId) {
    commentAdminService.deleteComment(commentId);
}

참고

profile
일단 해보자

0개의 댓글