2. Spring AOP 동작 원리 (Proxy 패턴)

2.1 JDK Dynamic Proxy vs CGLIB

JDK Dynamic Proxy (인터페이스 기반):

// 1. 인터페이스 정의
public interface UserService {
    User findUser(Long id);
}

// 2. 실제 구현체
@Service
public class UserServiceImpl implements UserService {
    @Override
    public User findUser(Long id) {
        return userRepository.findById(id);
    }
}

// 3. Spring이 생성하는 Proxy
public class UserServiceProxy implements UserService {
    private UserServiceImpl target;

    @Override
    public User findUser(Long id) {
        // Before Advice
        log.info("Before findUser");

        // Target 메서드 호출
        User result = target.findUser(id);

        // After Advice
        log.info("After findUser");

        return result;
    }
}

// 4. Bean 주입 시 Proxy가 주입됨
@Autowired
private UserService userService;  // ← Proxy 주입 (실제 타입: UserServiceProxy)

CGLIB (클래스 기반):

// 1. 구체 클래스만 있음 (인터페이스 없음)
@Service
public class UserService {
    public User findUser(Long id) {
        return userRepository.findById(id);
    }
}

// 2. Spring이 생성하는 Proxy (CGLIB)
public class UserService$$EnhancerBySpringCGLIB extends UserService {
    private UserService target;

    @Override
    public User findUser(Long id) {
        // Before Advice
        log.info("Before findUser");

        // Target 메서드 호출
        User result = super.findUser(id);  // 부모 클래스 호출

        // After Advice
        log.info("After findUser");

        return result;
    }
}

Proxy 선택 기준:

JDK Dynamic Proxy:
- 인터페이스가 있는 경우
- 빠른 Proxy 생성
- Java 표준 기술

CGLIB:
- 인터페이스가 없는 경우
- 클래스 상속으로 Proxy 생성
- @Configuration 클래스 (Spring은 CGLIB 사용)
- Spring Boot 2.0+: 기본값

설정:
spring.aop.proxy-target-class=true  # CGLIB 강제 사용
spring.aop.proxy-target-class=false # JDK Proxy 사용 (인터페이스 있을 때)

2.2 Proxy 확인 방법

@SpringBootTest
public class ProxyTest {
    @Autowired
    private UserService userService;

    @Test
    public void checkProxy() {
        System.out.println("Proxy class: " + userService.getClass());
        // 출력: com.example.service.UserService$$EnhancerBySpringCGLIB$$12345

        // Proxy 여부 확인
        boolean isProxy = AopUtils.isAopProxy(userService);
        System.out.println("Is Proxy: " + isProxy);  // true

        // CGLIB Proxy 확인
        boolean isCglibProxy = AopUtils.isCglibProxy(userService);
        System.out.println("Is CGLIB Proxy: " + isCglibProxy);  // true
    }
}

3. Pointcut Expression (포인트컷 표현식)

3.1 execution 지시자

기본 문법:

execution(modifiers? return-type declaring-type?method-name(param-types) throws?)

modifiers: public, private 등 (생략 가능)
return-type: 반환 타입 (* = 모든 타입)
declaring-type: 패키지 및 클래스 (생략 가능)
method-name: 메서드 이름 (* = 모든 메서드)
param-types: 파라미터 타입 (.. = 모든 파라미터)
throws: 예외 타입 (생략 가능)

예시:

@Aspect
@Component
public class PointcutExamples {

    // 1. 모든 public 메서드
    @Around("execution(public * *(..))")
    public Object allPublicMethods(ProceedingJoinPoint joinPoint) { }

    // 2. com.example.service 패키지의 모든 메서드
    @Around("execution(* com.example.service.*.*(..))")
    public Object allServiceMethods(ProceedingJoinPoint joinPoint) { }

    // 3. com.example.service 패키지와 하위 패키지의 모든 메서드
    @Around("execution(* com.example.service..*.*(..))")
    public Object allServiceAndSubPackages(ProceedingJoinPoint joinPoint) { }

    // 4. UserService의 모든 메서드
    @Around("execution(* com.example.service.UserService.*(..))")
    public Object allUserServiceMethods(ProceedingJoinPoint joinPoint) { }

    // 5. 메서드 이름이 find로 시작하는 모든 메서드
    @Around("execution(* find*(..))")
    public Object allFindMethods(ProceedingJoinPoint joinPoint) { }

    // 6. 파라미터가 Long 타입 1개인 메서드
    @Around("execution(* *(Long))")
    public Object methodsWithLongParam(ProceedingJoinPoint joinPoint) { }

    // 7. 파라미터가 Long으로 시작하는 메서드
    @Around("execution(* *(Long, ..))")
    public Object methodsStartingWithLong(ProceedingJoinPoint joinPoint) { }

    // 8. User 타입을 반환하는 모든 메서드
    @Around("execution(com.example.domain.User *(..))")
    public Object methodsReturningUser(ProceedingJoinPoint joinPoint) { }
}

3.2 @annotation 지시자

Custom Annotation 정의:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}

Aspect 적용:

@Aspect
@Component
public class ExecutionTimeAspect {

    @Around("@annotation(LogExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();

        Object result = joinPoint.proceed();

        long endTime = System.currentTimeMillis();
        long executionTime = endTime - startTime;

        log.info("{} executed in {}ms",
            joinPoint.getSignature(),
            executionTime
        );

        return result;
    }
}

사용:

@Service
public class UserService {

    @LogExecutionTime  // ✅ Aspect 적용
    public User findUser(Long id) {
        return userRepository.findById(id);
    }

    public void createUser(User user) {
        userRepository.save(user);  // ❌ Aspect 미적용
    }
}

3.3 Pointcut 조합

@Aspect
@Component
public class CombinedPointcuts {

    // Pointcut 정의
    @Pointcut("execution(* com.example.service..*(..))")
    public void serviceLayer() {}

    @Pointcut("execution(* com.example.repository..*(..))")
    public void repositoryLayer() {}

    @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
    public void transactionalMethod() {}

    // 조합 1: AND (&&)
    @Around("serviceLayer() && transactionalMethod()")
    public Object serviceAndTransactional(ProceedingJoinPoint joinPoint) {
        // Service 계층의 @Transactional 메서드에만 적용
    }

    // 조합 2: OR (||)
    @Around("serviceLayer() || repositoryLayer()")
    public Object serviceOrRepository(ProceedingJoinPoint joinPoint) {
        // Service 또는 Repository 계층에 적용
    }

    // 조합 3: NOT (!)
    @Around("serviceLayer() && !transactionalMethod()")
    public Object serviceNotTransactional(ProceedingJoinPoint joinPoint) {
        // Service 계층 중 @Transactional이 없는 메서드
    }
}


📚 다음 편: 준비 중입니다.


👈 이전 편: Spring AOP (Part 1: 개념과 기초)