로깅·트랜잭션·보안처럼 여러 곳에 흩어지는 공통 관심사를 따로 모아 필요한 지점에 끼워 넣는 기법(Aspect-Oriented Programming).
무엇을 해결하나
// AOP 없이 — 모든 메서드에 같은 코드가 반복된다
public void createOrder() {
long start = System.currentTimeMillis(); // 측정
tx.begin(); // 트랜잭션
try {
checkPermission(); // 보안
// ↓ 진짜 하고 싶은 일은 이 한 줄
repository.save(order);
tx.commit();
} catch (Exception e) { tx.rollback(); throw e; }
finally { log.info("{}ms", System.currentTimeMillis() - start); }
}
핵심 로직이 부가 코드에 묻힌다. 그리고 이 패턴이 수백 개 메서드에 복붙된다.
// AOP 적용 후
@Transactional
public void createOrder() {
repository.save(order);
}
용어
| 용어 | 뜻 |
|---|---|
| Aspect | 흩어진 관심사를 모은 모듈 (트랜잭션 처리 전체) |
| Advice | 실제로 끼워 넣을 동작 (@Before, @Around …) |
| Join Point | 끼워 넣을 수 있는 지점 (스프링은 메서드 실행만) |
| Pointcut | 그중 실제로 적용할 지점을 고르는 표현식 |
| Weaving | 끼워 넣는 과정 |
스프링 AOP는 프록시 기반이다 — 모든 한계의 근원
클라이언트 ──► [프록시] ──► 실제 객체
│
└─ 앞뒤로 부가 기능 실행
컨테이너가 빈을 만들 때 대신 프록시 객체를 등록한다. 클라이언트는 자기가 프록시를 쓰는지 모른다.
| 방식 | 조건 |
|---|---|
| JDK 동적 프록시 | 인터페이스가 있을 때. 인터페이스를 구현한 프록시 생성 |
| CGLIB | 인터페이스가 없을 때. 상속으로 프록시 생성 |
스프링 부트는 기본적으로 CGLIB를 쓴다(proxyTargetClass=true).
그래서 final 클래스·final 메서드에는 AOP가 걸리지 않는다 —
상속할 수 없기 때문이다.
가장 유명한 함정 — 자기 호출(self-invocation)
@Service
public class OrderService {
public void outer() {
inner(); // ← this.inner() 이다. 프록시를 거치지 않는다!
}
@Transactional
public void inner() { … } // 트랜잭션이 걸리지 않는다 ✗
}
왜 안 되는가
클라이언트 → [프록시] → 실제객체.outer()
│
└─ this.inner() ← 프록시가 아니라 자기 자신
부가 기능이 개입할 틈이 없다
@Transactional, @Cacheable, @Async, @Retryable 모두 같은 문제를 겪는다.
"어노테이션을 붙였는데 동작하지 않는다" 의 원인 1위다.
해결
① 클래스를 분리한다 (가장 깔끔)
② 자기 자신을 주입받는다 — @Lazy private OrderService self;
③ AopContext.currentProxy() 사용 (설정 필요, 권장하지 않음)
private 메서드에도 안 걸린다
CGLIB는 상속으로 프록시를 만드는데 private 메서드는 오버라이드할 수 없다.
public 메서드에 붙여야 한다.
실무 활용
@Around("@annotation(com.example.Timed)")
public Object measure(ProceedingJoinPoint pjp) throws Throwable {
long t = System.nanoTime();
try { return pjp.proceed(); }
finally { log.info("{} {}ms", pjp.getSignature(), (System.nanoTime()-t)/1_000_000); }
}
| 용도 | 비고 |
|---|---|
| 트랜잭션 | @Transactional — 스프링 AOP의 대표 사례 |
| 캐시 | @Cacheable |
| 보안 | @PreAuthorize |
| 성능 측정·로깅 | 직접 작성 |
| 재시도 | @Retryable |
남용하지 않는다
AOP는 코드에 보이지 않는 동작을 추가한다. 남발하면 "왜 이렇게 동작하지?"를 추적하기 어려워진다.
✅ 진짜 횡단 관심사(트랜잭션·로깅·보안)에만 ❌ 비즈니스 로직을 AOP로 숨기지 않는다