
GIF 출처 : https://sigridjin.medium.com/spring-transaction-관리에-대한-메모-f391fd2885b4
1 ) 프록시 패턴 ( Proxy Pattern ) 과 데코레이터 패턴 ( Decorator Pattern )
2 ) 동적 프록시 ( Dynamic Proxy )
3 ) 팩토리 빈 ( Factory Bean )
4 ) 프록시 팩토리 빈 ( ProxyFactoryBean )
5 ) 빈 후처리기
6 ) AOP
7 ) 정리
프록시 패턴 ( Proxy Pattern ) 이란 ?
: 프록시 패턴이란 클라이언트가 실제 타깃 객체에 직접 접근하지 못하게 하고 , 대리자 ( Proxy ) 를 통해 간접 접근하게 만드는 디자인 패턴을 의미한다. 클라이언트는 타깃이 아닌 프록시를 참조하며, 프록시의 메소드가 호출되면 프록시는 필요 시 실제 타깃 객체를 지연 초기화 ( Lazy Initialization ) 한 뒤요청을 타깃 객체에게 위임(delegate)한다.
데코레이터 패턴 ( Decorator Pattern ) 이란 ?
: 데코레이터 패턴이란 타깃에 부가적인 기능을 런타임시 다이나믹하게 부여해주기 위해 프록시를 사용하는 패턴을 의미한다.
서비스 객체의 경우 본래의 비즈니스 로직만 존재하는 것이 객체지향 설계의 이상적인 상태이다. 그러나 실제 애플리케이션은 공통 관심사가 반복적으로 필요하다.
- 트랜잭션 시작/종료
- 로깅
- 보안 및 권한 체크
- 성능 측정
해당 기능들을 서비스 코드에 직접 사용하게 되면 SRP 위반 뿐만 아니라 코드 가독성 등 다양한 문제가 발생한다. 이를 위해 객체를 직접사용하지 않고 래퍼 ( Wrapper ) 를 통해 간접적으로 참조하도록 하는 방식을 사용했다.
Spring 초기에는 현재와 같은 동적 프록시 기반 AOP가 보편화되지 않았기 때문에, 부가 기능을 적용하고 객체에 대한 접근을 제어하기 위해 프록시 패턴과 데코레이터 패턴을 함께 사용하는 구조가 주로 활용되었다.

하지만 해당 방식들은 여러 단점이 존재한다. 먼저 이런식으로 프록시를 기능을 추가할 때마다 매번 새로운 래퍼클래스를 정의해야 하고 인터페이스의 구현해야 할 메소드가 증가할수록 모든 메소드를 일일이 구현해서 위임해야하는 코드를 삽입해야하는 문제가 발생한다.
클래스의 구현은 생각보다 많은 제약이 따른다. 메소드 단위와 다르게 범위가 넓고 책임이 증가하기 때문에 특정 메소드만의 제외나 조건에 따른 적용이 어렵다.
또한 초기 Spring에서는 프록시와 데코레이터를 개발자가 직접 조합해야했기 때문에 설정단계에서 어떤 부가 기능을 설정하여 적용할지 명시적으로 표현해야했다. 이는 하나의 기능 추가에 따른 여러 후속작업으로 인한 피로도 증가를 야기했다.
ex ) Spring 초기의 XML 설정
<bean id="orderServiceTarget" class="OrderServiceImpl"/>
<bean id="orderServiceTx" class="TransactionProxy">
<property name="target" ref="orderServiceTarget"/>
</bean>
<bean id="orderServiceLog" class="LoggingDecorator">
<property name="target" ref="orderServiceTx"/>
</bean>
앞선 프록시 & 데코레이터 패턴 조합의 단점을 보완하기 위해 JDK의 동적 프록시를 도입하게 되었다.
동적 프록시는 런타임에 프록시 객체를 생성하여 메소드 호출을 가로채는 방식으로 동작한다. 이때 , 프로직스를 생성하는 방식에는 크게 2가지가 존재한다.
JDK 동적 프록시
: JDK의 동적 프록시 방식은 JDK에서 제공하는 java.lang.reflect.Proxy 인터페이스를 구현하여 프록시 객체를 런타임에 생성한다. 반드시 인터페이스가 필요하며 프록시는 인터페이스를 구현한다. 모든 메소드의 호출은 InvocationHandler로 위임된다.
CGLIB 프록시
: CGLIB ( Code Generation Library ) 를 이용해 대상 클래스를 상속한 서브 클래스 프록시를 생성한다. CGLIB 프록시는 JDK 동적프록시와 달리 인터페이스 없이 프록시를 생성할 수 있으며 바이트코드 조작을 통해 클래스를 생성한다. 메소드의 호출을 가로채서 부가 로직을 수행한다.
다만 final클래스 , 메소드는 프록시가 불가하며 구조가 복잡하고 비용이 크다는 단점이 있다.
스프링에서는 기본적으로 JDK 동적 프록시를 사용하며 인터페이스 기반으로 프록시 객체를 생성한다.
JDK 동적 프록시는 리플렉션(Reflection)과 InvocationHandler 를 기반으로 동작한다. 프록시 객체는 컴파일 시점에 어떤 메서드가 호출될지 알 수 없기 때문에, 모든 메서드 호출을 하나의 진입점으로 가로채는 구조를 사용한다. 이 역할을 담당하는 것이 InvocationHandler이다.
프록시 객체에서 호출된 모든 메서드는 자동으로 InvocationHandler의 invoke() 메서드로 위임되며, 이때 호출된 메서드에 대한 정보는 Method 객체 형태로 전달된다.
이후 InvocationHandler는 리플렉션 API를 사용해 Method.invoke() 를 호출함으로써 실제 타겟 객체의 메서드를 실행하고, 그 전후에 트랜잭션, 로깅과 같은 부가기능을 적용할 수 있다.
이 구조를 통해 JDK 동적 프록시는 각 메서드를 개별적으로 오버라이딩하지 않고도, 모든 메서드 호출을 공통된 방식으로 제어할 수 있다.
작동 과정을 정리하면 다음과 같다.
Client → 인터페이스 프록시 호출 →
InvocationHandler가 메서드 정보 수신 → 리플렉션으로 타깃 메서드 실행 → 결과 반환
다음은 JDK 동적 프록시를 이용해 Service 클래스의 메소드에 부가기능을 설정하는 방식을 보여주는 예시이다.
public interface Service {
void run();
}
public class ServiceImpl implements Service {
@Override
public void run() {
System.out.println("실제 비즈니스 로직 실행");
}
}
// InvocationHandler 인터페이스 구현체
public class LogInvocationHandler implements InvocationHandler {
private final Object target;
public LogInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 부가기능 추가
System.out.println("메서드 호출 전 로그");
// 리플렉션을 통한 실제 메서드 호출
Object result = method.invoke(target, args);
// 부가기능 추가
System.out.println("메서드 호출 후 로그");
return result;
}
}
// 클라이언트
public class Client {
public static void main(String[] args) {
Service target = new ServiceImpl();
Service proxy = (Service) Proxy.newProxyInstance(
Service.class.getClassLoader(), // 클래스 로더
new Class[]{Service.class}, // 프록시가 구현할 인터페이스
new LogInvocationHandler(target) // 호출을 위임할 핸들러
);
// 클라이언트는 프록시를 호출
proxy.run();
}
}
이때 , 동적 프록시에서 리플랙션을 사용하면 예외가 InvocationTargetException 으로 감싸져 전달되기 때문에 try-catch 에서 내부 예외 ( getTargetException() ) 를 꺼내서 해당 예외가 RuntimeException 이라면 처리할 로직을 추가해야한다.
: 스프링의 빈 등록 모델은 기본적으로 “어떤 클래스가 어떤 타입의 빈으로 등록되는가”를 설정 단계에서 명확히 정의하는 정적 모델을 전제로 한다. 하지만 동적 프록시는 프록시 클래스의 존재 자체가 컴파일 시점에는 알 수 없고, 어떤 인터페이스를 구현하는 프록시가 생성될지도 런타임에 결정된다.
이로 인해 동적 프록시 객체를 일반적인 @Component나 @bean 설정을 통해 미리 스프링 빈으로 등록하는 것은 불가능했고 결과적으로, 트랜잭션과 같은 부가기능을 적용하기 위해 동적 프록시를 사용하려면 프록시 생성 책임을 개발자가 직접 떠안아야 했고, 이는 설정 코드의 증가와 구조 복잡성으로 이어졌다.
이러한 한계는 동적 프록시를 DI의 대상으로 자연스럽게 사용할 수 없는 문제로 이어졌다.
동적 프록시는 런타임에 생성되는 객체라는 특성 때문에, 클래스 기반으로 빈을 정의하는 스프링의 일반적인 빈 등록 방식과 충돌했다. 이로 인해 프록시 객체를 DI 대상으로 사용하려면 개발자가 직접 프록시를 생성하고 주입해야 했고, 이는 설정의 복잡성과 코드 중복이라는 문제를 낳았다.
스프링은 이러한 한계를 해결하기 위해 프록시 객체 자체를 빈으로 등록하는 대신, 프록시를 생성하는 책임을 분리하는 방식을 선택했다. 그 결과 등장한 것이 바로 팩토리 빈 ( Factory Bean ) 이다. 팩토리 빈은 빈을 직접 등록하는 대신, 빈을 생성하는 역할을 하는 객체를 빈으로 등록함으로써 런타임에 만들어지는 동적 객체도 스프링의 빈 관리 대상에 포함시킬 수 있게 되었다.
스프링에서는 FactoryBean 인터페이스를 구현한 클래스를 스프링 빈으로 등록해서 사용하면 구현 클래스가 팩토리 빈으로 동작하게 된다.
< FactoryBean 인터페이스 구성 >
public interface FactoryBean<T> {
T getObject();
Class<?> getObjectType(); // 주인받는 빈 : getObject()가 반환한 객체
boolean isSingleton();
}
그럼 위에서 작성된 다이나믹 프록시 코드에 팩토리 빈을 적용하여 코드를 작성하면 다음과 같다.
public interface Service {
void run();
}
public class ServiceImpl implements Service {
@Override
public void run() {
System.out.println("실제 비즈니스 로직 실행");
}
}
// InvocationHandler 인터페이스 구현체
public class LogInvocationHandler implements InvocationHandler {
private final Object target;
public LogInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 부가기능 추가
System.out.println("메서드 호출 전 로그");
// 리플렉션을 통한 실제 메서드 호출
Object result = method.invoke(target, args);
// 부가기능 추가
System.out.println("메서드 호출 후 로그");
return result;
}
}
// 팩토리 빈 구현 클래스
public class ServiceProxyFactoryBean implements FactoryBean<Service> {
private final Service target;
public ServiceProxyFactoryBean(Service target) {
this.target = target;
}
@Override
public Service getObject() {
// 기존 Client에서 직접 프록시 객체를 생성하는 로직을 팩토리 빈 메소드로 변경
return (Service) Proxy.newProxyInstance(
Service.class.getClassLoader(),
new Class[]{Service.class},
new LogInvocationHandler(target)
);
}
@Override
public Class<?> getObjectType() {
return Service.class;
}
@Override
public boolean isSingleton() {
return true;
}
}
// Configuration 클래스
@Configuration
public class AppConfig {
@Bean
public Service serviceTarget() {
return new ServiceImpl();
}
@Bean
public ServiceProxyFactoryBean service() {
return new ServiceProxyFactoryBean(serviceTarget());
}
}
기존 코드의 경우 Client 클래스에서 직접 프록시 객체를 생성하였다. 위의 코드에서는 이를 FactoryBean 인터페이스를 구현한 클래스에 위임하고 , 스프링 컨테이너가 해당 프록시 객체를 생성 & 주입하도록 하여 이를 자동화하였다.

: FactoryBean을 사용하면 Client 클래스가 프록시 객체를 직접 생성하던 책임을 분리하여 프록시 생성 로직을 스프링 컨테이너에 위임할 수 있고, 그 결과 책임 분리 원칙(SRP)을 준수할 수 있게 된다.
또한 getObject() 메서드 내부에 프록시 생성, 조건 분기, 설정 기반 생성 로직 등을 캡슐화함으로써 복잡한 객체 생성 과정을 한 곳으로 집중시킬 수 있다. 이렇게 생성된 객체는 FactoryBean이 반환하는 값으로서 DI 대상에 편입되며, 스프링 컨테이너에 의해 일반 빈과 동일하게 관리된다.
: FactoryBean + InvocationHandler 조합의 경우 하나의 클래스 내부에 존재하는 여러 메소드에 대해서는 부가기능을 일괄적으로 적용할 수 있다. 그러나 여러 타깃 클래스에 공통적인 부가기능을 제공하기에는 한계가 존재한다. 타깃 클래스마다 Proxy 와 InvocationHandler를 별도로 생성해야 하기 때문이다.
또한 하나의 타깃 객체에 여러 부가기능을 적용하는 것 역시 쉽지 않다. 이를 위해서는 InvocationHandler의 invoke() 메소드 내부에서 부가기능을 직접 조합해야 하는데, 이 과정에서 동일한 부가기능 로직이 여러 Handler에 반복적으로 작성되며 보일러플레이트 코드가 증가할 가능성이 높아진다.
기존 팩토리 빈 방식의 한계를 극복하기 위해 ProxyFactoryBean + MethodInterceptor 구조가 도입되었다.
ProxyFactoryBean
: 프록시 객체 생성 & 프록시 방식 선택 ( JDK 동적 프록시 or CGLIB ) , 부가 기능 ( Advice ) 실행 체인을 구성하여 프록시 생성 로직을 완전히 캡슐화한다. 이를 통해 프록시 생성 책임을 프레임워크로 이동시켜 관심사를 분리한다.
MethodInterceptor
: 기존 InvocationHandler 의 단일 부가기능 부여에서 벗어나 체인을 통해 여러 부가기능을 조합할 수 있는 실행 모델을 제공한다. 프록시 기술 ( JDK 동적 프록시 or CGLIB ) 에 대해 알 필요가 없고 오직 부가 기능 실행 로직에만 집중한다.
또한 타깃 오브젝트에 대한 정보는 ProxyFactoryBean을 통해 MethodInvocation에 전달되므로 , MethodInterceptor 자체는 상태를 사지지 않고 독립적으로 생성될 수 있어 싱글톤 빈으로 등록이 가능하다.
MethodInterceptor는 일종의 콜백 오브젝트로 , proceed() 메소드를 실행하면 타깃 오브젝트의 메소드를 내부적으로 실행해주는 기능이 있다. 즉 , 이는 단순히 템플릿이기 때문에 싱글톤으로 두고 공유하여 사용할 수 있다.
앞서 팩토리 빈 방식에서는 새로운 부가기능 추가에 있어서 프록시 + 팩토리 빈 등록이 필요했으나 단순히 MethodInvocation만 ProxyFactoryBean의 addAdvice 메소드를 통해 등록하면 된다.
: 스프링에서는 MethodInterceptor 처럼 타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트를 어드바이스 ( Advice ) 라고 한다.
: 포인트컷 ( PointCut ) 이란 어드바이스를 어떤 메서드에 적용할지 결정하는 조건이다. 즉 , 부가기능을 적용할 메서드를 선별하는 필터조건이다.
: 어드바이저(Advisor)는 어드바이스(Advice)와 포인트컷(Pointcut)의 조합으로, 어떤 부가기능을 어디에 적용할지를 하나의 단위로 표현한 객체이다. 스프링에서는 어드바이저를 기준으로 프록시가 생성되며, 포인트컷 조건을 만족하는 메서드에만 어드바이스가 실행된다.
ProxyFactoryBean 방식은 다음과 같은 과정을 통해 수행된다. 먼저 포인트 컷을 통해 기능 부가 대상을 선정하고 어드바이스를 통해 추가할 부가기능을 선택하여 타깃 오브젝트에 이를 위임하게 된다. 이때 , 포인트컷과 어드바이스의 조합을 어드바이저 ( Advisor ) 라고 한다.

다음은 기존의 FactoryBean 을 적용한 코드를 ProxyBeanFactory 방식으로 리팩토링 한 것이다.
public interface Service {
void run();
}
public class ServiceImpl implements Service {
@Override
public void run() {
System.out.println("실제 비즈니스 로직 실행");
}
}
public class LogMethodInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 부가기능
System.out.println("메서드 호출 전 로그");
// 다음 인터셉터 또는 타깃 호출
Object result = invocation.proceed();
// 부가기능
System.out.println("메서드 호출 후 로그");
return result;
}
}
@Configuration
public class AppConfig {
@Bean
public Service serviceTarget() {
return new ServiceImpl();
}
@Bean
public LogMethodInterceptor logMethodInterceptor() {
return new LogMethodInterceptor();
}
// 어드바이저 ( Advisor ) = 어드바이스 ( Advice ) + 포인트 컷 ( PointCut )
@Bean
public Advisor logAdvisor() {
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("run"); // 포인트컷 적용 : run()에만 적용
return new DefaultPointcutAdvisor(pointcut, logMethodInterceptor());
// logMethodInterceptor ( 어드바이스 ) 추가
}
// ProxyFactoryBean에는 Advisor만 등록
@Bean
public ProxyFactoryBean service() {
ProxyFactoryBean pfb = new ProxyFactoryBean();
pfb.setTarget(serviceTarget());
pfb.setInterceptorNames("logAdvisor");
return pfb;
}
}
이때 , 타깃이 인터페이스를 구현한다면 JDK 동적 프록시 방식을 채택하며 인터페이스가 없다면 CGLIB 방식을 선택한다. Spring AOP는 기본적으로 프록시 기술 선택을 자동화하지만, 설정을 통해 CGLIB 기반 클래스 프록시를 강제로 사용할 수도 있다.
이로써 인터페이스 유무와 상관없이 AOP를 적용할 수 있는 유연성을 제공한다.
(1) FactoryBean에서 직접 지정
ProxyFactoryBean pfb = new ProxyFactoryBean();
pfb.setTarget(serviceTarget());
pfb.addAdvice(new LogMethodInterceptor());
// CGLIB 강제
pfb.setProxyTargetClass(true);
(2) @EnableAspectJAutoProxy 사용시 옵션 지정
@EnableAspectJAutoProxy(proxyTargetClass = true)
// true 선택 시 , CGLIB 강제
@Configuration
public class AppConfig {
}
: ProxyFactoryBean 방식의 가장 큰 문제점은 부가기능을 적용해야 하는 타깃 오브젝트마다 거의 동일한 ProxyFactoryBean 설정을 반복해서 등록해야 한다는 점이다.
예를 들어 서비스 클래스가 10개 존재한다면, 이에 대한 프록시 빈 또한 10개가 필요하며 결과적으로 동일한 설정을 10번 작성해야 한다.
이로 인해 설정 중복이 발생하고, 코드 및 설정량 증가로 인한 유지보수 비용 상승이라는 문제가 생긴다. 또한 새로운 공통 기능을 추가할 경우 모든 ProxyFactoryBean 설정을 수정해야 하는 한계가 있다.
ProxyFactoryBean 방식의 문제는 프록시를 언제, 누가 생성하느냐에 있었다. 프록시 생성 책임이 개발자에게 있었고, 스프링 컨테이너는 프록시 생성 과정에 관여하지 않았다.
이를 해결하기 위해서는 스프링 컨테이너가 빈 생성 시점에 직접 개입하여 필요한 경우 해당 빈을 프록시로 대체할 수 있어야 했다.
이러한 발상에서, 빈을 생성하는 과정에서 빈을 가로채어 부가기능 적용 대상인지 검사하고 대상일 경우 원본 빈 대신 프록시 객체를 반환하는 아이디어가 등장하게 되었다.
: 빈 후처리기( BeanPostProcessor ) 란 스프링이 생성한 빈에 대해 초기화 전·후 시점에 개입하여 빈을 가공하거나, 필요에 따라 원본 빈을 프록시 객체로 교체할 수 있도록 해주는 스프링의 확장 포인트를 의미한다.
: 빈 후처리기(BeanPostProcessor) 는 스프링에 등록된 빈이 생성될 때마다 호출되어 빈을 가공하거나 초기화 작업을 수행할 수 있는 확장 포인트다.
이를 통해 스프링은 일부 빈을 프록시로 감싸고, 프록시 객체를 빈으로 대신 등록할 수 있다. 이것이 바로 자동 프록시 생성 빈 후처리기의 핵심 원리다.

빈 후처리기는 다음 과정을 통해 자동으로 프록시를 생성한다.
< 1st > DefaultAdvisorAutoProxyCreator 빈 후처리기가 등록되면, 스프링은 빈이 생성될 때마다 해당 빈을 후처리기에 전달
< 2nd > 후처리기는 모든 등록된 어드바이저의 포인트컷을 검사하여 빈이 프록시 적용 대상인지 판단
< 3rd > 프록시 대상이면 내장된 프록시 생성기를 사용해 프록시를 만들고, 프록시에 어드바이저를 연결한 뒤 원본 빈 대신 컨테이너에 반환
< 4th > 결과적으로 스프링 컨테이너는 후처리기가 반환한 프록시를 최종 빈으로 등록하고 사용
스프링에서 빈 후처리기는 BeanPostProcessor 인터페이스를 구현한 클래스이다.
public interface BeanPostProcessor {
default Object postProcessBeforeInitialization(Object bean,String beanName)
throws BeansException {
return bean;
}
default Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
}
Spring AOP 에서는 대표적으로 2가지 클래스를 활용한다.
(1) DefaultAdvisorAutoProxyCreator
:모든 빈을 스캔해서 , 어드바이저 적용 대상이면 자동으로 프록시를 생성한다.
(2) AnnotationAwareAspectJAutoProxyCreator
: @AspectJ 기반의 AOP를 적용한다.
DefaultAdvisorAutoProxyCreator 특징: DefaultAdvisorAutoProxyCreator는 빈이 AOP 대상인지 자동으로 판단하고 프록시를 생성해야 하기 때문에, 단순히 메서드 이름만 검사하는 NameMatchPointcut으로는 충분하지 않다. 자동 프록시 생성기는 클래스와 메서드 단위로 대상 빈을 판별할 수 있는 포인트컷이 필요하다.
대표적인 포인트컷으로는 AspectJExpressionPointcut, AnnotationMatchingPointcut 등이 있으며, 이들을 통해 Advisor가 적용될 빈과 메서드를 정확히 선택하고, 프록시를 생성한 후 어드바이저를 연결한다.
다음은 ProxyFactoryBean 방식으로 구현된 코드를 빈 후처리기를 적용한 리팩토링 코드이다.
public interface Service {
void run();
}
public class ServiceImpl implements Service {
@Override
public void run() {
System.out.println("실제 비즈니스 로직 실행");
}
}
public class LogMethodInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("메서드 호출 전 로그");
Object result = invocation.proceed();
System.out.println("메서드 호출 후 로그");
return result;
}
}
@Configuration
public class AppConfig {
// 실제 서비스 빈
@Bean
public Service serviceTarget() {
return new ServiceImpl();
}
// 부가기능(Advice)
@Bean
public LogMethodInterceptor logMethodInterceptor() {
return new LogMethodInterceptor();
}
// AspectJ 표현식 기반 포인트컷 + Advisor
@Bean
public Advisor logAdvisor() {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
// 포인트 컷 표현식 : com.example.service 패키지 이하 ServiceImpl의 run() 메서드에만 적용
pointcut.setExpression("execution(* ServiceImpl.run(..))");
return new DefaultPointcutAdvisor(pointcut, logMethodInterceptor());
}
// 자동 프록시 생성기 등록 (빈 후처리기 역할)
@Bean
public DefaultAdvisorAutoProxyCreator autoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true); // CGLIB 기반 프록시 생성
return creator;
}
}
빈 후처리기 방식은 포인트컷은 클래스와 메서드 기준으로 조건을 포인트컷 표현식 ( Pointcut Expression ) 으로 지정하여 NameMatchPointcut 처럼 빈으로 직접 담아 등록할 필요가 없다. 즉 , ProxyFactoryBean 방식에서는 개발자가 각 빈마다 프록시를 만들어야 했지만 빈 후처리기 방식에서는 자동으로 프록시를 생성하도록 컨테이너에 등록하게 하여 프록시 생성 책임을 스프링에게 부여하였다.
: AspectJ 표현식은 어떤 클래스와 메서드에 AOP를 적용할지 지정하는 조건식을 의미한다. 이는 AspectJ 프레임워크에서 제공하는 것을 가져와 일부 문법을 확장해서 사용한다.
execution([접근제한자 패턴] 타입 패턴 [ 타입패턴. ]이름패턴 ( 타입패턴 | "..", ...)
[ throws 예외 패턴])
ex 1 )
execution(public void com.example.service.UserService.save(..))
: UserService 클래스의 save메소드 , 반환 타입 void , 접근제어자 public
ex 2 )
execution(* com.example.service..*(..))
: come.example.service 패키지 이하 모든 클래스
: AOP ( Aspect Oriented Programming ) 이란 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스팩트라는 모듈로 만들어서 설계하고 개발하는 방법을 의미한다. 애스팩트는 앞서 적용한 부가 기능을 의미한다.
AOP는 애스펙트 ( 부가기능 ) 을 분리함으로써 핵심기능을 설계하고 구현할 때 객체지향적인 가치를 지킬 수 있도록 도와준다. 기존 프록시 방식은 ProxyFactoryBean 이나 수동 프록시를 이용해서 각 빈마다 부가기능(Advice)을 적용해야 했다.
빈마다 설정을 반복해야 하고, 유지보수 비용도 커지는 문제가 있었다.
이 문제를 해결하기 위해 스프링은 자동 프록시 생성기(BeanPostProcessor)를 도입하고,
어드바이저를 활용해 부가기능을 적용할 빈을 자동으로 판단하도록 했다.
하지만 NameMatchPointcut처럼 단순 이름 기반 포인트컷은 한계가 있었다. 그래서 등장한 것이 AspectJ 표현식 기반 포인트컷이다.
AspectJ 표현식을 사용하면 클래스와 메서드 단위로 AOP 적용 대상을 정밀하게 지정할 수 있다. 자동 프록시 생성기와 결합하면 빈마다 프록시를 일일이 설정하지 않아도 원하는 클래스와 메서드에 정확히 부가기능을 적용할 수 있다.
그러나 프록시 기반의 AOP에는 한계가 있다.
(1) ⭐️ 클래스 내부 메서드 호출에는 어드바이스가 적용되지 않는다.
(2) 인터페이스 기반 호출 중심이기 때문에 모든 상황에서 정밀하게 제어하기 어렵다.
(3) 런타임 프록시를 사용하기 때문에 , 모든 AOP 기능이 프록시 계층 안에서만 작동한다.
해당 한계들을 극복하기 위해 스프링에서는 AspectJ 프레임워크가 도입되었다.
: AspectJ 프레임워크는 프록시처럼 간접적인 방법이 아닌 , 타깃 오브젝트를 뜯어고쳐 부가기능을 직접 넣어주는 방법을 사용한다. 컴파일된 타깃 클래스 파일 자체를 수정하거나 클래스가 JVM에 로딩되는 시점을 가로채서 바이트코드를 조작하는 방식을 사용한다.
이는 소스 코드를 수정하는 방식이 아니기 때문에 개발을 진행함에 있어 비즈니스 로직에 충실한 코드를 생성할 수 있도록 한다.
그럼 AspectJ는 왜 클래스 파일 수정이나 바이트코드 조작을 통해 부가기능을 설정하는 것인가 ?
① 바이트코드를 조작해서 타깃 오브젝트를 직접 수정하면 스프링과 같은 DI 컨테이너의 도움을 받아 자동프록시 생성방식을 사용하지 않아도 AOP를 적용할 수 있기 때문이다.
② 바이트코드를 직접 조작해서 AOP를 조작하면 오브젝트의 생성 , 필드 값의 조회와 조작 , 스태틱 초기화 등의 다양한 작업에 부가기능을 부여해줄 수 있어 기존에 제한된 메소드의 범위를 확대할 수 있다.
: Spring AOP 에서는 ProxyFactoryBean 이나 BeanPostProcessor를 직접 다르지 않고 어노테이션을 통해서 해당 클래스가 애스팩트 클래스 ( 부가기능 부여 ) 인지 특정 메소드를 어떤 포인트컷 표현식으로 조건분기를 걸지를 결정한다.
@Aspect: 해당 클래스가 애스팩트 임을 선언한다. 이때 , 단독으로 동작하지는 않으며 반드시 @Before,@After,@Around 와 같은 어노테이션이 붙은 메소드가 존재해야한다.
ex )
@Aspect
@Component
public class LogAspect {
}
@Pointcut: 어떤 메소드에 부가기능을 부여할지를 결정하는 포인트컷을 나타내는 어노테이션이다. 이때 포인트컷 클래스는 단순히 경로만 모아놓은 선언용 클래스로 사용되며 비즈니스 로직 , 실행 , 상태를 담지않는다.
ex )
@Aspect
@Component
public class CommonPointcuts {
@Pointcut("execution(* com.example.service..*(..))")
public void serviceLayer() {} // 포인트컷의 메소드는 비워둔다.
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
public void transactional() {}
}
@Around: 전 , 후 , 예외를 포함하여 전체를 제어한다. 해당 어노테이션은 유일하게 메서드 실행을 직접 호출한다.
@Aspect
@Component
public class LogAspect {
@Around("execution(* com.example.service.OrderService.order(..))") // 방식 1 , 직접 포인트 컷 지정
public Object log(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("실행 전");
Object result = pjp.proceed(); // 타겟 메소드 실행
System.out.println("실행 후");
return result;
}
}