들어가며
다이내믹 프록시를 이용한 부가 기능 분리 포스트에서 트랜잭션이라는 부가 기능을 비즈니스 로직으로부터 분리하기 위해 다이내믹 프록시를 도입해보았고, 이러한 다이내믹 프록시를 스프링 빈으로 등록해서 사용하기 위해 팩토리 빈 방식을 사용해보았다.
하지만 이러한 프록시 팩토리 빈 방식은 부가 기능을 담당하는 InvocationHandler를 구현한 오브젝트가 프록시 팩토리 빈의 개수(부가 기능을 사용하는 빈의 개수)만큼 만들어진다는 문제점과 여러 개의 클래스에 부가 기능을 적용해야 한다면 프록시 팩토리 빈을 생성하는 설정 코드가 중복되는 것을 막을 수 없다는 문제점이 존재하였다.
이번 포스트에서는 앞서 언급한 문제점들을 조금 더 살펴본 뒤, AOP와 빈 후처리기를 이용하여 해당 문제점들을 해결해보도록 하겠다.
문제점 분석
문제점 1. 부가 기능을 담당하는 오브젝트의 중복
기존의 부가 기능을 담당하는 InvocationHandler를 구현한 오브젝트는 요청을 위임할 타깃 오브젝트를 가지고 있어야 하기 때문에 여러 개의 클래스에 부가 기능을 적용해야 한다면 적용하려는 클래스의 개수만큼 오브젝트를 만들어야 한다.
이러한 문제점을 해결하기 위해서는 부가 기능을 타깃으로부터 분리할 필요가 있다. 그래야지 부가 기능을 하나만 생성해서 여러 클래스가 공유해서 사용하도록 할 수 있을 것이다.
문제점 2. 프록시 팩토리 빈 생성 코드 중복
기존 방식으로는 여러 개의 클래스에 부가 기능을 적용해야 한다면 적용하려는 클래스의 개수만큼 프록시 팩토리 빈 생성 코드를 작성해야 한다. 이러한 중복을 막기 위한 방법은 없는 것일까? 스프링 빈 오브젝트가 생성될 때마다 해당 빈 오브젝트가 부가 기능 적용의 대상이라면 프록시로 감싸주는, 그런 기능이 있다면 중복 되는 생성 코드를 작성하는 수고를 덜 수 있을 것 같지 않은가?
이제 위 2가지 문제점의 해결책에 대해 알아보도록 하자.
AOP란 무엇인가
AOP란 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스펙트라는 모듈로 만들어서 설계하고 개발하는 방법을 말한다.

즉, 위 그림에서 트랜잭션 처리와 같은 부가 기능을 별도의 모듈(애스펙트)로 분리하는 것이다. 만약 부가 기능이 핵심 기능 안으로 들어가 버리면, 핵심 기능 설계에 객체지향적인 가치를 온전히 부여하기 어려워진다. AOP는 애스펙트를 분리함으로써 핵심 기능을 설계하고, 구현할 때 객체지향적인 가치를 지킬 수 있도록 도와주는 것이라고 보면 된다.
AOP를 이용한 부가 기능 담당 오브젝트 중복 문제 해결
첫 번째 문제점을 해결하기 위해서는 부가 기능을 타깃으로부터 분리할 필요가 있다. 따라서 AOP를 활용해서 부가 기능을 별도의 모듈로 분리하는 방법을 생각해볼 수 있다.
그런데 여기서 "부가 기능을 담당하는 오브젝트를 타깃으로부터 분리하면 이제 더 이상 타깃을 모르는데 어떻게 비즈니스 로직을 호출할 수 있는가?" 하는 의문이 들 수 있다. 이는 스프링이 제공하는 ProxyFactoryBean을 사용하여 프록시를 생성하면 부가 기능을 담당하는 오브젝트는 ProxyFactoryBean으로부터 타깃 오브젝트에 대한 정보까지도 함께 제공받기 때문에 타깃에 상관없이 부가 기능 오브젝트를 독립적으로 만들 수 있는 것이다.
이러한 스프링이 제공하는 프록시 생성 기술의 이용은 빈 후처리기에서 좀 더 자세히 알아보도록 하자.
이제 부가 기능 모듈인 애스펙트를 만들어 볼 차례이다.
애스펙트는 한 개 또는 그 이상의 포인트컷과 어드바이스의 조합으로 만들 수 있다. 여기서는 모든 Service 클래스의 모든 메서드에 적용할 부가 기능 모듈을 만들어보도록 하자.
어드바이스 - 타깃에게 제공할 부가 기능을 담은 모듈
조인 포인트 - 어드바이스가 적용될 수 있는 위치
포인트컷 - 어드바이스를 적용할 조인 포인트를 선별하는 작업

위 애스펙트 코드를 보면, 먼저 어드바이스에 동작 시점을 @Around 어노테이션으로 설정하여 핵심 로직 실행 전후에 부가 기능(applyTransaction 어드바이스)을 수행할 수 있도록 하였다. 그리고 @Around 어노테이션에 포인트컷 표현식을 통해 포인트컷을 "execution(* *..*.Service.*(..))"와 같이 기술하여 모든 Service 클래스의 모든 메서드에 어드바이스를 적용해주도록 하였다.
다음으로 어드바이스의 로직을 보면 부가 기능(트랜잭션)을 적용한 뒤, ProceedingJoinPoint를 통해 비즈니스 메소드를 호출한다. ProceedingJoinPoint는 JoinPoint 인터페이스를 상속한 인터페이스로 타깃 오브젝트에 대한 정보를 가지고 있을 뿐만 아니라 핵심 로직을 호출하는 proceed 메서드를 지원한다.
이렇게 부가 기능 모듈인 애스펙트를 구현해보았는데, 어디에도 타깃에 대한 코드가 들어가있지 않다. 따라서 이러한 애스펙트도 독립적인 싱글톤 빈으로 등록하고 여러 프록시가 사용할 수 있도록 할 수 있다.
정리해보면, AOP를 도입하여 부가 기능을 별도의 모듈로 분리함으로써 더 이상 부가 기능을 담당하는 오브젝트가 프록시의 개수만큼 만들어지지 않게 되었고, 단 하나의 애스펙트만 스프링 빈으로 등록해서 여러 프록시가 공유해서 사용할 수 있게 되었다.
포인트컷 표현식은 별도로 학습하도록 하자.
빈 후처리기를 이용한 프록시 팩토리 빈 생성 코드 중복 문제 해결
두 번째 문제점을 해결하기 위해서는 스프링 빈 오브젝트가 생성될 때마다 해당 빈 오브젝트가 부가 기능 적용의 대상이라면 프록시로 감싸주는 기능이 필요하다. 이러한 기능은 스프링이 제공하는 빈 후처리기 중 하나인 DefaultAdvisorAutoProxyCreator가 제공해준다.
그렇다면 DefaultAdvisorAutoProxyCreator는 어떻게 동작하는지 알아보도록 하자.

DefaultAdvisorAutoProxyCreator 빈 후처리기를 스프링 빈으로 등록하면 스프링은 빈 오브젝트를 만들 때마다 후처리기에게 빈을 보낸다. 그러면 DefaultAdvisorAutoProxyCreator는 빈으로 등록된 모든 애스펙트 내의 포인트컷을 이용해 전달받은 빈이 프록시 적용 대상인지 확인한다.
적용 대상이라면 그때는 내장된 프록시 생성기에게 현재 빈에 대한 프록시를 만들게 하고, 만들어진 프록시에 애스펙트를 연결해준다. 빈 후처리기는 프록시가 생성되면 원래 컨테이너가 전달해준 빈 오브젝트 대신 프록시 오브젝트를 컨테이너에게 돌려주고, 컨테이너는 최종적으로 빈 후처리기가 돌려준 프록시 오브젝트를 빈으로 등록한다.
// 빈 후처리기 등록
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
return new DefaultAdvisorAutoProxyCreator();
}
따라서 개발자는 부가 기능인 어드바이스와 부가 기능을 적용할 빈을 선정하는 로직이 담긴 포인트컷의 조합인 애스펙트만 빈으로 등록하고 빈 후처리기를 사용하면, 일일이 프록시 팩토리 빈 생성 코드를 작성하지 않아도 타깃 오브젝트에 자동으로 프록시가 적용되도록 할 수 있게 된다.
사실 스프링 부트의 Common Application Properties를 보면 @EnableAspectJAutoProxy 어노테이션을 달아주는 spring.aop.auto 프로퍼티의 default 설정이 true이다.
즉, 이미 기본적으로 스프링 빈 오브젝트가 만들어질 때마다 애스펙트를 확인해서 적용 대상인지 판단한 후, 적용 대상이라면 프록시를 만들어준다는 것이다. 따라서 스프링 부트를 사용한다면 빈 후처리기를 도입하지 않아도 정상적으로 동작하게 된다.
마치며
다이내믹 프록시를 이용하여 부가 기능을 분리했을 때 발생하는 문제점들을 AOP를 통해 해결해보았다. 더 이상 부가 기능을 담당하는 오브젝트가 프록시의 개수만큼 생성되지도 않고, 프록시를 생성하는 코드가 중복되지도 않는다. 이처럼 AOP는 핵심 기능에서 부가 기능을 깔끔하게 분리할 수 있도록 도와주는 강력한 기법이다. 이번 포스트을 작성하며 AOP에 조금 더 익숙해질 수 있었던 것 같고, 동시에 아직 많은 학습이 필요하다는 생각이 들었다.
혹시 잘못된 부분이 있다면 지적 혹은 조언 부탁드립니다:)
Reference
'Backend > Spring' 카테고리의 다른 글
테스트 격리하기 (0) | 2022.05.04 |
---|---|
트랜잭션 전파 알아보기 (0) | 2022.04.26 |
다이내믹 프록시를 활용한 JPA QueryCounter 구현기 (0) | 2022.03.20 |
다이내믹 프록시를 이용한 부가 기능 분리 (0) | 2022.03.12 |
의존관계 주입(DI)과 객체 지향 설계 (0) | 2022.03.02 |