오늘 알아볼 내용은 Spring 에서 사용하는 aop 프록시 원리에 대해서 살펴보도록 하자.

기본적으로 Spring에서 aop의 프록시 매커니즘은 두가지를 이용한다. 하나는 JDK 동적 프록시와 다른 하나는 Cglib 프록시를 사용하고 있다.
JDK 동적 프록시 경우에는 java의 리플렉션을 이용해서 객체를 만드는데 Cglib 경우에는 바이트코드를 조작해 프록시 객체를 만든다.
스프링으로 개발하다보면 인터페이스 한개당 구현체도 한개의 경우가 많이 있다. 예전에는 인터페이스를 구현하지 않으면 aop가 동작하지 않는다고하여(물론 이 이유만은 아니다.) 인터페이스 한개 당 구현체도 한개 존재하는 경우가 많았다. 하지만 요즘에는 필자의 경우에도 인터페이스를 잘 작성하지 않는다. 구현체가 여러개가 될 경우를 제외하고는 거의 만들지 않는다. 물론 이것도 말이 많다. 인터페이스 한개 당 구현체도 하나인데 굳이 뭐하러 만드냐? 구현체가 여러개 있을 경우에만 만들자. 시간낭비다. 툴에서 구현체까지 이동하려면 굳이 키를 더 눌러야 한다. 는 반응이 있는과 반면에 아니다. Spring의 철학과 맞지 않는다. 등등 여러 논쟁이 아직도 있는 듯 하다. 답은 없다. 적당한 융통성을 갖고 개발하면 된다.

아무튼 그건 그렇고 아마도 예전에도 인터페이스가 없어도 동작했을 것으로 판단한다. 물론 Cglib 라이브러리를 추가했으면 말이다. 예전의 Spring까지 테스트하기는 좀 그렇고 지금 현재의 Spring버전으로 살펴보도록하자.
왜 이때까지 쓸 때 없이 인터페이스에 관한 이야기를 했을까? 인터페이스와 관련이 있어서 잠시 이야기를 했는데 딴길로 빠질뻔.

Spring에서는 프록시 대상의 객체가 최소 하나의 인터페이스를 구현했다면 JDK 프록시를 사용하는 반면 대상 객체의 인터페이스를 구현하지 않았다면 Cglib를 사용해 프록시 객체를 생성한다. 원래는 cglib는 Spring에 존재 하지 않았다. 하지만 3.2부터 재패키징되어 Cglib가 Spring core에 포함 되었다.

한번 Test를 해보자. 테스트 소스는 다음과 같다.

public interface ServiceTest {
  void print();
}

@Service
public class ServiceTestImpl implements ServiceTest {
  @Async
  public void print() {
  }
}

@RestController
public class HelloController {
  private final ServiceTest service;
  public HelloController(ServiceTest service) {
    this.service = service;
    System.out.println(service.getClass());
  }
}

굳이 왜 @Async를 썼냐고 물어보면 AOP를 사용하기 위해 넣었다. (@Transactional도 동일하다.) 그래야 프록시 객체가 들어간다. 뭐 어찌됬건 테스트 코드이니 다른거에는 집중하지 말고 어떤 프록시 객체가 들어갔나 확인만 하면 된다. 일단 위의 코드는 인터페이스를 구현한 ServiceTestImpl 코드가 있다. 그럼 HelloController의 생성자에 있는 print를 출력해보자. 그럼 다음과 같이 출력된 것을 확인할 수 있다.

class com.sun.proxy.$Proxy60

JDK 동적 프록시가 주입된 것을 눈으로 확인 할 수 있다. 그럼 인터페이스가 없다면 어떻게 될까? 코드를 조금 수정해보자.

@Service
public class ServiceTestImpl {
  @Async
  public void print() {
  }
}

@RestController
public class HelloController {
  private final ServiceTestImpl service;
  public HelloController(ServiceTestImpl service) {
    this.service = service;
    System.out.println(service.getClass());
  }
}

이번에는 인터페이스를 제거하고 다시 실행시켜보자. 그럼 어떤 결과가 출력 될까?

class me.wonwoo.ServiceTestImpl$$EnhancerBySpringCGLIB$$bababcc1

위와 같이 Cglib 프록시가 주입되었다. 인터페이스가 있고 없고에 따라서 프록시객체의 주입이 다르다는 걸 알 수 있다. 근데 왜 굳이 이렇게 나눠 놨을까? 스프링 철학에 따라 무조건 인터페이스를 만들어서 사용하게 강제화 할 수 있는데 인터페이스를 만들지 않아도 AOP를 가능하게 하게 만든 이유는 뭘까? 아마도 레거시를 위한 코드이거나 인터페이스를 사용할 수 없는 코드를 위해서 만든걸까? 정확한 이유는 잘 모르겠다.

한번 우리도 spring aop 를 이용하여 프록시 객체를 만들어보자. Spring에서 제공해주는 ProxyFactory클래스를 사용하면 된다. 아마도 Spring에서도 내부적으로 해당 클래스를 사용하고 있지 않을까?

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setInterfaces(ServiceTest.class);
proxyFactory.setTarget(new ServiceTestImpl());
proxyFactory.addAdvice(new ServiceAdvice());
final ServiceTest proxy = (ServiceTest) proxyFactory.getProxy();
System.out.println(proxy.getClass());

아까와 동일하게 인터페이스를 구현한 클래스를 넣어서 테스트를 해보자. 굳이 ServiceAdvice는 없어도 테스트는 가능하다. Advice는 이곳을 참고하자. 그럼 다음과 같이 출력될 것이다.

class com.sun.proxy.$Proxy4

이것 역시 인터페이스가 있어서 JDK 동적 프록시가 주입되었다. 다음은 인터페이스를 삭제하고 구현클래스에도 인터페이스를 제거한 후에 다시 테스트를 해보자.

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new ServiceTestImpl());
proxyFactory.addAdvice(new ServiceAdvice());
final ServiceTestImpl proxy = (ServiceTestImpl) proxyFactory.getProxy();
System.out.println(proxy.getClass());

그럼 예상 했던 결과 그대로 다음과 같이 출력 된다.

class me.wonwoo.ServiceTestImpl$$EnhancerBySpringCGLIB$$52006800

cglib가 아주 좋아보이지만 제약사항이 있다. final 메서드와 클래스 경우에는 Advice할 수 없다. 예로 Spring 설정과 관련해서 @Bean 메서드에 final을 정의하면 에러가 발생한다.

@Bean
public final SomeObject someObject(){
  return new SomeObject();
}

클래스도 마찬가지 이다.

@Service
public final class ServiceTestImpl {
   //blabla
}

위와 같이 final class, final method를 테스트를 해보면 에러가 발생한다. 하지만 JDK 동적 프록시 경우에는 final 이여도 상관없이 잘 동작한다.

그리고 만약 프록시 객체에 cglib를 강제화 하고 싶다면 아래와 같은 설정하면 된다.

@EnableAsync(proxyTargetClass = true)
@EnableCaching(proxyTargetClass = true)
// 기타 등등

그러면 인터페이스가 있더라도 cglib의 프록시 객체가 주입된다. 각각의 용도에 맞게 잘 사용하면 되겠다.

이렇게 오늘은 Spring의 AOP 매커니즘에 대해서 알아봤다!