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
↓
실제 로직 실행
즉,
- 요청 전체를 제어하는 게 아니라
- 특정 메서드 실행 전/후에 개입한다
실제 구현
가장 먼저 적용한 건 실행 시간 측정이었다.
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가:
- 실행 전 로직 수행
- 실제 메서드 호출
- 실행 후 로직 수행
이 과정을 담당한다.
구현하면서 헷갈렸던 부분
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는 요청을 제어하는 기술이 아니라, 비즈니스 로직에서 공통 관심사를 분리하는 기술에 더 가깝다.

