토비의 스프링, 스프링 AOP 뿌수기 (2), 프록시 팩토리 빈

2023. 5. 18. 20:40자바/스프링

728x90
반응형

프록시 팩토리 빈

 

ProxyFactoryBean

 

 

스프링에서 제공해주는 프록시팩토리빈을 사용하면 프록시를 생성해서 빈 오브젝트로 등록하는 동작을 해준다. 지난 포스팅에서 소개한 FactoryBean 인터페이스를 구현해서 만드는 방법과의 차이는 FactoryBean 인터페이스를 구현한 경우에는 프록시를 통해 제공해줄 부가기능도 해당 구현 코드 내부에 추가해줘야했다. 반면에 ProxyFactoryBean은 프록시를 생성하는 작업만 담당한다. 그래서 프록시를 통해 제공해줄 부가기능은 별도의 빈에 둘 수 있다. 즉, 부가기능의 분리가 가능하다는 점이다.

 

ProxyFactoryBean에서 생성하는 프록시에서 사용할 부가기능은 'MethodInterceptor'라는 인터페이스를 구현해서 만든다. 이전 포스팅에서 보았던 'InvocationHandler'와 비슷해보이지만 InvocationHandler 인터페이스를 구현해서 부가기능을 추가할 때에는 타깃을 알아야한다는 단점이 있었다. 하지만 MethodInterceptor는 타깃과는 상관없이 독립적으로 만들어 질 수 있다.

 

하지만 한번에 여러 클래스에 공통적인 부가기능을 적용해야할 때나 하나의 타깃에 여러 서로 다른 부가기능을 적용할 때에는 문제가 발생한다.  

이전 포스팅에서 작성했던 FactoryBean을 사용해 프록시 객체를 직접 등록할 때 발생하는 문제점이다. 이는 모두 InvocationHandler를 구현할 때 타깃을 알아야한다는 점 때문에 발생했던 문제점들이다. MethodInterceptor는 이러한 문제점을 제거해주기 때문에 위의 문제점을 해결할 수 있을 것으로 보인다.

 

 

 Methodinterceptor 인터페이스의 invoke 메서드를 구현하면 메서드에 대해서 어떤 부가기능을 추가할지 설정할 수 있다. 

 

 

그리고 위와 같이 new ProxyFactory()를 통해 프록시를 생성해주면 된다. 이때 addAdvice() 메서드를 통해 어떤 부가기능을 추가할건지 설정해줄 수 있고, setTarget을 통해  부가기능을 추가할 '대상'을 설정할 수 있다.

 

MethodInvocation은 일종의 콜백 오브젝트로 proceed() 메서드를 실행하면 타겟 오브젝트의 메서드를 내부적으로 실행시켜주는 기능이 있다. MethodInvocation을 타깃과 분리했기 때문에 이를 싱글톤으로 두고 여러 타깃들 사이에서 공유할 수 있다. 즉, 위에서 언급한 하나의 부가기능을 여러 클래스에서 공유하지 못한다는 FactoryBean의 문제점을 해결할 수 있는 것이다. 

 

그리고 위에 부가기능을 추가할 때에는 addAdvice라는 메서드를 사용해주고 있는 것을 알 수 있다. 이는 AOP에서 사용하는 개념이다. 자, 이제 용어를 정리할 시간이다.

 

 

AOP 용어

 

타깃: 부가기능을 부여할 대상

사실 위에서 몇 번 언급했던 용어가 존재한다. 타깃이라는 용어를 줄곧 사용해왔는데, 타깃은 AOP에서도 사용하는 용어이다. 바로 부가기능을 부여할 대상을 의미한다. 뭐... 이는 문맥상으로 파악할 수 있어서 간단하게 알 수 있을 것이라 생각한다. 

 

어드바이스: 타깃에게 제공할 부가기능을 담은 모듈

어드바이스는 우리가 추가할 부가기능이라고 생각하면 된다. 이전까지 주요한 메서드 호출 앞 뒤로 'I'm ~ !!'이라는 문장을 매번 출력했는데, 이 출력하는 부가기능이 바로 어드바이스라고 생각하면 된다. 

 

조인 포인트: 어드바이스가 적용될 수 있는 위치

조인 포인트는 어드바이스를 적용할 수 있는 범위이다. AspectJ에서 조인 포인트는 매우 넓지만 스프링 AOP에서 조인 포인트는 메서드의 실행 단계뿐이다. 즉, 메서드의 실행 앞 뒤에만 부가기능을 적용할 수 있다고 생각하면 된다. 조금 더 구체적으로 범위를 좁혀보면 타깃 오브젝트가 구현한 인터페이스의 모든 메서드가 조인 포인트가 된다고 생각하면 된다.

 

포인트 컷: 어드바이스를 적용할 조인 포인트를 선별하는 작업 또는 기능을 정의한 모듈

포인트 컷은 조인 포인트 중에서 어떤 곳에 어드바이스를 적용할지 선별하는 것이라고 생각하면 된다. 스프링 AOP에서는 조인 포인트가 메서드의 실행로 한정되어있기 때문에 여러 메서드 중 어떤 메서드에다가 어드바이스를 적용할지 선별하는 작업이라고 생각하면 된다.

 

어드바이저: 포인트 컷과 어드바이스를 하나씩 갖고 있는 오브젝트

어드바이저는 포인트 컷과 어드바이스를 합친 개념이다. 뒤에서도 살펴보겠지만 동일한 프록시에서 메서드마다 다른 부가기능을 부여하고 싶다면 여러 어드바이스와 포인트 컷을 정의해야한다. 이후 어떤 메서드에 어떤 부가기능을 부여할지 설정해야하는데, 이때 이 둘을 묶어주는 것이 바로 어드바이저이다. 스프링에서 프록시를 생성할 때에는 이 어드바이저를 기준으로 프록시를 생성해준다. 이는 스프링 AOP에서만 사용하는 개념이고, 일반적인 AOP에서는 사용되지 않는다고 한다.

 

에스팩트: 한 개 이상의 포인트컷과 어드바이스의 조합으로 만들어지는 모듈

개인적으로 책에서의 에스펙트에 관한 설명은 다소 이해하기가 어려워서 다른 책들에서 설명하는 개념을 내가 이해한 방식으로 풀어보겠다. 에스팩트는 부가기능을 어디에 적용할 지에 대한 관심 대상들을 뭉퉁그려서 설명하는 개념으로 이해했다. 그래서 AOP를 적용할 때 관심의 대상들(부가기능이나 포인트컷들)에 대한 종합적인 정보라고 이해하면 좋을 것 같다. 

 

이제 다시 코드로 돌아와보자.

ProxyFactory에서 사용하는 addAdvice라는 메서드는 이름에서도 알 수 있듯이 프록시에 부가기능을 추가하는 것이라고 생각하면 된다. 또한 Target을 설정해서 어떤 객체에게 부가기능을 추가할지를 ProxyFactory에게 알려줄 수 있다. 용어를 알고 코드를 다시 보니 굉장히 직관적으로 메서드명이 구성된 것을 알 수 있다.

 

그리고 add라는 말에서도 알 수 있듯이 여러 개의 Advice도 등록할 수 있다는 것을 알 수 있다. 

 

MethodInterceptor를 구현한 새로운 어드바이스를 만들어주고

 

 

코드 상에서 실행시켜주면

 

아래와 같이 여러 부가기능이 모두 처리되는 것을 알 수 있다. 이전에 프록시 객체를 직접 만드는 경우에는 부가기능이 추가될 때마다 매번 프록시를 생성해야한다는 단점이 있었다. 그런데 이 방식을 사용하면 타깃과는 무관하게 부가기능만 따로 구현하면 되기 때문에 확장성이 어마무시해진다는 장점이 있다.

 

그리고 재미있는 점은 아래와 같이 addAdvice의 순서를 바꿔주면!

 

 

 

부가기능이 호출되는 순서도 달라진다는 것을 확인할 수 있다. 먼저 추가해준 어드바이스가 가장 바깥쪽에서 호출되고 있는 것을 알 수 있다. chat gpt에게 물어보니 어드바이스(어드바이저)를 추가해준 순서대로 부가기능이 적용된다고 한다.

 

아무튼 다시 돌아와서! 그런데 여전히 아직 문제가 있어보인다. 지금은 proxy의 모든 메서드에 대해서 동일하게 어드바이스가 적용된 것을 알 수 있다. speakName, speakRole 메서드에서는 어드바이스를 적용하고 싶은데 speakJob 메서드에서는 어드바이스를 적용하고 싶지 않으면 어떻게 해야할까?

 

이때 바로 포인트컷을 사용하면 된다.

 

포인트컷

 

 

포인트컷은 위에서도 어드바이스를 적용할 조인포인트를 선별하는 것을 의미한다. 즉, 스프링 aop를 기준으로 봤을 때 어떤 메서드에 어드바이스를 적용할 수 있을지 설정할 수 있다. 이를 적절하게 사용하기 위해서는 포인트컷 활용 방법에 대해서 익혀야한다. 

 

그런데 문제는 포인트컷 구현체가 너무 많아서 사용하기가 쉽지만은 않다는 점이다. '전문가를 위한 스프링 5'에서는 8개의 포인트컷 구현체를 소개하고 있다. 이 중 두 개의 추상클래스는 정적, 동적 포인트컷을 생성할 때 사용하는 편의 클래스이며, 나머지 여섯 개의 클래스는 아래의 기능을 사용할 수 있는 구체 클래스이다.

 

- 여러 포인트컷을 함께 구성

- 제어 흐름 포인트컷의 처리

- 단일 이름 기반 매칭 수행

- 정규식을 사용한 포인트컷 정의

- AspectJ 표현식을 사용한 포인트컷 정의

- 클래스나 메서드 레벨에서 특정 애너테이션을 찾는 포인트컷 정의

 

솔직히 이를 다 공부해보는 것은 지금 수준에서는 크게 의미가 없어보이고, 이 중 AspectJ 표현식을 사용해서 포인트컷을 정의하는 것에 대해 조금만 알아보도록 하겠다.

 

포인트컷 지시자 중 가장 대표적으로 사용되는 것은 execution()이다. []는 옵션항목이어서 생략이 가능하고 |는 or 조건임을 의미한다고 생각하고 아래의 표현식을 이해하면 된다.

 

execution([(1)접근제한자 패턴]  (2)타입패턴 [(3)타입패턴.](4)이름패턴 (5)(타입패턴 | "..", ...) (6)[throws 예외 패턴])

 

(1) 접근제한자 패턴: public이나 private과 같은 접근제한자로 생략이 가능하다. (생략가능하다.)

(2) 타입패턴: 리턴 값의 타입 패턴이다.

(3) 타입패턴: 패키지와 클래스 이름에 대한 패턴이다. (생략가능하다.)

(4) 이름패턴: 메소드 이름 패턴이다.

(5) 타입패턴 | "..", ...: 파라미터의 타입 패턴을 순서대로 넣을 수 있다. 와일드카드 또한 사용 가능하다.

(6) 예외패턴: 예외 이름 패턴이다. (생략가능하다.)

 

다소 복잡해보인다. 실제 사용 예시를 보면 조금 더 이해가 잘 된다. 몇 가지 예시만 한번 살펴보자.

 

 

execution(* minus(int, int))

 

리턴 타입은 상관없이 minus라는 메소드 이름, 두 개의 int 파라미터를 가진 모든 메소드를 선정하는 포인트컷 표현식이다.

 

execution(* minus(..))

 

리턴 타입과 파라미터 타입과는 상관없이 메소드 이름이 minus인 메소드를 선정하는 포인트컷 표현식이다.

 

execution(* *(..))

 

리턴 타입과 파라미터 타입, 메소드 명과는 상관없이 모든 메소드를 선정하는 포인트컷 표현식이다.

 

이젠 어느 정도 눈에 들어오는 것 같다. 위와 같이 포인트컷 표현식을 작성해서 AspectJ 포인트컷에 넘겨주고 이를 ProxyFactoryBean에 등록하면 포인트컷도 등록할 수 있다.

 

이번에는 포인트컷을 등록했다. setExpression을 사용해서 포인트컷을 등록했다. 이때 메소드 타입 패턴을 적는 위치에 *이 들어간 것을 볼 수 있다. 이는 유추할 수 있듯이 speak으로 시작하고 e로 끝나는 모든 메소드명을 선별해라~라는 의미이다. 

 

두번째로 주목할 점은 ProxyFactoryBean에 포인트컷을 그대로 등록하는 것이 아니라 advisor를 등록하고 있다는 점이다. 어드바이저라는 것은 포인트컷과 어드바이스가 합쳐진 개념이라고 위에서 서술했다. 즉, 어떤 메서드에 어떤 부가기능을 추가할지를 한번에 묶어서 등록한 것이라고 생각하면 된다. 만약 포인트컷과 어드바이스를 따로 등록하면 어떤 어드바이스에대해 어떤 포인트컷을 적용해야할지 애매해지기 때문에 이 둘을 조합해서 등록하게 된다. 그리고 이를 실행해보면!

 

backsoo는 줄바꿈이 없어서 뒤의 글자와 연결돼서 나온다.

backsoo를 제외한 나머지에 어드바이스가 적용된 것을 알 수 있다. 이와 같이 포인트컷을 활용하면 어떤 메서드에 어드바이스를 적용시킬지 정할 수 있다. 

 

물론 여러 개의 어드바이저를 등록할 수도 있다.

 

두 개의 어드바이저를 등록해줬다. 이번에는 speakJob 메소드를 선별하는 포인트컷과 AgentWowDecorator 어드바이스를 함께 어드바이저에 등록했다. 그리고 출력을 해보면!

다음과 같은 출력이 콘솔창에 찍히는 것을 확인할 수 있다. 크으... 예제를 직접 만들어가면서 하고 있는데, 이렇게 잘되면 기분이 좋다.

 

내친김에 몇 가지 더 테스트로 돌려봐야겠다.

 

 

동일한 객체를 생성하여 둘 다 target으로 등록해줬다. 그리고 프록시 객체를 만들어서 타입을 변환해준 뒤 출력을 해보면!

 

아래에 추가한 타깃으로 변경된다. 

 

그리고 타입이 다른 두 클래스를 타깃으로 한번에 등록해보면!

 

 

 

 

캐스팅에 실패했다고 예외가 발생한다. (그리고 재미있는 점은 CGLIB로 프록시 객체를 생성한다는 점이다. 클래스를 프록시 객체로 만드는 경우 CGLIB로, 인터페이스를 프록시 객체로 만들 경우에는 다이나믹 JDK로 만든다고 한다. 자세한건 나도 잘 모른다.) 

 

아무튼 위와 같은 예외들이 시사하는 바는 하나의 프록시 객체를 만드는 동작에는 한가지 타깃만 추가할 수 밖에 없다는 점이다. 지금 우리는 main 메소드를 통해 어드바이저가 잘 등록되는지를 확인하고 있지만 실제로는 이러한 프록시 객체들을 스프링 빈으로 등록한 다음에 해당 객체들을 사용하게 될 것이다. 그러나 위와 같이 타깃 하나에 대해서만 프록시 객체를 생성할 수 있다는 점은 다시금 반복적인 작업을 수반해야한다는 것을 의미한다.  

 

다시 돌아와서 아까 위에서 보았던 FactoryBean의 문제점을 보자.

하지만 한번에 여러 클래스에 공통적인 부가기능을 적용해야할 때나 
하나의 타깃에 여러 서로 다른 부가기능을 적용할 때에는 문제가 발생한다.  

 

후자의 문제는 해결이 되었지만 전자의 문제는 여전히 남아있다. 이를 어떻게 해결해볼 수 있을까? 바로 'DefaultAdvisorAutoProxyCreator' 라는 프록시 자동 생성기를 통해 이를 해결할 수 있다. 이는 빈 후처리기 중 하나이다. 어드바이저가 등록되어있으면 빈 객체가 생성될 때마다 모든 어드바이저를 확인해서 전달받은 빈이 프록시 적용대상인지 확인한다. 프록시 적용 대상이면 프록시를 만들어서 어드바이저를 연결해주고, 전달받은 빈 객체 대신 생성한 프록시 객체를 전달한다. 

 

자, 여기까지가 기본적인 AOP에 대한 설명이다. 다음 포스팅에서는 @Aspect 어노테이션을 활용해서 어떻게 어드바이저를 등록하는지 과정에 대해 설명하도록 하겠다! 이번 포스팅에서 제목에는 '토비의 스프링'이 들어갔지만 사실 토비의 스프링 외에도 다른 책들을 조금씩 참고해서 끌고온 내용들이 있었다. aop 내용이 광범위해서 조금 더 조리있게 글을 작성하고자 참고했던 내용들이었다. 다음 포스팅부터는 다른 책을 메인으로 한번 정리해보도록 하겠다.

728x90
반응형