🎯 AOP Advice & Advisor
개요
이 문서는 Sprout Framework의 AOP(Aspect-Oriented Programming) 핵심 구성 요소인 Advice와 Advisor 시스템에 대한 심층적인 기술 분석을 제공합니다. 어드바이스의 생성과 저장, 포인트컷 매칭 전략, 그리고 어드바이저 레지스트리의 내부 구조를 상세히 분석하여 Sprout AOP의 설계 철학과 구현 메커니즘을 이해할 수 있도록 합니다.
AOP 아키텍처 개요
핵심 구성 요소 관계도
@Aspect 클래스
↓
AdviceFactory → AdviceBuilder → Advice (인터셉터)
↓ ↓ ↓
DefaultAdvisor ← Pointcut ← PointcutFactory
↓
AdvisorRegistry (저장 및 매칭)
↓
프록시 생성 시 사용
주요 컴포넌트 역할
- Advice: 실제 부가 기능을 실행하는 인터셉터
- Advisor: Advice + Pointcut + 실행 순서를 담는 컨테이너
- Pointcut: 어드바이스가 적용될 조인 포인트를 결정하는 조건
- AdviceFactory: 어노테이션을 분석하여 적절한 Advisor 생성
- AdvisorRegistry: 생성된 Advisor들을 저장하고 메서드별 적용 가능한 Advisor 검색
Advice 시스템 분석
1. Advice 인터페이스: 통합된 인터셉션 모델
단순하고 강력한 인터페이스
public interface Advice {
Object invoke(MethodInvocation invocation) throws Throwable;
}
설계 특징
- 단일 메서드: 모든 어드바이스 타입이 동일한 시그니처 사용
- MethodInvocation 기반: Spring의 Interceptor 패턴과 유사
- 예외 투명성: Throwable을 통한 모든 예외 전파
- 체이닝 지원:
invocation.proceed()
를 통한 다음 어드바이스 호출
2. AdviceType: 어드바이스 타입 분류 시스템
열거형 기반 타입 관리
public enum AdviceType {
AROUND(Around.class),
BEFORE(Before.class),
AFTER(After.class);
private final Class<? extends Annotation> anno;
public static Optional<AdviceType> from(Method m) {
return Arrays.stream(values())
.filter(t -> m.isAnnotationPresent(t.anno))
.findFirst();
}
}
핵심 설계 결정
- 어노테이션과 타입 매핑: 각 AdviceType이 해당 어노테이션 클래스 보유
- 스트림 기반 검색: Java 8+ 스트림 API로 간결한 타입 탐지
- Optional 반환: null 안전성 보장
- 확장 가능성: 새로운 어드바이스 타입 추가 용이
3. AdviceFactory: 어드바이스 생성의 중앙 집권화
팩토리 패턴과 전략 패턴 결합
@Component
public class AdviceFactory implements InfrastructureBean {
private final Map<AdviceType, AdviceBuilder> builders;
private final PointcutFactory pointcutFactory;
public AdviceFactory(PointcutFactory pointcutFactory) {
this.pointcutFactory = pointcutFactory;
this.builders = Map.of(
AdviceType.AROUND, new AroundAdviceBuilder(),
AdviceType.BEFORE, new BeforeAdviceBuilder(),
AdviceType.AFTER, new AfterAdviceBuilder()
);
}
public Optional<Advisor> createAdvisor(Class<?> aspectCls, Method m, Supplier<Object> sup) {
return AdviceType.from(m)
.map(type -> builders.get(type).build(aspectCls, m, sup, pointcutFactory));
}
}
아키텍처 특징
- 불변 빌더 맵:
Map.of()
를 통한 컴파일 타임 빌더 매핑 - 의존성 주입: PointcutFactory를 생성자 주입으로 받음
- 타입 안전성: 제네릭과 Optional을 통한 타입 안전성 보장
- 단일 책임: 어드바이스 생성만 담당, 실제 구현은 빌더에 위임
4. AdviceBuilder 구현체들
BeforeAdviceBuilder: 사전 처리 어드바이스
파라미터 검증과 빌더 생성
public class BeforeAdviceBuilder implements AdviceBuilder {
@Override
public Advisor build(Class<?> aspectCls, Method method, Supplier<Object> aspectSup, PointcutFactory pf) {
Before before = method.getAnnotation(Before.class);
// 1. 파라미터 검증
if (method.getParameterCount() > 1 ||
(method.getParameterCount() == 1 &&
!JoinPoint.class.isAssignableFrom(method.getParameterTypes()[0]))) {
throw new IllegalStateException("@Before method must have 0 or 1 JoinPoint param");
}
// 2. 포인트컷 생성
Pointcut pc = pf.createPointcut(before.annotation(), before.pointcut());
// 3. static 메서드 처리
Supplier<Object> safe = Modifier.isStatic(method.getModifiers()) ? () -> null : aspectSup;
// 4. 어드바이스와 어드바이저 생성
Advice advice = new SimpleBeforeInterceptor(safe, method);
return new DefaultAdvisor(pc, advice, 0);
}
}
AroundAdviceBuilder: 완전한 제어 어드바이스
ProceedingJoinPoint 필수 검증
public class AroundAdviceBuilder implements AdviceBuilder {
@Override
public Advisor build(Class<?> aspectCls, Method method, Supplier<Object> sup, PointcutFactory pf) {
Around around = method.getAnnotation(Around.class);
// ProceedingJoinPoint 필수 검증
if (method.getParameterCount() != 1 ||
!ProceedingJoinPoint.class.isAssignableFrom(method.getParameterTypes()[0])) {
throw new IllegalStateException("Around advice method must have exactly one parameter of type ProceedingJoinPoint");
}
Pointcut pc = pf.createPointcut(around.annotation(), around.pointcut());
Supplier<Object> safe = Modifier.isStatic(method.getModifiers()) ? () -> null : sup;
Advice advice = new SimpleAroundInterceptor(safe, method);
return new DefaultAdvisor(pc, advice, 0);
}
}
Around 어드바이스의 특징
- 엄격한 시그니처: 정확히 하나의 ProceedingJoinPoint 파라미터만 허용
- 완전한 제어: 원본 메서드 호출 여부와 시점을 어드바이스에서 결정
- 반환값 조작: 원본 메서드 반환값을 가로채고 변경 가능
5. Advice 인터셉터 구현체들
SimpleBeforeInterceptor: 사전 실행 인터셉터
사전 실행 후 원본 메서드 호출
public class SimpleBeforeInterceptor implements Advice {
private final Supplier<Object> aspectProvider;
private final Method adviceMethod;
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 1. aspect 인스턴스 획득 (static이면 null)
Object aspect = java.lang.reflect.Modifier.isStatic(adviceMethod.getModifiers())
? null : aspectProvider.get();
try {
// 2. 어드바이스 메서드 실행
if (adviceMethod.getParameterCount() == 0) {
adviceMethod.invoke(aspect);
} else {
JoinPoint jp = new JoinPointAdapter(invocation);
adviceMethod.invoke(aspect, jp);
}
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
// 3. 원본 메서드 실행
return invocation.proceed();
}
}
SimpleAfterInterceptor: 사후 실행 인터셉터
예외 상황을 고려한 After 처리
public class SimpleAfterInterceptor implements Advice {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Object result;
Throwable thrown = null;
try {
// 1. 원본 메서드 먼저 실행
result = invocation.proceed();
} catch (Throwable t) {
thrown = t;
result = null;
}
// 2. After 어드바이스 실행 (예외 발생 여부와 관계없이)
Object aspect = java.lang.reflect.Modifier.isStatic(adviceMethod.getModifiers())
? null : aspectProvider.get();
try {
if (adviceMethod.getParameterCount() == 0) {
adviceMethod.invoke(aspect);
} else {
JoinPoint jp = new JoinPointAdapter(invocation);
adviceMethod.invoke(aspect, jp);
}
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
// 3. 원본 예외가 있으면 다시 던지기
if (thrown != null) throw thrown;
return result;
}
}
After 어드바이스의 핵심 특징
- 예외 무관 실행: try-catch로 예외 포착 후 어드바이스 실행
- 예외 보존: 원본 메서드의 예외를 어드바이스 실행 후 재전파
- finally 시맨틱스: Java의 finally 블록과 유사한 동작
SimpleAroundInterceptor: 완전 제어 인터셉터
ProceedingJoinPoint를 통한 완전한 제어
public class SimpleAroundInterceptor implements Advice {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 1. ProceedingJoinPoint 어댑터 생성
ProceedingJoinPoint pjp = new PjpAdapter(invocation);
// 2. aspect 인스턴스 획득
Object aspect = java.lang.reflect.Modifier.isStatic(adviceMethod.getModifiers())
? null : aspectProvider.get();
try {
// 3. Around 어드바이스 메서드 실행 (원본 메서드 호출 제어권 넘김)
adviceMethod.setAccessible(true);
return adviceMethod.invoke(aspect, pjp);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
}
Around의 특별함
- 호출 제어: 어드바이스가
pjp.proceed()
호출 여부 결정 - 반환값 제어: 원본 메서드 반환값을 가로채고 변경 가능
- 예외 처리: try-catch로 원본 메서드 예외 처리 가능
Advisor 시스템 분석
1. Advisor 인터페이스: Advice와 Pointcut의 결합
단순하고 명확한 컨트랙트
public interface Advisor {
Pointcut getPointcut();
Advice getAdvice();
default int getOrder() {
return Integer.MAX_VALUE; // 기본값, 가장 낮은 우선순위
}
}
설계 철학
- 합성 패턴: Advice와 Pointcut을 조합하여 완전한 어드바이스 단위 구성
- 순서 지원: getOrder()로 여러 어드바이스의 실행 순서 제어
- 기본값 제공: 순서를 지정하지 않으면 가장 낮은 우선순위
2. DefaultAdvisor: 표준 어드바이저 구현
불변 객체로 설계된 어드바이저
public class DefaultAdvisor implements Advisor {
private final Pointcut pointcut;
private final Advice advice;
private final int order;
public DefaultAdvisor(Pointcut pointcut, Advice advice, int order) {
this.pointcut = pointcut;
this.advice = advice;
this.order = order;
}
@Override
public Pointcut getPointcut() { return pointcut; }
@Override
public Advice getAdvice() { return advice; }
@Override
public int getOrder() { return order; }
}
불변성의 이점
- 스레드 안전성: 생성 후 상태 변경 불가로 멀티스레드 환경에서 안전
- 예측 가능성: 한번 생성된 어드바이저의 동작이 변경되지 않음
- 캐싱 친화적: 상태가 변경되지 않아 캐싱 전략에 유리