Study with book/토비의 스프링 3.1

6장. AOP3


6.5 스프링 AOP

6.5.1 자동 프록시 생성

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/6.5.1

프록시 팩토리 빈 방식의 접근 방법의 한계라고 생각했던 두 가지 문제가 있었다. 그 중에서 부가기능이 타깃 오브젝트마다 새로 만들어지는 문제는 스프링 ProxyFactoryBean의 어드바이스를 통해 해결됐다. 남은 것은 부가기능의 적용이 필요한 타깃 오브젝트마다 거의 비슷한 내용의 ProxyFactoryBean 빈 설정정보를 추가해주는 부분이다. 새로운 타깃이 등장했다고 해서 코드를 손댈 필요는 없어졌지만, 설정은 매번 복사해서 붙이고 target 프로퍼티의 내용을 수정해줘야 한다.

 

중복 문제의 접근 방법

반복적인 프록시의 메소드 구현을 코드 자동생성 기법을 이용해 해결했다면 반복적인 ProxyFactoryBean 설정 문제는 설정 자동등록 기법으로 해결할 수 는 없을까?

 

빈 후처리기를 이용한 자동 프록시 생성기

스프링은 컨테이너로서 제공하는 기능 중에서 변하지 않는 핵심적인 부분 외에는 대부분 확장할 수 있도록 확장 포인트를 제공한다. 그 중에서 관심을 가질 만한 확장 포인트는 바로 BeanPostProcessor 인터페이스를 구현해서 만드는 빈 후처리기다. 빈 후처리기는 이름 그대로 스프링 빈 오브젝트로 만들어지고 난 후에, 빈 오브제트를 다시 가공할 수 있게 해준다.

DefaultAdvisorAutoProxyCreator는 어드바이저를 이용한 자동 프록시 생성기다. 빈 후처리기 자체를 빈으로 등록하면 스프링은 빈 후처리기가 빈으로 등록되어 있으면 빈 오브젝트가 생성될 때마다 빈 후처리기에 보내서 후처리 작업을 요청한다.

이를 잘 이용하면 스프링이 생성하는 빈 오브젝트의 일부를 프록시로 포장하고, 프록시를 빈으로 대신 등록할 수도 있다. 

빈 후처리기를 이용한 프록시 자동 생성

DefaultAdvisorAutoProxyCreator 빈 후처리기가 등록되어 있으면 스프링은 빈 오브젝트를 만들 때마다 후처리기에게 빈을 보낸다. DefaultAdvisorProxyCreator는 빈으로 등록된 모든 어드바이저 내의 포인트컷을 이용해 전달받은 빈이 프록시 적용 대상인지 확인한다. 프록시 적용 대상이면 그때는 내장된 프록시 생성기에게 현재 빈에 대한 프록시를 만들게 하고, 만들어진 프록시에 어드바이저를 연결해준다.

 

확장된 포인트컷

두 가지 기능을 정의한 Pointcut 인터페이스

만약 Pointcut 선정 기능을 모두 적용한다면 먼저 프록시를 적용할 클래스인지 판단하고 나서 적용 대상 클래스인 경우에는 어드바이스를 적용할 메소드인지 확인하는 식으로 동작한다. 클래스 자체가 프록시 적용 대상이 아니라면 어드바이스를 통한 부가기능 부여는 적용되지 않는다.

 

6.5.2 DefaultAdvisorAutoProxyCreator의 적용

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/6.5.2

 

클래스 필터를 적용한 포인트컷 작성

내용은 실습 코드로 대체

 

어드바이저를 이용하는 자동 프록시 생성기 등록

적용할 자동 프록시 생성기인 DefaultAdvisorAutoProxyCreator는 등록된 빈 중에서 Advisor 인터페이스를 구현한 것을 모두 찾는다. 그리고 생성되는 모든 빈에 대해 어드바이저의 포인트컷을 적용해보면서 프록시 적용 대상을 선정한다.

 

포인트컷 등록

내용은 실습 코드로 대체

 

어드바이스와 어드바이저

ProxyFactoryBean으로 등록한 빈에서처럼 transactionAdvisor를 명시적으로 DI하는 빈은 존재하지 않는다. 대신 어드바이저를 이용하는 다동 프록시 생성기인 DefaultAdvisorAutoProxyCreator에 의해 자동수집되고, 프록시 대상 선정 과정에 참여하며, 자동생성된 프록시에 다이내믹하게 DI돼서 동작하는 어드바이저가 된다.

 

ProxyFactoryBean 제거와 서비스 빈 원상복구

프록시를 도입했던 때부터 아이디를 바꾸고 프록시에 DI 돼서 간접적으로 사용돼야 했던 userServiceImpl 빈의 아이디를 이제는 당당하게 userService로 되돌려놓을 수 있다.

 

자동 프록시 생성기를 사용하는 테스트

내용은 실습 코드로 대체

 

자동생성 프로시 확인

내용은 실습 코드로 대체

 

6.5.3 포인트컷 표현식을 이용한 포인트컷

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/6.5.3

 

6.5.4 AOP란 무엇인가?

AOP: 애스펙트 지향 프로그래밍

전통적인 객체지향 기술의 설계 방법으로는 독립적인 모듈화가 불가능한 트랜잭션 경계설정과 같은 부가기능을 어떻게 모듈화할 것인가를 연구해온 사람들은 이 부가기능 모듈화 작업은 기존의 객체지향 설계 패러다임과는 구분되는 새로운 특성이 있다. 그래서 이런 부가기능 모듈을 객체지향 기술에서 주로 사용하는 오브젝트와는 다르게 특별한 이름으로 부르기 시작했다. 그것이 바로  애스펙트이다. 에스펙트란 그 자체로 애플리케이션의 핵심기능을 담고 있지는 않지만, 애플리케이션을 구성하는 중요한 한 가지 요소이고, 핵심기능에 부가되어 의미를 갖는 특별한 모듈을 가리킨다. 

애스펙트는 부가될 기능을 정의한 코드인 어드바이스와 어드바이스를 어디에 적용할지를 결정하는 포인트컷을 함께 갖고 있다. 에스펙트는 그 단어의 의미대로 애플리케이션을 구성하는 한 가지 측면이라고 생각할 수 있다. 

독립 애스펙트를 이용한 부가기능의 분리와 모듈화

왼쪽은 애스펙트로 부가기능을 분리하기 전의 상태다. 핵심기능은 깔끔한 설계를 통해서 모듈화되어 있고, 객체지향적인 장점을 잘 살릴 수 있도록 만들었지만, 부가기능이 핵심기능의 모듈에 침투해 들어가면서 설계와 코드가 모두 지저분해졌다. 트랜잭션 경계설정 코드를 처음 사용자 관리 서비스 클래스에 추가했을 때를 생각해보자. 핵심기능을 담은 코드는 부가기능인 트랜잭션 코드와 함께 섞여 있어서 핵심기능인 사용자 관리 로직을 파악하고, 수정하고, 테스트하기가 매우 불편했다. 트랜잭션 외에도 핵심기능이 아닌 다양한 부가기능을 모두 넣으면 아마도 핵심기능은 부가기능 코드에 가려서 보이지 않을지도 모른다. 나름 트랜잭션 추상화까지는 적용했지만 더 이상은 기존의 객체지향 설계 기법으로 해결할 방법은 없다.

 

오른쪽은 이렇게 핵심기능 코드 사이에 침투한 부가기능을 독립적인 모듈인 애스펙트로 구분해낸 것이다. 2차원적인 평면구조에서는 어떤 설계 기법을 동원해도 해결할 수 없었던 것을 3차원의 다면체 구조로 가져가면서 각각 성격이 다른 부가기능은 다른 면에 존재하도록 만들었다. 이렇게 독립된 측면에 존재하는 애스펙트로 분리한 덕에 핵심기능은 순수하게 그 기능을 담은 코드로만 존재하고 독립적으로 살펴볼 수 있도록 구분된 면에 존재하게 된 것이다.

결국 런타임 시에는 왼쪽의 그림처럼 각 부가기능 애스펙트는 자기가 필요한 위치에 다이내믹하게 참여하게 될 것이다. 하지만 설계와 개발은 오른쪽 그림처럼 다른 특성을 띤 애스펙트들을 독립적인 관점으로 작성하게 할 수 있다.

 

이렇게 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스펙트라는 독특한 모듈로 만들어서 설계하고 개발하는 방법을 애스펙트 지향 프로그래밍(AOP)이라고 부른다. AOP는 OOP를 돕는 보조적인 기술이지 OOP를 완전하게 대체하는 새로운 개념은 아니다. 왼쪽 그림처럼 부가기능이 핵심기능 안으로 침투해서 들어가 버리면, 핵심기능 설계에 객체지향 기술의 가치를 온전히 부여하기가 힘들어진다. 부가된 코드로 인해 객체지향적인 설계가 주는 장점을 잃어버리기 십상이다. AOP는 애스펙트를 분리함으로써 핵심기능을 설계하고 구현할 때 객체지향적인 가치를 지킬 수 있도록 도와주는 것이라고 보면 된다.

 

6.5.5 AOP 적용 기술

프록시를 이용한 AOP

프록시로 만들어서  DI로 연결된 빈 사이에 적용해 타깃의 메소드 호출 과정에 참여해서 부가기능을 제공해주도록 만들었다. 따라서  스프링 AOP는 자바의 기본 JDK와 스프링 컨테이너 외에는 특별한 기술이나 환경을 요구하지 않는다. 스프링 AOP의 부가기능을 담은 어드바이스가 적용되는 대상은 오브젝트의 메소드다. 프록시 방식을 사용했기 때문에 메소드 호출 과정에 참여해서 부가기능을 제공해주게 되어 있다. 어드바이스가 구현하는 MethodInterceptor 인터페이스는 다이내믹 프록시의 InvocationHandler와 마찬가지로 프록시로부터 메소드 요청정보를 전달받아서 타깃 오브젝트의 메소드를 호출한다. 타깃의 메소드를 호출하는 전후에 다양한 부가기능을 제공할 수 있다.

 

바이트코드 생성과 조작을 통한 AOP

AOP 기술의 원조이자 가장 강력한 AOP 프레임워크로 꼽히는 AspectJ는 프록시를 사용하지 않는 대표적인 AOP 기술이다. AspectJ는 스프링처럼 다이내믹 프록시 방식을 사용하지 않는다. AspectJ는 프록시처럼 간접적인 방법이 아니라, 타깃 오브젝트를 뜯어고쳐서 부가 기능을 직접 넣어주는 직접적인 방법을 사용한다. 부가기능을 넣는다고 타깃 오브젝트의 소스코드를 수정할 수 없으니, 컴파일된 타깃의 클래스 파일 자체를 수정하거나 클래스가 JVM에 로딩되는 시점을 가로채서 바이트코드를 조작하는 복잡한 방법을 사용한다. 

 

왜 Aspectj는 컴파일된 클래스 파일 수정이나 바이트코드 조작과 같은 복잡한 방법을 사용할까?

첫째는 바이트코드를 조작해서 타깃 오브젝트를 직접 수정해버리면 스프링과 같은 DI 컨테이너의 도움을 받아서 자동 프록시 생성 방식을 사용하지 않아도 AOP를 적용할 수 있기 때문이다. 스프링과 같은 컨테이너가 사용되지 않는 환경에서도 손쉽게 AOP의 적용이 가능해진다.

둘째는 프록시 방식보다 훨씬 강력하고 유연한 AOP가 가능하기 때문이다. 프록시를 AOP의 핵심 메커니즘으로 사용하면 부가기능을 부여할 대상은 클라이언트가 호출할 때 사용하는 메소드로 제한된다. 하지만 바이트코드를 직접 조작해서  AOP를 적용하면 오브젝트의 생성, 필드 값의 조회와 조작, 스태틱 초기화 등의 다양한 작업에 부가기능을 부여해줄 수 있다.

 

물론 대부분의 부가기능은 프록시 방식을 사용해 메소드의 호출 시점에 부여하는 것으로도 충분하다. 게다가 AspectJ 같은 고급 AOP 기술은 바이트코드 조작을 위해 JVM의 실행 옵션을 변경하거나 별도의 바이트코드 컴파일러를 사용하거나, 특별한 클래스 로더를 사용하게 하는 등의 번거로운 작업이 필요하다. 따라서 일반적인 AOP를 적용하는 데는 프록시 방식의 스프링 AOP로도 충분하다. 

 

6.5.6 AOP용어

타깃

타깃은 부가기능을 부여할 대상이다.

 

어드바이스

타깃에게 제공할 부가기능을 담은 모듈이다. 어드바이스는 오브젝트로 정의하기도 하지만 메소드 레벨에서 정의할 수도 있다.

 

조인포인트

어드바이스가 적용될 수 있는 위치를 말한다. 스프링의 AOP에서 조인 포인트는 메소드의 실행 단계뿐이다. 타깃 오브젝트가 구현한 인터페이스의 모든 메소드는 조인 포인트가 된다.

 

포인트컷

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

 

프록시

클라이언트와 타깃 사이에 투명하게 존재하면서 부가기능을 제공하는 오브젝트이다.

 

어드바이저

포인트컷과 어드바이스를 하나씩 갖고 있는 오브젝트이다. 어드바이저는 어떤 부가기능(어드바이스)을 어디에(포인트컷) 전달할 것인가를 알고 있는 AOP의 가장 기본이 되는 모듈이다.

 

애스펙트

한 개 또는 그 이상의 포인트컷과 어드바이스의 조합으로 만들어지며 보통 싱글톤 형태의 오브젝트로 존재한다. 

 

6.5.7 AOP 네임스페이스

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/6.5.7


[참고자료]

토비 스프링 vol.1 - 이일민

'Study with book > 토비의 스프링 3.1' 카테고리의 다른 글

6장. AOP4  (0) 2020.04.26
6장. AOP2  (1) 2020.04.19
6장. AOP1  (0) 2020.04.12
5장. 서비스 추상화  (0) 2020.04.05
4장. 예외  (0) 2020.03.29