스프링 AOP
애플리케이션 기능의 종류에는 크게 핵심 기능과 부가 기능으로 나눌 수 있다.
OrderService라는 객체에서 핵심 기능이라 함은 ‘주문을 한다’이고 부가 기능이라 함은 ‘트랜잭션 처리’라고 예를 들 수 있다.
이러한 부가 기능은 보통 여러 클래스에서 동시에 적용되는 데 이를 횡단 관심사(cross-cutting concerens)이라고 한다. 이러한 부가 기능을 적용하기 위해선 보통 많은 반복과 중복 코드, 그리고 중복으로 인한 많은 수정을 요하게 된다.
AOP는 관점 지향 프로그래밍(Aspect-Oriented Programming)의 약자로 이러한 중복되는 횡단 관심사를 모듈화 하여 분리하기 위해 나온 패러다임이다.
동적 프록시
스프링에서는 AOP를 구현하기 위해 동적 프록시 기술을 사용한다.
클라이언트가 서버를 호출한다고 했을 때 직접 호출하는 것이 아니라 어떤 대리자를 통해 간접적으로 요청할 수도 있는데 이 대리자를 프록시라고 한다.
클라이언트 클래스의 코드와 실제 객체 클래스의 코드를 거의 건드리지 않고 부가 기능을 추가하기 위해 중간에 프록시를 호출하고, 프록시가 부가 기능(트랜잭션 처리)과 실제 객체의 로직을 함께 호출하도록 하는 것이다.
이렇게 프록시를 사용하여 새로운 기능 추가를 목적으로 하는 것을 데코레이터 패턴이라고 한다.
하지만 그냥 프록시를 사용하게 되면 부가 기능이 추가될 때마다 새로운 프록시 클래스를 만들어야 한다. (트랜잭션 프록시, 비동기 프록시, 로깅 프록시…) 이를 해결하기 위해 동적 프록시 기술을 사용한다.
동적 프록시 기능을 사용하면 개발자가 직접 프록시 클래스를 만들지 않아도 개발자가 부가 기능과 적용 대상만 잘 지정하면 런타임에 동적으로 만들어진다.
스프링에서는 인터페이스 기반의 JDK 동적 프록시와 상속을 통한 CGLIB 프록시 기술을 제공한다. 최근 스프링부트에서는 인터페이스가 존재하든 안하든 CGLIB 프록시로 프록시를 생성하도록 하고 있다.
프록시 적용해보기
위처럼 직접 만든 어노테이션을 붙인 UserService의 changePassword 메서가 있다.
아래의 테스트에서 stubUserHistoryDao는 현재 log 메서드를 호출하면 예외가 발생하도록 되어 있다. 때문에 channgePassword를 하면 user의 비밀번호는 update하지만 log를 호출하면서 예외가 발생한다. 트랜잭션이 적용된다면 원래 비밀번호가 새로운 비밀번호와 같지 않다는 아래 테스트는 통과해야 한다.
당연히 현재는 프록시가 적용되지 않았기 때문에 실패한다. 이를 통과하도록 만들어 보자.
Advise, Pointcut, Advisor
지금부터 스프링이 지원하는 방법으로 프록시를 만들어볼 텐데 우선 다음의 용어를 익혀야 한다.
- 포인트컷(Pointcut): 어디에 부가 기능을 적용할지 판단하는 필터링 로직
- 어드바이스(Advice): 프록시가 호출하는 부가 기능
- 어드바이저(Advisor): 단순하게 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것
- 어드바이저 = 포인트컷 + 어드바이스
TransactionPointcut
우선 포인트컷부터 만들어보자. 포인트컷은 Pointcut 인터페이스를 구현하여 만들 수 있다. 아래는 구현을 더 편하게 하기 위해 StaticMethodMatcherPoitncut을 상속하여 조건을 명시했다. (Target 클래스의 클래스 이름, 메소드 이름을 기반으로 Pointcut을 만들고자 할 때 사용하는 클래스이다)
메서드나 클래스에 MyTransactional 어노테이션이 존재하면 true를 반환하도록 하여 부가 기능을 적용할 대상을 판단하도록 구현했다.
TransactionAdvice
MethodInterceptor는 Interceptor를, Interceptor는 Advice 인터페이스를 상속한다. invoke 메서드를 오버라이드 하여 트랜잭션 처리 부가 기능을 추가했다. MethodInvocation 객체 안에 메서드를 호출하는 방법, 현재 프록시, 메서드 매개 변수, 메서드 정보 등이 포함되어 있어서 proceed() 메서드를 호출하는 것으로 실제 기능을 호출할 수 있게 된다.
TransactionAdvicor
어드바이저는 포인트컷 + 어드바이스라고 했다. 위처럼 코드를 구성하면 준비는 끝난다.
ProxyFactory
스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리(ProxyFactory)라는 기능을 제공한다
프록시 팩토리를 통해 프록시를 생성할 때 어드바이저를 제공하면 어디에 어떤 기능을 제공할지 알 수 있다.
실제 객체(target)를 받아 프록시를 생성하는 메서드를 만들었다. 프록시 팩토리에 target과 어드바이스와 포인트컷으로 만든 어드바이저를 넣어주면 getProxy() 메서드를 통해 동적으로 프록시를 생성할 수 있다.
테스트 실행 결과 통과하는 것을 확인했고 트랜잭션이 잘 적용되었다.
빈 후처리기를 통한 프록시 주입
스프링 IoC 컨테이너로 UserService를 주입받는다고 했을 때 어떻게 프록시로 대체해줄 수 있을까? 아무것도 해주지 않으면 아래처럼 주입받으면 실제 객체가 들어올 것이다.
스프링에서는 이를 빈 후처리기를 통해 해결한다. 빈 후처리기란 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 전에 조작하거나 대체하고 싶을 때 사용할 수 있다.
BeanPostProcessor 인터페이스를 구현하여 만들 수 있다. 아래 코드는 어떤 빈이 TransactionPointcut을 만족하면 프록시로 바꿔 칠 수 있도록 구현했다.
빈 후처리기를 빈으로 등록하면 DI를 통해 빈을 주입받더라도 프록시로 대체되게 된다.
스프링이 제공하는 자동 프록시 생성기
implementation 'org.springframework.boot:spring-boot-starter-aop'
일일이 BeanPostProcessor를 구현하는 것도 귀찮기 때문에 스프링에서는 AnnotationAwareAspectJAutoProxyCreator라는 자동 프록시 생성기를 제공한다.
위 의존성을 추가하면 단순히 아래처럼 Advisor를 빈으로 등록하는 것만으로 포인트컷에 해당하는 빈을 프록시로 대체할 수 있다.
@Aspect AOP
Advisor, Advise, Pointcut 인터페이스를 각각 구현해서 Advisor를 빈으로 등록하는 것도 여간 귀찮은 것이 아니기 때문에 @Aspect 어노테이션으로 편하게 Advisor를 등록할 수 있다.
- @Aspect 어노테이션으로 Advisor를 등록할 수 있다.
- @Aspect만으로는 빈 등록이 안 되기 때문에 @Component나 @Bean을 통해 따로 등록해주어야 한다.
- @Around가 붙은 메서드는 Advise가 된다.
- @Around는 실제 기능 앞 뒤로 부가 기능을 넣겠다는 뜻
- 내부에 포인트컷 표현식을 넣을 수 있는데 위에선 MyTransactional 어노테이션을 인식하도록 했다.
- ProceedingJointPoint를 통해 실제 호출 대상을 호출할 수 있다.
이런 식으로 커스텀 Advisor를 만들어놓으면 자동 프록시 생성기는 다음과 같이 동작한다.
- 빈들이 자동 프록시 생성기(빈 후처리기)에 전달됨
- Advisor 빈을 조회
- @Aspect 어노테이션이 붙은 클래스를 찾아 어드바이저 빌더가 Advisor로 변환
- Advisor들 안의 포인트컷으로 프록시 대상 빈을 바꿔치기해서 스프링 빈으로 등록
참고
'스프링' 카테고리의 다른 글
[Spring] @Transactional과 @Async (0) | 2022.11.17 |
---|---|
왜 스프링 프레임워크인가 (0) | 2022.04.19 |