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이 없는 메서드
}
}
📚 다음 편: 준비 중입니다.
💬 댓글