토비의 스프링, 스프링 AOP 뿌수기 (1), 빌드업

2023. 5. 17. 16:16자바/스프링

728x90
반응형

오랜만에 뿌수기 시리즈로 돌아왔다. 그런데 이번에는 토비의 스프링이다. 사실 다음주에 테코톡 발표 주제로 스프링 AOP를 선정했는데, 공식문서만으로는 자료가 매우 미흡해질 것으로 예상되어 이렇게 여러 책들이 소개하는 스프링 AOP에 대해 정리해보고자 한다.

 

그 중에 가장 먼저 정리하고자하는 책은 '토비의 스프링'에서 설명하는 스프링 aop이다.


토비의 스프링, 스프링 AOP 빌드업

 

다이내믹 프록시와 팩토리 빈

 

 

토비의 스프링에서는 aop를 설명하기 위한 빌드업이 등장한다. 먼저 핵심 기능에 부가기능을 추가했을 때 벌어지는 현상들에 대해서 설명하고 있다. 처음에는 핵심 기능에 로그나 롤백 등의 부가기능을 추가했을 때 코드가 복잡해진다라는 점을 단점으로 꼽는다. 코드가 그렇게 복잡하게 될 경우에는 우리가 원하는 핵심 기능을 확실하게 캐치하기 어려워지고, 이로인해 유지보수 비용이 많이 들게 된다. 

 

이를 개선하기 위해 전략패턴을 도입할 수 있다. 부가기능을 담당하는 클래스를 인터페이스화해서 주입을 시켜주는 식으로 처리할 수 있다. 그러나 이 또한 완전히 부가기능을 분리한 것은 아니다. 여전히 비즈니스 로직 내부에 부가기능을 처리하는 로직이 포함되어있기 때문이다. 그러면 이를 어떻게 개선해볼 수 있을까?

 

본격적으로 설명하고자하는 내용은 지금부터이다. 이를 해결하기 위해 프록시 패턴을 사용할 수 있다. 프록시는 사용 목적에 따라 두 가지로 구분할 수 있다.

 

(1) 클라이언트가 타깃에 접근하는 것을 제어하기 위해서

(2) 타깃에 부가적인 기능을 부여해주기 위해서

 

여기서 2번째 사용목적을 의미하는 프록시를 사용하면 위의 문제를 해결할 수 있다. 디자인 패턴적으로 봤을 때 여기에는 데코레이터 패턴이 들어간다. 

 

 

데코레이터 패턴이란?


 

데코레이터 패턴은 타깃에 부가적인 기능을 런타임에 다이나믹하게 부여해주기 위해 프록시를 사용하는 패턴을 의미한다. 객체를 부가기능으로 포장한다고 생각하면 된다.

 

 

핵심 기능은 본인의 이름을 출력하는 것이라고 했을 때, 자기소개를 하기에 따라 '내 이름은  ~ 입니다.' 혹은 '나는 ~ 이에요.' 등으로 문장을 꾸밀 수 있다. 어쨌든 핵심 기능은 이름을 출력하는 것이고, 그 외의 부가기능은 그 외의 문장이라고 했을 때 다음과 같이 데코레이터 패턴을 적용해볼 수 있다.

 

 

그리고 외부에서는 agent를 사용하는 것이 아니라 agentProxy를 사용할 수 있다. 이렇게 나누게되면 핵심 기능을 agent에 위임하고 그 밖의 부가 기능만 proxy가 처리해주면 된다. 따라서 핵심기능을 담당하는 agent에서는 부가기능을 고려하지 않아도 된다. 그리고 프록시 내부의 필드도 인터페이스이기 때문에 어느 데코레이터에서 어느 타깃으로 연결될지에 대해서는 코드 레벨에서 알 수 없다는 점도 특징이다. 

 


 

위와 같은 데코레이터 패턴을 적용한 프록시는 타깃의 기능을 확장하거나 추가하는 대신 클라이언트가 타깃에 접근하는 방식을 변경한다. 타깃 오브젝트를 생성하기 복잡하거나 이를 바로 필요로하지 않는다면 생성 시점을 미뤄주는 것이 좋다고 한다.(왜 그런지는 잘 모르지만 아마 초기 로드하는데 걸리는 시간을 줄이기 위해서가 아닐까 싶다.) 위와 같이 프록시 패턴을 사용하는 경우에는 클라이언트에게 타깃에 대한 레퍼런스를 넘길 때에도 타깃 오브젝트 대신 프록시를 넘겨줄 수 있기 때문에 프록시 생성을 최대한 늦출 수 있다. 

 

말만 들으면 프록시가 만능인 것 같다. 핵심 기능을 수정하지도 않고 처리가 가능하다니! 그러면 확장성이 어마무시한거 아닌가? OCP에 완전히 충실해보인다. 하지만 프록시를 만드는 과정은 상당히 번거롭게 느껴진다. 매번 새로운 클래스도 정의해야하고 메서드도 일일히 구현해서 위임해야하기 때문이다. 물론 depth가 깊어지면 복잡도도 높아진다는 문제가 있어보이기도 한다. 또한 부가기능 코드가 중복될 가능성도 존재한다. 

 

예시를 한번 살펴보자.

 

Agent의 메서드가 많아졌다.

 

따라서 이를 구현하는 구현체들의 메서드들 또한 많아지게 된다. 

물론 이때 사용하는 부가기능은 동일하다.

그래서 위와 같이 중복코드가 반복적으로 나타나게 된다. 무척 비효율적으로 느껴진다. 

 

이러한 문제를 해결하기 위해 JDK의 다이내믹 프록시를 적용할 수 있다. 다이내믹 프록시는 리플렉션 기능을 이용해서 프록시를 만들어준다. 

 

다이내믹 프록시는 프록시 팩토리에 의해 런타임 시 다이내믹하게 만들어지는 오브젝트이다. 다이내믹 프록시 오브젝트는 타깃의 인터페이스와 같은 타입으로 만들어진다. 리플렉션 기능을 사용하기 때문에 invoke 메서드를 통해 메서드와 인자를 전달받는다. 이후 부가기능을 invoke 메서드 내에서 처리해주면 된다. 다이내믹 프록시가 온전히 타깃에게 위임을 해서 처리하기 위해서는 핸들러를 가지고 처리해야한다. 

중간에 한글 인코딩이 안돼서 영어로 변경했다.

그래서  InvocationHandler를 구현한 구현체를 만들고, 내부에서 Agent를 전달하여 각각 메서드를 공통적으로 처리해준다. 

 

그리고 위와 같이 각 메서드를 호출하면 아래와 같은 출력문구가 등장한다.

 

 

그러면 위와 같이 공통적인 기능들로 묶어줄 수 있다.

 


그런데, 여기서 또 다시 문제가 발생한다. 스프링 프레임워크를 사용한다면 우리가 위와 같은 프록시 객체를 빈으로 등록해야하는데 이를 빈으로 등록할 수 있는 방법이 없다는 점이다. 스프링의 빈은 기본적으로 클래스 이름과 프로퍼티로 정의된다. 스프링은 지정된 클래스의 이름을 가지고 리플렉션을 이용해서 해당 클래스의 오브젝트를 만든다. 다이내믹 프록시 오브젝트는 이러한 방식으로 생성되지 않는다. 클래스 자체를 내부적으로 동적으로 새로 정의해서 사용하기 때문에 사전에 프록시 오브젝트의 클래스 정보를 미리 알아내서 스프링 빈으로 등록할 수 없다.

 

다행인건는 스프링에서 디폴트 생성자를 통해서 오브젝트를 만드는 것 뿐만아니라 팩토리 빈을 이용해 오브젝트를 만들기도 한다는 점이다. 우리는 이러한 팩토리 빈 인터페이스를 직접 구현해서 오버라이딩하고, 이를 등록하면 된다. 이때 중요한 점은 팩토리 빈의 getObject라는 메서드에 다이내믹 프록시 오브젝트를 만드는 코드를 넣어주면 사용할 수 있다. 

 

우리는 aop를 설명하기 위한 빌드업을 쭉 하고 있기에 팩토리 빈에 대한 예시는 패스하도록 하겠다.

 

어쨌든 이러한 팩토리 빈을 사용하게 되면 재사용할 수 있다는 점에서 기존 코드에 비해(아까 보았던 하나하나 기능을 추가하는 코드) 많이 개선된 것을 알 수 있다. 부가기능 코드의 중복 문제도 해결될 뿐만아니라 일일히 메서드를 구현하는 지루한 과정도 하지 않아도 되기 때문에 굉장히 큰 이점이 있다고도 볼 수 있다. 또한 DI도 할 수 있으니 모든 문제가 해결된 것처럼 보인다. 

 

하지만 한번에 여러 클래스에 공통적인 부가기능을 적용해야할 때 하나의 타킷에 여러 서로 다른 부가기능을 적용할 때에는 문제가 발생한다. 여전히 위에서 보았던 문제와 비슷한 양상을 띄는 것 같다. 이를 어떤 식으로 해결할 지는 다음 포스팅에서 한번 다뤄보도록 하겠다.

 

728x90
반응형