본문 바로가기

Backend/Spring

같은 클래스 내에서 메소드 호출 시 AOP 적용 문제

프록시 기반 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를 적용하는 과정

  1. 대상 객체 생성
public class TargetClass {
    public void targetMethod() {
        System.out.println("Target method is executed.");
    }
}
  1. 프록시 객체 생성 (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);
    }
}
  1. 프록시 객체에서 메소드 호출 처리
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;
    }
}
  1. 프록시 객체를 사용한 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  (1) 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