백엔드 면접 용어 사전
Spring·JPA

AOP

로깅·트랜잭션처럼 여러 곳에 흩어지는 관심사를 분리해 주입하는 기법. 스프링은 프록시로 구현한다.

로깅·트랜잭션·보안처럼 여러 곳에 흩어지는 공통 관심사를 따로 모아 필요한 지점에 끼워 넣는 기법(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로 숨기지 않는다

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — Spring·JPA — IoC/DI·AOP·영속성 컨텍스트·N+1