클라이언트객체와 실제객체 변화없이 부가기능 추가를 위해 프록시를 사용하는 데코레이터 패턴을 도입했다. 그리고 일일이 프록시 클래스를 생성해야하는 문제를 보완하기 위해 동적프록시를 도입했다. 동적프록시 기술은 크게 두가지가 있다.
인터페이스 기반 프록시는 상속관계가 아니어서 프록시에서 부모의 생성자를 호출한다던가 하는 제약이 없어서 더 좋다고 했다. 그러면 실제객체의 인터페이스 유무에 따라 다른 기술을 적용시키기 위해 InvocationHandler 와 MethodInterceptor 를 각각 구현해놓아야할까?
스프링에서 프록시객체 생성시 추상화를 통해 동적 프록시 기술을 자동으로 선택해주는 프록시 팩토리에 대해 알아보자
프록시 팩토리란 스프링에서 동적 프록시를 통합해서 편리하게 만들어주는 추상화된 기술이다. 프록시 팩토리의 존재로 다음과 같은 동적 프록시의 한계를 해결할 수 있다.
기존에 프록시객체를 생성할 때는 if 문으로 인터페이스 유무를 확인하고, 기술에 따른 프록시객체 생성방식을 사용하여야 했다.
이제는 프록시팩토리에 실제객체를 넘겨주며 프록시객체를 생성한다. 이 때 프록시팩토리는 실제객체의 인터페이스 유무를 확인하고, 기술을 자동으로 선택하여 프록시객체를 생성해준다.
기존에 하나의 공통 부가기능을 적용하려면, JDK 동적 프록시에서 사용하는 InvocationHandler 와 CGLIB 에서 사용하는 MethodInterceptor 를 둘다 만들어놔야했다.
프록시팩토리 내부에는 InvocationHandler 와 MethodInterceptor 가 기본적으로 존재한다. 그리고 이 둘은 프록시팩토리가 주입받는 advice 를 호출하여 사용한다. 그래서 우리는 공통적으로 호출되는 advice 만 구현하면된다.
기존에 Handler 내부에 부가기능 적용여부를 결정하는 로직이 포함되었다. Handler 가 부가기능+적용여부를 모두 결정하는 것이다. 이는 SRP 에 위배되며, 이를 위해 Pointcut 이라는 개념을 도입해 Pointcut 에서 부가기능 적용여부를 확인하도록 하였다.
Advice 를 생성한다. Advice는 MethodInterceptor 를 구현하며 생성한다. CGLIB 이 아닌 aopaliance 패키지의 MethodInterceptor 임에 유의한다. 프록시팩토리가 실제객체를 주입받으며 생성되어 이미 알고있으므로 필드에 실제객체가 필요없다.
실제객체를 주입하며 프록시팩토리를 생성하고. 프록시팩토리에 어드바이저를 추가한뒤, 프록시객체를 생성한다. 실제객체의 인터페이스 유무에 따라 JDK 동적 프록시나 CGLIB 으로 프록시객체를 생성한다. addAdvice 에는 항상 True인 Pointcut.TRUE 가 자동으로 들어간다.
참고로 setProxyTargetClass(true) 로 실제객체의 인터페이스가 있더라도 CGLIB 을 사용하도록 설정할 수 있다.
포인트컷(Pointcut) : 어디에 부가기능을 적용할지 어디에 적용하지 않을지 판단하는 필터링 로직이다. 주로 클래스와 메서드의 이름으로 필터링 한다.
어드바이스(Advice) : 프록시가 호출하는 부가기능이다.
어드바이저(Advisor) : 포인트컷1 + 어드바이스1 이다. 즉 어디에 어떤로직을 적용할지 알고 있다. 프록시팩토리에 반드시 주입되어야하며, 생성되는 프록시객체에 주입된다.
즉 생성되는 프록시객체는 어드바이저를 주입받는다. 그래서 프록시 객체는 어디에 어떤 기능을 수행할지를 알고있다. 포인트컷을 확인하고, 포인트컷이 True 라면 Advice 의 invoke 를 호출하고, 포인트컷이 false 라면 실제객체의 메서드를 호출하게된다.
프록시팩토리에는 반드시 addAdvisor 로 어드바이저를 추가해줘야한다. Advisor 객체는 new DefaultPointcutAdvisor 로 주로 생성한다.
Pointcut 인터페이스를 구현해 직접 생성한 포인트컷을 Advisor 생성시 파라미터로 넣을 수도 있다. getClassFilter 와 getMethodMatcher 메서드를 오버라이딩 해줘야한다.
getMethodMatcher 의 반환값으로 사용하려고 MethodMatcher 인터페이스를 구현하였다. 내부의 matches 메서드를 오버라이딩하여 매치로직을 정하면된다.
스프링은 기본적으로 NameMatchMethodPointcut, AnnotationMatchingPointcut, AspectJExpressionPointcut 과 같은 포인트컷을 기본적으로 제공하므로 직접 구현해서 사용할 일은 거의 없다.
실무에서는 사용도 편리하고 기능도 많은 aspectJ 표현식을 기반으로 사용하는 AspectJExpressionPointcut 을 사용하게된다.
어드바이저는 하나의 포인트컷과 어드바이저 쌍으로 이루어진다고 하였다. 그러면 여러 어드바이저를 적용하려면 위 그림처럼 중간에 여러 프록시객체를 생성해야할까?
아니다. 하나의 프록시는 여러 어드바이저를 적용할 수 있다. 즉 하나의 프록시팩토리에 여러 어드바이저를 추가할 수 있으며, 생성된 프록시 객체는 추가된 어드바이저가 순서대로 적용된다.
위 코드에서 볼 수 있듯이 여러 어드바이저를 생성하고, 프록시팩토리에 추가할 수 있다. 먼저 추가한 어드바이저가 먼저 호출된다.
이제 프록시 팩토리를 활용해 로그추적기를 적용해보자.
MethodInterceptor 를 구현하여 로그추적기 기능을 수행하는 Advice 를 생성하였다.
빈 수동등록에 앞서 프록시팩토리에 추가할 어드바이저를 생성하였다. 포인트컷에 의해 request, order, save 가 붙은 메서드에만 로그를 출력할 것이다.
이제 프록시객체를 생성하고 빈으로 등록하면 된다. 각 Repository, Service, Controller 를 실제객체로 사용하고. 어드바이저를 추가한 뒤 프록시객체를 생성해 빈으로 등록하였다. 프록시팩토리의 도입으로 인터페이스 기반 구현인 V1과 구체클래스로만 구현된 V2의 프록시객체 생성방식이 완전 동일하다. 그래서 V2 버전 프록시객체 생성은 생략하였다.
모든 실제객체마다 프록시객체를 생성하고 빈으로 등록해줘야한다. 이 때 어드바이저가 동일하다해도 이 과정을 각각 해줘야한다. 실제로 수동 빈 등록하는 Config 클래스를 보면 중복이 많고 지저분한 것을 알 수 있다.
뿐만 아니라 프록시객체를 빈으로 등록하여야 하기에 컴포넌트 스캔을 활용한 자동 빈 등록에는 끼어들 틈이 없어 프록시를 활용할 수 없게된다.
동적 프록시 생성을 추상화한 프록시팩토리의 도입으로 실제객체의 인터페이스 유무와 무관하게 프록시객체를 생성할 수 있게 되었다.
어드바이스의 도입으로 Handler 와 MethodInterceptor 를 각각 구현하지 않아도 된다. 최종적으로 어드바이스를 호출해주기 때문이다.
포인트컷의 도입으로 공통로직 적용여부를 분리할 수 있게 되었다.
즉 어드바이스와 포인트컷만 정해서 넣어주면 프록시팩토리가 기술을 선택해 실제객체에 추가로 경우에 따라 부가기능을 수행하는 프록시객체를 생성해준다.
근데 프록시객체를 빈으로 등록해야 부가기능이 적용되므로, 수동 빈 등록에서만 가능하고 일일이 프록시객체 생성과정을 추가해줘야하는 한계가 있다.