포스트

AOP를 이해하면서 정리한 생각 — 공통 로직은 어디에 있어야 할까

AOP를 이해하면서 정리한 생각 — 공통 로직은 어디에 있어야 할까

들어가면서

최근 Filter, Interceptor를 정리하면서 “공통 로직을 어디에서 처리해야 하는가”를 계속 고민하게 됐다.

  • Filter → 요청 입구
  • Interceptor → MVC 흐름 제어

그리고 마지막으로 남은 게 AOP였다.

👉 Filter vs Interceptor vs AOP — 어디서 무엇을 처리해야 할까

처음에는 AOP도 단순히 “공통 코드 분리 기술” 정도로 생각했다.

그런데 직접 적용해보니 AOP는 요청 흐름을 다루는 기술이라기보다,

“비즈니스 로직에 공통 관심사를 분리하는 방식”

에 더 가까웠다.


처음에는 왜 필요한지 잘 몰랐다

처음엔 이런 생각이었다.

“메서드 안에 로그 찍으면 되는 거 아닌가?”

실제로 처음에는 서비스 메서드마다 직접 작성했다.

1
log.info("주문 생성 시작");

그런데 점점 반복이 생기기 시작했다.

  • 로그
  • 실행 시간 측정
  • 예외 처리
  • 트랜잭션

그리고 어느 순간부터 비즈니스 로직보다 “부가 로직”이 더 눈에 들어오기 시작했다.


AOP를 적용하고 나서 달라진 점

AOP를 적용하고 가장 크게 느낀 건 하나였다.

“비즈니스 로직이 다시 비즈니스 로직처럼 보이기 시작했다”


Before

1
2
3
4
5
6
7
8
9
10
11
12
13
public void createOrder() {

    log.info("주문 생성 시작");

    long start = System.currentTimeMillis();

    try {
        // 비즈니스 로직
    } finally {
        long end = System.currentTimeMillis();
        log.info("실행 시간 = {}", end - start);
    }
}

After

1
2
3
public void createOrder() {
    // 비즈니스 로직
}

부가 기능은 Aspect로 분리했다.


AOP는 어디에서 동작할까

Filter나 Interceptor는 요청 흐름과 관련 있다.

반면 AOP는:

메서드 실행 자체에 개입한다

흐름으로 보면 이런 느낌이다.

1
2
3
4
5
6
7
Controller
   ↓
Service Method
   ↓
AOP Proxy
   ↓
실제 로직 실행

즉,

  • 요청 전체를 제어하는 게 아니라
  • 특정 메서드 실행 전/후에 개입한다

AOP 동작 흐름


실제 구현

가장 먼저 적용한 건 실행 시간 측정이었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Aspect
@Component
@Slf4j
public class TimeTraceAspect {

    @Around("execution(* com.example..service.*.*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {

        long start = System.currentTimeMillis();

        try {
            return joinPoint.proceed();
        } finally {
            long end = System.currentTimeMillis();

            log.info("{} 실행 시간 = {}ms",
                    joinPoint.getSignature(),
                    end - start);
        }
    }
}

구현하면서 가장 신기했던 부분

처음에는 이게 이해가 잘 안 됐다.

“왜 서비스 코드를 안 건드렸는데 로그가 실행되지?”

그때 처음으로 Proxy 개념을 제대로 보게 됐다.


Spring AOP는 Proxy 기반으로 동작한다

Spring은 실제 객체를 직접 호출하지 않는다.

대신:

“Proxy 객체를 앞에 두고 중간에서 가로챈다”


흐름을 보면

1
2
3
4
5
Client
 ↓
Proxy
 ↓
실제 Service

Proxy가:

  • 실행 전 로직 수행
  • 실제 메서드 호출
  • 실행 후 로직 수행

이 과정을 담당한다.

Proxy 구조


구현하면서 헷갈렸던 부분

self invocation 문제

이건 실제로 꽤 헷갈렸다.

같은 클래스 내부에서 메서드를 호출하면:

1
this.internalMethod();

Proxy를 거치지 않는다.

즉,

AOP가 적용되지 않는다


왜 이런 일이 생길까

이 부분은 결국 Spring AOP가 “Proxy 기반”으로 동작하기 때문에 발생한다.

외부에서 호출할 때는:

Client → Proxy → Target

반드시 Proxy를 거친다.

그래서:

@Transactional 로깅 실행 시간 측정

같은 부가 기능이 적용된다.

하지만 내부 호출은 다르다.

Target → this.method()

같은 객체 내부에서 메서드를 호출하면 Proxy를 거치지 않고 직접 메서드를 호출한다.

즉,

AOP가 개입할 기회를 잃게 된다

처음에는 단순히:

“왜 AOP가 안 먹지?”

정도로 생각했는데, 구조를 따라가 보니 결국 핵심은 Proxy였다.

이 부분은 이전에 Proxy 구조를 정리하면서 조금 더 자세하게 다뤘다.

👉 Spring Proxy: 핵심 로직을 투명하게 감싸는 ‘기술적 안경’


느낀 점

AOP는 단순히 “공통 코드 분리”가 아니었다.

Proxy 런타임 위빙 호출 구조

같은 개념들이 모두 연결되어 있었다.

결국 AOP를 이해하려면 Proxy를 이해해야 했고,

Proxy를 이해하고 나니까 왜 Spring의 여러 기능들이 “메서드 호출 구조”에 의존하는지도 조금씩 보이기 시작했다.

그리고 나서야 왜 Spring이 트랜잭션도 AOP 기반으로 처리하는지 조금 이해되기 시작했다.


Filter / Interceptor / AOP를 다시 보면

정리하고 나니까 역할이 훨씬 명확해졌다.

Filter

  • 요청 입구
  • 인증 / 보안

👉 Filter를 이해하면서 정리한 생각 — 인증은 어디서 시작되는가


Interceptor

  • MVC 흐름 제어
  • Controller 전/후 처리

👉 Interceptor를 이해하면서 정리한 생각 — 요청 흐름은 어디서 제어되는가


AOP

  • 비즈니스 로직 공통 관심사 분리
  • 메서드 실행 제어

마무리

처음에는 셋 다 비슷해 보였다.

하지만 직접 구현하면서 느낀 건:

“공통 로직”도 어느 계층에서 처리하느냐에 따라 완전히 다른 기술이 된다

그리고 AOP를 이해하면서 Spring이 왜 Proxy 기반으로 동작하는지 조금씩 보이기 시작했다.


한 줄 정리

AOP는 요청을 제어하는 기술이 아니라, 비즈니스 로직에서 공통 관심사를 분리하는 기술에 더 가깝다.


References

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.