프록시 기반 AOP
프록시 기반 AOP는 AOP를 구현하는 방식 중 하나로, 런타임에 동적으로 프록시 객체를 생성하여 원본 객체에 대한 참조를 감싸고, 원본 객체의 메소드 호출 시 추가적인 로직을 적용하는 방식입니다. 이를 통해 @Transactional
, @Async
, @Cacheable
등의 어노테이션을 쉽게 사용할 수 있습니다.
Spring 프레임워크는 AOP를 구현하는 데 주로 프록시 기반 AOP를 사용하지만, AspectJ와 같은 다른 AOP 구현도 지원합니다. AspectJ는 컴파일 타임에 AOP를 적용하는 방식으로, AspectJ 컴파일러를 사용하여 원본 코드에 직접 수정을 가하는 방식입니다.
같은 클래스 내에서 메소드 호출 시 AOP 적용 문제
같은 클래스 내에서 메소드를 호출할 때 AOP가 적용되지 않는 이유는 프록시 객체를 통하지 않고 원본 객체의 메소드를 직접 호출하기 때문입니다. (this를 사용하기 때문)이 문제는 주로 @Transactional
과 같은 AOP 어노테이션을 사용할 때 발생합니다.
예를 들어, 다음과 같은 MyService
클래스가 있다고 가정합니다.
@Service
public class MyService {
@Transactional
public void outerMethod() {
// ...
innerMethod();
}
@Transactional
public void innerMethod() {
// ...
}
}
해결 방법
1. 메소드 호출을 외부 빈으로 분리
메소드 호출을 외부 빈으로 분리하면 프록시 객체를 통해 호출되도록 할 수 있습니다. 예를 들어, outerMethod()
와 innerMethod()
를 각각 다른 클래스로 분리하고, 각 클래스에 @Service
와 @Transactional
어노테이션을 사용하면, 두 메소드 모두 프록시 객체를 통해 호출되어 AOP가 적용됩니다.
2. AopContext를 사용하여 프록시 객체 명시적 호출
AopContext
를 사용하면 현재 프록시 객체를 가져와 명시적으로 메소드를 호출할 수 있습니다. 이 방법을 사용하면 같은 클래스 내에서도 프록시 객체를 통해 메소드를 호출할 수 있어 AOP가 적용됩니다.
예를 들어, 다음과 같은 MyService
클래스가 있다고 가정합니다.
@Service
public class MyService {
@Transactional
public void outerMethod() {
// ...
((MyService) AopContext.currentProxy()).innerMethod();
}
@Transactional
public void innerMethod() {
// ...
}
}
outerMethod()
내에서 AopContext.currentProxy()
를 사용하여 현재 프록시 객체를 가져온 후, innerMethod()
를 호출하였습니다. 이렇게 함으로써 프록시 객체를 통해 innerMethod()
가 호출되고 @Transactional
이 적용됩니다.
그러나 이 방법은 코드가 복잡해지고 AOP 구현에 의존적이므로 권장되지 않습니다. 가능하면 메소드 호출을 외부 빈으로 분리하여 문제를 해결하는 것이 더 나은 방법입니다. 그러나 상황에 따라 AopContext
를 사용하는 것이 합리적인 선택일 수도 있습니다. 이 경우, 코드의 가독성과 유지보수성에 주의해야 합니다.
3. AspectJ 사용하기
Spring AOP 외에도 AspectJ라는 또 다른 AOP 구현 방법이 있습니다. AspectJ는 컴파일 시점에 AOP를 적용하기 때문에 프록시 기반의 문제를 피할 수 있습니다. 이를 사용하면 같은 클래스 내에서도 AOP가 적용됩니다.
Spring에서 AspectJ를 사용하려면, 의존성을 추가하고 몇 가지 설정을 변경해야 합니다.
프록시 객체를 이용하여 AOP를 적용하는 과정
- 대상 객체 생성
public class TargetClass {
public void targetMethod() {
System.out.println("Target method is executed.");
}
}
- 프록시 객체 생성 (JDK 동적 프록시 사용 예시)
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyFactory {
public static Object createProxy(Object target) {
ClassLoader classLoader = target.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
InvocationHandler handler = new ProxyHandler(target);
return Proxy.newProxyInstance(classLoader, interfaces, handler);
}
}
- 프록시 객체에서 메소드 호출 처리
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class ProxyHandler implements InvocationHandler {
private Object target;
public ProxyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before the target method.");
Object result = method.invoke(target, args);
System.out.println("After the target method.");
return result;
}
}
- 프록시 객체를 사용한 AOP 예제
public class Main {
public static void main(String[] args) {
TargetClass target = new TargetClass();
TargetClass proxy = (TargetClass) ProxyFactory.createProxy(target);
proxy.targetMethod();
}
}
위 예제에서 TargetClass
가 대상 객체이고, ProxyFactory
를 이용하여 JDK 동적 프록시 객체를 생성하였습니다. ProxyHandler
에서 invoke()
메소드를 오버라이드하여 메소드 호출 전후에 로그를 출력하도록 하였습니다. Main
클래스에서 프록시 객체를 사용하여 targetMethod()
를 호출하면 출력 결과는 다음과 같습니다.
Before the target method.
Target method is executed.
After the target method.
Spring에서는 이러한 프록시 객체를 자동으로 생성해주며, AOP 어노테이션들을 적용하기 위해 사용합니다. 예를 들어, @Transactional
어노테이션을 적용할 경우, Spring은 프록시 객체를 생성하고, 메소드 호출 시 트랜잭션 처리를 추가한 후 원래의 메소드를 호출합니다.
'Backend > Spring' 카테고리의 다른 글
Spring Boot Test Can’t Start Redis Server (2) | 2023.07.29 |
---|---|
@Transactional 전파 전략 (0) | 2023.05.07 |
DI와 IoC (0) | 2023.04.07 |
Spring Container와 Bean의 Lifecycle (0) | 2023.04.07 |
Spring Boot Controller Test (0) | 2023.04.07 |