Study with book/토비의 스프링 3.1

6장. AOP1


서비스 추상화와 더블어 스프링 3대 기반기술의 하나인 AOP에 대해서 알아보자. OOP를 대체하려고 하는 것처럼 보이는 AOP라는 이름 뒤에 감춰진, 그 필연적인 등장배경과 스프링이 그것을 도입한 이유, 그 적용을 통해 얻을 수 있는 장점이 무엇인지에 대한 충분한 이해를 해보자.

6.1 트랜잭션 코드의 분리

6.1.1 메소드 분리

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

메소드로 비지니스 로직을 담당하는 코드를 독립시켜보자. 적어도 순수하게 사용자 레벨 업그레이드를 담당하는 비지니스 로직 코드만 메소드에 담겨 있으니 이해하기도 편하고, 수정하기에도 부담이 없다.

 

6.1.2 DI를 이용한 클래스 분리

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

그러나 트랙잭션을 담당하는 기술적이 코드가 버젓이 UserService 안에 자리 잡고 있다. 트랜잭션 코드를 밖으로 뽑아내는 여정을 떠나보자.

 

DI 적용을 이용한 트랜잭션 분리

트랜잭션 코드를 어떻게 해서든 밖으로 빼버리면 UserSerivce 클래스를 직접 사용하는 클라이언트 코드에서는 트랜잭션 기능이 빠진 UserService를 사용하게 될 것이다. 구체적인 클래스를 직접 참조하는 경우의 전형적인 단점이다. 현재 구조는 UserService 클래스와 그 사용 클라이언트 간의 관계가 강한 결합도로 고정돼 있다. 이 사이를 비집고 다른 무언가를 추가하기는 힘들다. UserService를 인터페이스로 만들자. 그렇게되면 유연한 확장이 가능해진다. 보통은 DI에를 사용하는 이유는 구현 클래스를 바꿔가면서 사용하기 위해서이다. 그러나 꼭 그래야 한다는 제약은 없다. 우리는 현재 트랜잭션 경계설정을 담당하는 코드를 외부로 빼내려는 것이다. 

트랜잭션 경계설정을 위한 UserServiceTx의 도입

UserServiceTx 구현체는 UserServiceImpl를 대신하려고하는 것이 아니다. 단지 트랜잭션의 경계설정이라는 책임을 맡고 있을 뿐이다. 그리고 스스로는 비지니스 로직을 담고 있지 않기 때문에 또 다른 비지니스 로직을 담고 있는 UserServiceImpl 클래스에게 작업을 위임한다. 이렇게하면 클라이언트 입장에서 볼 때는 결국 트랜잭션이 적용된 비지니스 로직 구현이라는 기대하는 동작이 일어날 것이다.

 

UserService 인터페이스 도입

이렇게 UserService 인터페이스를 도입하니 트랜잭션을 고려하지 않고 단순하게 로직만을 구현했던 처음 모습으로 돌아왔다. 이 자체로만 보면 UserService는 User라는 도메인 정보를 가진 비지니스 로직에만 충실한 깔끔한 코드이다.

 

분리된 트랜잭션 기능

UserServiceTx 구현체의 경우 같은 인터페이스를 구현한 다른 오브젝트에게 고스란히 작업을 위임한다. 이렇게하면 적어도 비지니스 로직에 대해서는 UserServiceTx가 아무런 관여도 하지 않는다. 그리고 UserServiceTx에서 트랜잭션을 적용하고 싶은 메소드에 트랜잭션 관련한 작업을 넣어주면 된다. 

 

트랜잭션 적용을 위한 DI 설정

클라이언트가 UserService라는 인터페이스를 통해 사용자 관리 로직을 이용하려고 할 때 먼저 트랜잭션을 담당하는 오브제트가 사용돼서 트랜잭션에 관련된 작업을 진행해주고, 실제 사용자 관리 로직을 담은 오브젝트가 이후에 호출돼서 비지니스 로직에 관련된 작업을 수행하도록 만든다.

트랜잭션 기능의 오브젝트가 적용된 의존관계

 

트랜잭션 분리에 따른 테스트 수정

TestUserService 오브젝트를 UserServiceTx 오브젝트에 수동 DI시킨 후에 트랜잭션 기능까지 포함된 UserServiceTx의 메소드를 호출하면서 테스트를 수행하도록 해야 한다.

 

트랜잭션 경계설정 코드 분리의 장점

트랜잭션 경계설정 코드의 분리와 DI를 통한 연결은 어떤 장점이 있을까?

첫째, 비지니스 로직을 담당하고 있는 UserServiceImpl의 코드를 작성할 때는 트랜잭션과 같은 기술적인 내용에는 전혀 신경 쓰지 않아도 된다.

둘째, 비지니스 로직에 대한 테스트를 손쉽게 만들어낼 수 있다.

6.2 고립된 단위 테스트

6.2.1 복잡한 의존관계 속의 테스트

테스트 대상의 의존구조

UserService는 UserDao, TransactionManager, MailSender라는 세 가지 의존관계를 가지고 있다. 따라서 그 세 가지 의존관계를 갖는 오브젝트들이 테스트가 진행되는 동안 같이 실행된다. 따라서 UserService를 테스트하는 것처럼 보이지만 사실은 그 뒤에 존재하는 훨씬 더 많은 오브젝트와 환경, 서비스, 서버, 심지어 네트워크까지 함께 테스트하는 셈이 된다.

 

6.2.2 테스트 대상 오브젝트 고립시키기

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

따라서 우리는 다른 클래스의 코드에 종속되고 영향을 받지 않도록 고립시킬 필요가 있다. 바로 테스트를 위한 대역을 사용하는 것이다.

테스트를 위한 UserServiceImpl 고립

고립시킨 UserServiceImpl에 대한 테스트 구조

의존 오브젝트나 외부 서비스에 의존하지 않는 고립된 테스트 방식으로 만든 UserServiceImpl은 아무리 그 기능을 수행돼도 그 결과가 DB 등을 통해서 남지 않으니, 기존 방법으로는 작업 결과를 검증하기 힘들다. 그래서 이럴 땐 테스트 대상인 UserServiceImpl과 그 협력 오브젝트인 UserDao에게 어던 요청을 했는지 확인하는 작업이 필요하다. 테스트 중에 DB에 결과가 반영되지는 않았지만, UserDao의 update() 메소드를 호출하는 것을 확인할 수 있다면, DB에 그 결과가 반영될 것이라고 결론을 내릴 수 있다.

 

6.2.3 단위 테스트와 통합 테스트

단위 테스트는 정하기 나름이다. 사용자 관리 기능 전체를 하나의 단위로 볼 수도 있고 하나의 클래스나 하나의 메소드를 단위로 볼 수 있다. 중요한 것은 하나의 단위에 초점을 맞춘 테스트라는 점이다. 테스트 대상 클래스를 목 오브젝트 등의 테스트 대역을 이용해 의존 오브젝트나 외부 리소스를 사용하지 않도록 고립시켜서 테스트를 하는 것단위 테스트라고 토비 스프링 책에서는 정의한다. 반면에 두 개 이상의 성격이나 계층이 다른 오브젝트가 연동하도록 만들어 테스트하거나 외부의 DB나 파일 서비스 등의 리소스가 참여하는 테스트통합 테스트라고 부른다.

 

6.2.4 목 프레임워크

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

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

6.3.1 프록시와 프록시 패턴, 테코레이터 패턴

핵심기능 인터페이스 적용

마치 자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 것을 대리자, 대리인과 같은 역할을 한다고 해서 프록시라고 부른다.

 

데코레이터 패턴

테코레이터 패턴은 타깃에 부가적인 기능을 런타임 시 다이내믹하게 부여해주기 위해 프록시를 사용하는 패턴을 말한다. 다이내믹하게 기능을 부가한다는 의미는 컴파일 시점, 즉 코드상에서는 어떤 방법과 순서로 프록시와 타킷이 연결되어 사용되는지 정해져 있지 않다는 뜻이다.

테코레이터 패턴 적용 예

프록시 패턴

프록시 패턴에서 프록시는 타킷의 기능을 확장하거나 추가하지 않는다. 대신 클라이언트가 타킷에 접근하는 방식을 변경해준다. 타깃 오브젝트를 생성하기가 복잡하거나 당장 필요하지 않는 경우에는 꼭 필요한 시점까지 오브젝트를 생성하지 않는 편이 좋다. 그런데 타깃 오브젝트에 대한 레퍼런스가 미리 필요할 수 있다. 이럴 때 프록시 패턴을 적용하면 된다. 클라이언트에게 타깃에 대한 레퍼런스를 넘겨야 하는데, 실제 타깃 오브젝트는 만드는 대신 프록시를 넘겨주는 것이다. 만약 레퍼런스를 갖고 있지만 끝까지 사용하지 않거나, 많은 작업이 진행된 후에 사용되는 경우라면, 이렇게 프록시를 통해 생성을 최대한 늦춤으로써 얻는 장점이 많다.

또한 특별한 상황에서 타깃에 대한 접근권한을 제어하기 위해 프록시 패턴을 사용할 수 있다.

 

6.3.2 다이내믹 프록시

많은 개발자들은 타깃 코드를 직접 고치지 말지 번거롭게 프록시를 만들지 않겠다고 생각한다. 프록시를 만드는 일이 상당히 번거롭게 느껴지기 때문이다. 매번 새로운 클래스를 정의해야 하고, 인터페이스의 구현해야해야 할 메소드는 많으면 모든 메소드를 일일히 구현해서 위임하는 코드를 넣어야  하기 때문이다. 프록시도 일일이 모든 인터페이스를 구현해서 클래스를 새로 정의하지 않고도 편리하게 만들어서 사용할 방법이 있을까? 바로 자바에 java.lang.reflect 패키지 안에 프록시를 손쉽게 만들 수 있도록 지원해 준다.

 

프록시의 구성과 프록시 작성의 문제점

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

프록시는 타깃과 같은 메소드를 구현하고 있다가 메소드가 호출되면 타깃 오브젝트로 위임한다. 그리고 지정된 요청에 대해서는 부가기능을 수행한다. 왜 프록시를 만들기가 번거로울까?

첫 째는 타깃의 인터페이스를 구현하고 위임하는 코드를 작성하기가 번거롭다는 점이다. 부가기능이 필요 없는 메소드도 구현해서 타깃으로 위임하는 코드를 일일이 다 만들어줘야 한다.

둘 째는 부가기능 코드가 중복될 가능성이 많다.

 

리플렉션

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

 

프록시 클래스

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

 

다이내믹 프록시 적용

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

다이내믹 프록시는 프록시 팩토리에 의해 런타임 시에 다이내믹하게 만들어지는 오브젝트이다. 다이내믹 프록시 오브젝트는 타깃의 인터페이스와 같은 타입으로 만들어진다. 클라이언트는 다이내미 프록시 오브젝트를 타깃 인터페이스를 통해 사용할 수 있다. 이 덕분에 프록시를 만들 때 인터페이스를 모두 구현가면서 클래스를 정의하는 수고를 덜 수 있다. 프록시로서 필요한 부가기능 제공 코드는 직접 작성한다. 다이내믹 프록시 오브젝트는 클라이언트의 모든 요청을 리플렉션 정보로 변환해서 InvocationHandler 구현 오브젝트의 invoke() 메소드로 넘기는 것이다. 타깃 인터페이스의 모든 메소드 요청이 하나의 메소드로 집중되기 때문에 중복되는 기능을 효과적으로 제공할 수 있다.

InvocationHanlder를 통한 요청 처리 구조

6.3.3 다이내믹 프록시를 이용한 트랜잭션 부가기능

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

 

6.3.4 다이내믹 프록시를 위한 팩토리 빈

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

앞 절에서는 어떤 타겟에도 적용 가능한 트랜잭션 부가기능을 담은 TransactionHandler를 만들었고, 이를 이용하는 다이내믹 프록시를 UserService에 적용하는 테스트를 만들어 봤다. 이제 TransactionHandler와 다이내믹 프록시를 스프링의 DI를 통해서 사용할 수 있도록 만들어야 한다. 그런데 문제는 DI의 대상이 되는 다이내믹 프록시 오브젝트는 일반적인 스프링의 빈으로는 등록할 방법이 없다. 스프링은 내부적으로 리플렉션 API를 이용해서 빈 정의에 나오는 클래스 이름을 가지고 빈 오브젝트를 생성한다. 문제는 다이내믹 프록시 오브젝트는 이런 식으로 프록시 오브젝트가 생성되지 않는다.   

 

팩토리 빈

위 문제를 해결하기 위해서 스프링은 클래스 정보를 가지고 디폴트 생성자를 통해 오브젝트를 만드는 방법 외에도 빈을 만들 수 있는 여러 가지 방법을 제공하는데, 그 중 하나가 팩토리 빈을 이용하여 빈을 만드는 것이다. 팩토리 빈이란 스프링을 대신해서 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈을 말한다.

 

다이내믹 프록시를 만들어주는 팩토리 빈

팩토리 빈을 사용하면 다이내믹 프록시 오브젝트를 스프링의 빈으로 만들어줄 수가 있다.

팩토리 빈을 이용한 트랜잭션 다이내믹 프록시의 적용

트랜잭션 프록시 팩토리 빈

UserService 외에도 트랜잭션 부가기능이 필요한 오브젝트를 위한 프록시를 만들 때 얼마든지 재사용이 가능하다. 설정이 다른 여러 개의 TxProxyFactoryBean 빈을 등록하면 된다. 여기서 설정은 타겟을 의미한다.

 

6.3.5 프록시 팩토리 빈 방식의 장점과 한계

한번 부가기능을 가진 프록시를 생성하는 프록시 팩토리 빈을 만들어두면 타깃의 타입에 상관없이 재사용할 수 있다.

설정 변경을 통한 트랜잭션 기능 부가

프록시 팩토리 빈을 이용하면 프록시 기법을 아주 빠르고 효과적으로 적용할 수 있다. 코드 한 줄 만들지 않고 기존 코드에 부가적인 기능을 추가해줄 수 있다는 것은 매력적인 방법이다.

 

프록시 팩토리 빈의 장점

앞에서 테코레이터 패턴이 적용된 프록시를 사용하면 많은 장점이 있음에도 적극적으로 활용되지 못하는 이유는 아래 두 문제가 있다.

 1) 프록시를 적용할 대상이 구현하고 있는 인터페이스를 구현하는 프록시 클래스를 일일이 만들어야 한다는 번거로움

 2) 부가적인 기능이 여러 메소드에 반복적으로 나타나게 돼서 코드의 중복이 발생한다는 문제가 있다.

 

위 데코레이터 패턴의 단점을 극복하는 방법으로 다이내믹 프록시를 이용하는 것이다. 타깃 인터페이스를 일일이 만드는 번거로움을 제거하고, 하나의 핸들러 메소드를 구현하는 것만으로 수많은 메소드에 부가기능을 부여해줄 수 있으니 부가기능 코드의 중복 문제도 사라진다. 그리고 프록시 팩토리 빈을 활용한 DI까지 더해주면 번거로운 다이내믹 프록시 생성 코드도 제거할 수 있다.

 

프록시 팩토리 빈의 한계

 1) 프록시를 통해서 타깃에 부가기능을 제공하는 것은 메소드 단위로 일어나는 일이다. 하나의 클래스 안에 존재하는 여러 개의 메소드에 부가기능을 한 번에 제공하는 일은 어렵지 않게 가능했다. 하지만 여러 클래스에 공통적인 부가기능을 제공하는 일은 지금까지 살펴본 방법으로 불가능하다.

 2) 하나의 타깃에 여러 개의 부가기능을 적용하려고 할 때도 문제다. 적용 대상인 서비스 클래스가 200개쯤 된다면 보통 하나당 3~4줄이면 되는 서비스 빈의 설정에 5~6줄씩되는 설정 코드가 들어간다. 설정 파일이 급격히 복잡해지는 것은 바람직하지 못하다.

 3) TransactionHandler 오브젝트가 프록시 팩토리 빈 개수만큼 만들어 진다는 것이다. TransactionHandler는 타깃 오브젝트를 프로퍼티로 갖고 있다. 따라서 트랜잭션 부가기능을 제공하는 동일한 코드임에도 불구하고 타깃 오브젝트가 달라지면 새로운 TransactionHandler 오브젝트를 만들어야 한다.


[참고자료]

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

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

6장. AOP3  (0) 2020.04.26
6장. AOP2  (1) 2020.04.19
5장. 서비스 추상화  (0) 2020.04.05
4장. 예외  (0) 2020.03.29
3장. 템플릿2  (0) 2020.03.22