[Spring] AOP(Aspect Oriented Programming) 컨트롤러 진입 전 공통 코드 넣기

이지연·2026년 1월 30일

API를 만들다 보면 “모든 요청에서 공통으로 하고 싶은 일”이 생긴다. 예를 들면 요청/응답 로그를 남기거나, API별 응답 시간을 재서 병목 구간을 찾는 작업이다. 이런 코드를 각 컨트롤러/서비스 메서드마다 복붙하면 중복이 폭발하고, 핵심 로직이 지저분해진다. 이 문제를 해결하는 대표적인 방법이 AOP(관점 지향 프로그래밍) 이다.


AOP가 필요한 이유: 공통 관심사 vs 핵심 관심사

각 API의 핵심 기능(주문 생성, 상품 조회 등)은 “핵심 관심사(core concern)”다. 반면 다음 같은 기능은 대부분의 API에 공통으로 붙는다.

  • 모든 요청의 로그 남기기
  • 응답 시간 측정(성능 모니터링)
  • 인증/인가 체크
  • 예외 로깅, 트랜잭션 처리 등

이런 것들을 “공통 관심사(cross-cutting concern)”라고 부른다.
AOP의 주된 목적은 공통 관심사와 핵심 관심사를 분리해서, 비즈니스 코드(핵심 로직)를 깔끔하게 유지하는 것이다.


“컨트롤러 앞에서 공통 코드”는 어디에 두면 좋을까?

스프링에서 요청이 들어왔을 때 공통 처리를 할 수 있는 지점은 여러 개다.

  • 필터(Filter): HTTP 요청이 서블릿/스프링으로 들어올 때 가장 앞단에서 동작
  • 인터셉터(Interceptor): 스프링 MVC 레벨에서 컨트롤러 전/후로 동작
  • AOP: 컨트롤러/서비스/레포지토리 등 메서드 호출 전/후 어디든 적용 가능

여기서 핵심은 “유연성”이다.

  • 필터는 보통 목적이 꽤 명확하다(인코딩, 인증 토큰, CORS 같은 웹 레벨 공통 처리).
  • AOP는 컨트롤러 앞, 서비스 앞, 레포지토리 앞 등 원하는 계층에 적용 가능하고,
    “특정 컨트롤러 A~F 중 A,B,C만”처럼 선택적으로 낚아채는 구조를 만들기 쉽다.

이런 이유로, “모든 컨트롤러 진입 직전에 공통코드” 같은 요구에 AOP가 자주 사용된다. (인터셉터라는 표현을 같이 쓰기도 하는데, 핵심 아이디어는 ‘중간에서 가로채기’다.)


AOP는 어떻게 “가로채는가”: 프록시(Proxy) 개념

AOP가 가능한 이유는 프록시(Proxy) 기술 때문이다.

  • 프록시는 “진짜 객체 대신 앞에 서 있는 대리 객체(가짜 객체)”다.
  • 클라이언트(요청 흐름)는 진짜 객체를 호출한다고 생각하지만, 실제로는 프록시를 통해 호출한다.
  • 프록시는 메서드 호출을 가로챈 뒤,
    1) 부가 로직(로그/성능 측정 등)을 실행하고
    2) 그 다음에 실제 객체의 메서드를 호출한다.

즉, “사용자의 요청을 스프링한테 혼선을 줘서(= 진짜가 아니라 프록시를 보게 해서) 중간에서 낚아채는” 방식이 AOP의 본질이다.


용어 정리: Join Point / Pointcut / Advice

AOP를 이해하려면 용어가 먼저 정리되면 좋다.

  • Join Point
    관점 코드(부가기능)가 적용될 수 있는 “실행 지점”이다. 실무에서는 보통 “어떤 메서드 실행 시점”으로 이해하면 된다.

  • Pointcut
    “어떤 Join Point에 적용할지”를 고르는 규칙이다.
    예: 특정 패키지의 컨트롤러만, 특정 어노테이션이 붙은 메서드만 등.

  • Advice
    실제로 실행되는 부가 로직이다.
    실행 시점에 따라 Before/After/Around 같은 형태가 있다.

그리고 가장 많이 쓰는 흐름이 이거다.

  • join point before → join point(실제 메서드 실행) → join point after

특히 성능 측정/로깅은 보통 @Around로 한 번에 처리하는 게 편하다.


Spring AOP에서 자주 쓰는 어노테이션

@Aspect

이 클래스가 “AOP 관점(부가기능 모듈)”임을 나타낸다.
즉, 로깅/성능 측정 같은 공통 관심사를 모아둔 클래스라는 선언이다.

@Pointcut

어드바이스가 적용될 위치(메서드/클래스/패키지)를 정의한다.
“어디를 낚아챌지” 정하는 필터 규칙 같은 느낌이다.

@Around

메서드 실행 전/후를 모두 감싸서 실행한다.
성능 측정(시작 시간/종료 시간), 공통 로깅, 예외 로깅 같은 작업에 특히 적합하다.


AOP로 로깅/성능측정 구현 절차(흐름)

1) @Aspect를 클래스 레벨에 선언한다.
2) 스프링 빈으로 등록되도록 @Component(또는 @Configuration + @Bean)로 등록한다.
3) 공통 관심사 메서드에 @Around를 붙이고 적용 범위를 설정한다(패키지/어노테이션/클래스 등).
4) 어드바이스 내부에서 joinPoint.proceed()를 호출한다.

  • 이 호출이 실제 비즈니스 메서드(조인 포인트)를 실행시키는 트리거다.
  • proceed() 전후로 로그/시간 측정 코드를 넣으면 된다.

예시: 컨트롤러 진입 전후 성능 측정 & 로그(Around)

아래는 “컨트롤러 메서드 실행 전/후로 시간을 재고 로그를 남기는” 전형적인 형태다.
(개념 설명용으로 간단히 작성)

@Aspect
@Component
public class ApiLoggingAspect {

    @Around("execution(* com.example..controller..*(..))")
    public Object logAndMeasure(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();

        // 진입 전 로그(원하면 요청 파라미터/메서드명 등 기록)
        String methodName = joinPoint.getSignature().toShortString();
        System.out.println("[ENTER] " + methodName);

        try {
            // 실제 컨트롤러 메서드 실행(핵심 로직)
            Object result = joinPoint.proceed();
            return result;
        } finally {
            long elapsed = System.currentTimeMillis() - start;
            // 종료 후 로그(응답 시간)
            System.out.println("[EXIT ] " + methodName + " took " + elapsed + "ms");
        }
    }
}

이렇게 하면 컨트롤러 코드에는 성능 측정/로그 코드가 전혀 없는데도, 모든 요청에 대해 공통으로 동작한다.
또한 Pointcut만 바꾸면 “특정 컨트롤러만”, “특정 어노테이션 붙은 API만”처럼 적용 대상을 정교하게 조절할 수 있다.


마무리: AOP를 언제 쓰면 좋은가?

  • 컨트롤러/서비스/레포지토리 등 여러 계층에 걸쳐 “반복되는 공통 로직”이 있을 때
  • 로깅, 성능 측정, 인증/보안처럼 핵심 기능이 아니지만 반드시 필요한 부가기능을 분리하고 싶을 때
  • 특정 대상(A,B,C)에만 선택적으로 적용하고 싶을 때

반대로 “HTTP 레벨에서 무조건 처리해야 하는 일”이라면 필터/인터셉터가 더 자연스러운 경우도 많다.
하지만 “메서드 실행 전후를 유연하게 낚아채서 공통 코드를 넣고 싶다”면 AOP는 강력한 선택지다.

profile
Eazy하게

0개의 댓글