본문 바로가기
Spring/관련 지식

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

by 정구정구 2026. 7. 27.

AOP(Aspect-Oriented Programming)란?

여러 클래스에 공통으로 필요한 로직(로깅, 트랜잭션, 실행시간 측정 등)을 비즈니스 로직과 분리해서 한 곳에 모아두는 방식

  • 만약 모든 api의 실행 시간을 체크해야 한다면, 모든 api에 시간 측정 코드를 넣는게 아닌, 반복되는 부분을 별도 클래스로 빼서, "실행시켜야 할 대상"만 지정하면 자동으로 앞뒤에 끼어들게 만드는 방식
  • 우리가 자주 쓰는 @Transactional도 AOP로 구현됨 

 

API 전체 처리 경로

더보기

클라이언트 요청
    │
    ▼
① Filter (JwtFilter 등) - 요청 방향
    │  - request.setAttribute("userId", ...) 등 세팅
    │  - chain.doFilter() 호출
    ▼
② DispatcherServlet
    │  - HandlerMapping으로 어떤 Controller.method를 호출할지 결정
    ▼
③ Interceptor.preHandle (AdminInterceptor 등)
    │  - 권한 체크 등
    │  - false면 여기서 즉시 응답 방향으로 turn (Controller 호출 안 됨)
    ▼ (true인 경우만 진행)
④ ArgumentResolver (AuthUserArgumentResolver 등)
    │  - @Auth AuthUser 같은 커스텀 파라미터 조립
    │  - @Valid 검증도 이 시점에 함께 수행됨
    ▼
⑤ Controller
    │  - Service 메서드 호출
    ▼
⑥ AOP (@Around 등, AdminRequestLoggingAop, TimeCheckAop)
    │  - Service(또는 지정한 대상) 메서드 실행을 앞뒤로 감쌈
    │  - joinPoint.proceed() 호출 시점에 실제 Service 로직 실행
    ▼
⑦ Service → Repository → DB
    │
    ▼ (다시 위로 리턴하며 거슬러 올라감)
⑥' AOP 후처리
    │  - 실행시간 로깅, 응답 데이터 로깅 등
    ▼
⑤' Controller가 응답 객체 리턴
    ▼
③' Interceptor.postHandle
    │  - View 렌더링 전 (예외 없을 때만)
    ▼
   (예외 발생 시) GlobalExceptionHandler
    │  - @ExceptionHandler가 예외를 잡아서 응답으로 변환
    ▼
③'' Interceptor.afterCompletion
    │  - 예외 발생 여부와 무관하게 항상 실행
    │  - 로깅, 리소스 정리
    ▼
② DispatcherServlet이 최종 응답 작성 (JSON 직렬화 등)
    ▼
① Filter - 응답 방향
    │  - chain.doFilter() 이후 코드 실행 (응답 헤더 조작 등)
    ▼
클라이언트 응답

 

 

예시 코드 

전체 예시 코드

admin 권한이 필요한 모든 service 메서드에 대해서

처리 시간, 요청자 정보, request body, response body를 로깅하는 메서드 

@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 "직렬화 실패";
        }
    }
}

 

 

파트 별 설명

 

클래스 어노테이션

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

 

  • @Aspect: 이 클래스가 AOP의 공통 로직(Aspect)임을 선언

 

Advice - 언제, Pointcut - 무엇을

@Around("execution(* org.example.expert.domain..service..*AdminService.*(..))")

 

  • @Around : Advice라고 불리며, 어느 메서드의 어느 타이밍에 적용될지 결정
    • @Around : 실행 전후 모두
    • @Before : 실행 전만 
    • @AfterReturning : 정상 리턴 후 → 응답 값만 로깅하고 싶을 때
    • @AfterThrowing : 예외 발생 시 → 실패 케이스만 로깅하고 싶을 때
  • execution(~) : Pointcut 표현식이라고 불리며, 어느 메서드에 적용될지 결정
    • * (리턴 타입 자리) : 리턴 타입 무관
    • org.example.expert.domain..service.. : domain 하위 어디든, service라는 경로가 들어간 곳
    • *AdminService : 이름이 AdminService로 끝나는 모든 클래스 (UserAdminService, CommentAdminService 등)
    • .*(..) : 그 클래스의 모든 메서드, 파라미터 무관

 

데이터 정의

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();
  • RequestContextHolder.currentRequestAttributes()를 쓰는 이유 : AOP는 HTTP 요청 객체에 직접 접근할 방법이 없어서 현재 스레드에 바인딩된 요청 정보를 꺼내오는 우회 경로가 필요하기 때문

 

메서드 실행

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

 

 

응답 로깅

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

 

 

결과

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