본문 바로가기
Spring/- [시리즈] Spring 프로젝트 되살리기

Spring 프로젝트 되살리기 9 - API 로깅 기능 구현 (AOP)

by 정구정구 2026. 7. 27.

이전 이야기

https://record47584.tistory.com/94

 

Spring 프로젝트 되살리기 8 - 인가(Authorization) 인터셉터 구현

이전 이야기https://record47584.tistory.com/93 Spring 프로젝트 되살리기 7 - 리팩토링 4 (record, 정적 팩토리 메서드)이전 이야기https://record47584.tistory.com/91 Spring 프로젝트 되살리기 6 - 유닛 테스트 코드 고

record47584.tistory.com

 

 

시나리오 9

관리자 api 기능을 요청했을 때, 요청 시간과 요청자 id, request/response body 값을 로깅해달라는 요청이 들어왔다.

 

AOP를 이용해 이를 구현해보자

 

(AOP에 대한 개념과 구현 방법은 아래 글을 참고)

https://record47584.tistory.com/95

 

AOP(Aspect-Oriented Programming)의 개념과 구현

AOP(Aspect-Oriented Programming)란? 여러 클래스에 공통으로 필요한 로직(로깅, 트랜잭션, 실행시간 측정 등)을 비즈니스 로직과 분리해서 한 곳에 모아두는 방식만약 모든 api의 실행 시간을 체크해야

record47584.tistory.com

 

 

관리자 로깅 AOP 구현

@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class AdminAop {

    private final TimeUtil timeUtil;
    private final ObjectMapper objectMapper;

    @Around("execution(* org.example.expert.domain..service..*AdminService.*(..))")
    public Object logAdminRequest(ProceedingJoinPoint joinPoint) throws Throwable {

        HttpServletRequest request =
                ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();

        // 시간
        long startTime = System.currentTimeMillis();

        // 데이터
        Long userId = (Long) request.getAttribute("userId");
        String requestUrl = request.getRequestURI();
        String requestBody = extractRequestBody(joinPoint.getArgs());
        String methodName = joinPoint.getSignature().toShortString();

        log.info("[Admin API 요청] userId={}, time={}, url={}, method={}, requestBody={}",
                userId, timeUtil.toReadableTime(startTime), requestUrl, methodName, requestBody);

        // 실제 메서드 실행 -> Filter에서 doFilter 와 비슷함.
        Object result = joinPoint.proceed();

        String responseBody = toJson(result); // void면 null이 그대로 찍힘
        log.info("[Admin API 응답] userId={}, url={}, responseBody={}",
                userId, requestUrl, responseBody);

        return result;
    }

    private String extractRequestBody(Object[] args) {
        for (Object arg : args) {
            if (arg != null && !isSimpleType(arg)) {
                return toJson(arg); // DTO가 있으면 그대로
            }
        }
        // DTO가 없으면(commentId처럼 단순 타입만 있는 경우) 파라미터 전체를 JSON으로 남김
        return toJson(args);
    }

    private boolean isSimpleType(Object arg) {
        return arg instanceof Long || arg instanceof Integer
                || arg instanceof String || arg instanceof Boolean;
    }

    private String toJson(Object obj) {
        try {
            return objectMapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            log.warn("JSON 직렬화 실패: {}", e.getMessage());
            return "직렬화 실패";
        }
    }
}

 

구현 내용

  • @Around("execution(* org.example.expert.domain..service..*AdminService.*(..))") : UserAdminService, CommentAdminService에 적용됨. (추후 TodoAdminService 등이 추가된다면 거기도 자동 적용됨)
  • userId : JwtFilter가 미리 request.setAttribute("userId", ...)로 심어둔 값을 꺼냄
  • requestUrl :  현재 스레드에 바인딩된 HttpServletRequest에서 URL 추출
  • requestBody : joinPoint.getArgs()(메서드 파라미터 배열)에서 DTO로 보이는 값을 찾아 JSON화
  • methodName : joinPoint.getSignature()로 어떤 메서드 정보 확인

(더 자세한 코드 설명은 아래 글 참고)

https://record47584.tistory.com/95

 

AOP(Aspect-Oriented Programming)의 개념과 구현

AOP(Aspect-Oriented Programming)란? 여러 클래스에 공통으로 필요한 로직(로깅, 트랜잭션, 실행시간 측정 등)을 비즈니스 로직과 분리해서 한 곳에 모아두는 방식만약 모든 api의 실행 시간을 체크해야

record47584.tistory.com

 

 

결과

10:16:41.691 [INFO ] : [Admin API 요청] userId=2, time=2026-07-27 10:16:41.690, url=/admin/users/4, method=UserAdminService.changeUserRole(..), requestBody={"role":"ADMIN"}

10:16:41.711 [INFO ] : [Admin API 응답] userId=2, url=/admin/users/4, responseBody=null