※ 해당 내용은 토비의 스프링3을 보고 정리한 내용이다.
6.0
AOP는 Ioc/DI, 서비스 추상화와 같이 스프링 3대 기반 기술중 하나이다. 스프링에서 AOP가 도입된 이유, 장점 등에 대한 이해가 필요하다.
6.1. 트랜잭션 코드의 분리
현재까지 서비스 추상화를 통해서 UserService에 트랜잭션 기술에 종속적이지 않은 코드를 작성했지만, 트랜잭션 경계설정을 하는 부분과 비즈니스 로직과 아직 분리되지 않았기 때문에, 메서드를 이해하는데 있어서 복잡도가 있다.
트랜잭션 경계설정 코드와 비즈니스 로직 코드는 서로 주고받는 데이터가 없고 구조상 완벽하게 분리되어 있지만 결국 트랜잭션의 경계설정은 특정 비즈니스 로직의 전후에 처리되어야 하므로, 이를 설정하는 코드가 비즈니스로직과 공존할 수 밖에 없다. 이 같은 상황을 타개하고 이 두 부분의 코드를 어떻게 분리할 수 있을까?
메서드 분리
결합되어 있지 않은 두 부분 중에서 비즈니스로직을 따로 메서드로 분리해서 관리해도 문제가 없다. 하지만 이렇게 분리한다고 해도 결국 트랜잭션 관리에 대한 코드가 UserService내에 있으므로 분리 이전과 큰 차이가 없다.
DI를 이용한 클래스 분리
현재 구조에서는 UserService 자체가 구체클래스이고 이 클래스를 참조하는 영역에서는 직접적으로 해당 클래스를 호출하기 때문에 클래스를 분리해서 트랜잭션관련 처리를 보도록 할 수 없는 구조이다.
이를 대응하기 위해서 UserService를 인터페이스로 변환하고 이를 구현하는 구체클래스를 생성한다.
해당 구조에서 UserServiceTx는 비즈니스 로직을 처리하지는 않고 UserServiceImple에 위임한다. 다만 전후로 트랜잭션 관련 처리를 수행한다.
위와 같이 UserSertiveTx 내부에 UserService 인터페이스에 대한 멤버변수를 두고 이를 통해서 실제 비즈니스로직을 가지고 있는 UserServiceImpl의 메서드를 호출 할 수 있다.
다만 실제 트랜잭션 처리 적용을 하려면 DI설정 변경이 필요하다. 따라서 UserService 의 대표 빈을 UserServiceTx로 설정하고 UserServiceTx내의 userService에는 UserServiceImpl을 주입받도록 설정해야한다.
이렇게 트랜잭션 로직과 비즈니스 로직을 분리함으로써, 비즈니스 로직 작성시에 불필요한 코드를 추가할 필요가 없어졌다. 또한 책임이 다른 부분의 클래스 분리를 통해서 실제 비즈니스 로직에 대한 테스트 작성이 용이해졌다.
6.2 고립된 단위 테스트
단위테스트를 작성하기 가장 좋은 방법은 테스트 대상을 가장 작은 단위로 쪼개서 테스트하는 것이다. 여러 의존성을 가지는 로직은 동작하는 테스트를 만들고 이를 실행하는 것이 어려워지고 테스트 실패 시, 원인을 찾는 것도 어렵다.
현재 UserService에는 단순 로직밖에 없지만 테스트 코드를 작성하려면 DB, MailSender, TxManager 와 같은 의존성을 주입해줘야한다. 여기서 끝이 아니라 특정 특정 DB의 경우, DB드라이버, 네트워크 통신 등 내부에 추가로 가지는 의존성이 많다. 이러한 부차적인 코드들 때문에 테스트를 작성하기 어려워진다.
테스트 대상 오브젝트 고립시키기
위의 원인들 때문에 테스트 대상은 환경, 외부 서버, 다른 클래스 코드 등에 영향을 받지 않도록 고립시킬 필요가 있다. 예시로 테스트 대역을 사용할 수 있다. TxManager는 이전 단계에서 클래스 분리를 통해서 의존성을 제거했었고 DAO나 MailSender 같은 경우는 Mock객체를 사용할 수 있다.
테스트 대역 오브젝트를 이용하기 이전에는 스프링 컨테이너에서 가져온 UserService 빈을 이용해 테스트 코드를 실행했지만, 이는 외부 환경에 많은 의존성을 가지고 있으므로 테스트를 용이하게 하기 위해서 Mock객체를 사용해서 의존성을 제거한 테스트 대상을 수기로 직접 생성해서 사용한다.
이렇게 외부 의존성을 제거함으로써 테스트는 간결해지고 빨라진다.
6.3 다이나믹 프록시와 팩토리 빈
프록시와 프록시 패턴, 데코레이터 패턴
이전 트랜잭션 경계설정 코드와 비즈니스 로직을 분리했을때 프록시 패턴을 이용해서 아래와 같은 구조를 사용했었다.
이러한 구조를 통해서 핵심기능을 가지는 타겟 접근 이전에 프록시 객체에서 부가적인 기능을 처리할 수 있고, 클라이언트의 타겟 접근방법을 제어할 수 있다.
데코레이터 패턴은 타겟의 부가적인 기능을 런타임시에 다이나믹하게 부여하기 위해서 프록시를 사용하는 패턴을 말하고 여기서 데코레이터 객체는 N개를 가질 수 있다. 자바 IO패키지의 InputStream, OutputStream이 데코레이터 패턴 사용의 대표적 예로 볼 수 있다.
아래의 코드는 타겟(FileInputStream)에 버퍼 읽기 기능을 제공해주는 BufferedInputStream 데코레이터를 적용한 예시이다.
new BufferedInputStream(new FileInputStream("a.txt"));
이전에 작성했던 예시 기준으로, UserServiceImpl에 트랜잭션 관련 데코레이터 UserServiceTx를 적용시킨 구조로도 이를 볼 수 있다. 스프링 DI를 이용하면 인터페이스를 통해서 런타임에 적절한 데코레이터를 붙이는 작업이 간편해진다. 이전에도 봤듯이, 데코레이터 빈의 프로퍼티로 같은 인터페이스를 구현한 데코레이터, 혹은 타겟을 빈 설정 하면 된다.
이후부터는 타겟과 같은 인터페이스를 구현해두고 위임하는 방식을 취하는 모든 객체를 프록시라고 지칭하되, 사용의 목적이 기능의 부가(데코레이터)인지, 접근 제어(프록시)인지를 구분이 가능하다.
다이나믹 프록시
프록시는 기존 코드에 영향을 주지 않으면서 타겟의 기능을 확장하거나 접근방법을 제어할 수 있지만, 구조의 복잡성이 늘어난다는 단점을 가지고 있다. 이를 해결하기 위해서 자바에서는 reflect 패키지 안에 이를 손쉽게 해결할 수 있도록 지원해주는 클래스를 제공한다.
프록시는 부가기능 수행, 타겟으로 위임 두 가지 기능으로 구성된다. 이러한 구성을 지키기 위해서 프록시는 다음과 같은 단점이 생긴다.
- 타겟의 인터페이스를 구현하고 위임하는 코드 작성이 번거로움 (부가기능을 필요로 하지 않는 메서드도 구현해서 타겟 위임 코드를 모두 생성해줘야함)
- 부가기능 코드가 중복될 가능성이 많음
이러한 단점을 다이나믹 프록시를 추가함으로써 해결할 수 있다.
// InvocationHandler
public Object invoke(Object proxy, Method method, Object[] args);
다이나믹 프록시는 런타임 시점에 프록시 팩토리를 통해서 만들어지고, 클라이언트의 모든 요청을 리플렉션 정보로 변환해서 InvocationHandler의 invoke() 메서드로 넘긴다. 이때 핸들러가 타겟의 오브젝트 레퍼런스를 가지고 있다면 리플렉션을 통해서 타겟 메서드에 처리를 위임할 수 있다. 즉 인터페이스의 모든 메서드를 구현할 필요 없이 InvocationHandler.invoke() 메서드 하나를 통해서 처리할 수 있게 된다. 또한 InvocationHandler 방식을 차용하면 리플렉션을 사용해서 invoke 호출하기 때문에 타겟의 종류에 상관없이 적용이 가능하다.
아래와 같이 다이나믹 프록시를 생성하고 이때 넘겨주는 생성자에서 InvocationHandler 구현 클래스를 같이 넘겨준다.
다이나믹 프록시를 이용한 트랜잭션 부가 기능
현재 작업된 UserServiceTx는 UserService 인터페이스 메서드를 모두 구현해야하고 트랜잭션이 필요한 메서드마다 처리코드 중복이 발생하는 비효율적인 구조이다. 이런 경우, 트랜잭션 부가기능은 다이나믹 프록시를 만들어서 적용시킴으로써 개선할 수 있다.
InvocationHandler를 구현하는 TransactionHandler를 만들고 구현 내부에 아래와 같이 invoke메서드와 트랜잭션 처리를 구현한다. 요청을 위임할 타겟 객체는 DI로 받고, Object 로 선언함으로써 어떤 타겟도 처리할 수 있고 따로 pattern변수를 만듦으로써 특정 메서드명 (ex. insert, update)을 가지는 메서드 수행 시 트랜잭션을 적용시킬 수 있다.
다이나믹 프록시를 위한 팩토리 빈
이전까지 TransactionHandler와 다이나믹 프록시를 사용해서 트랜잭션 처리를 할 수 있도록 개선했지만 추가로 스프링의 DI를 통해서 해당 객체를 받아서 사용할 수 있도록 해야한다.
하지만 다이나믹 프록시 객체의 경우 name, property로 구성되는 스프링빈으로 등록이 불가하다. 다이나믹 프록시의 경우, Proxy 클래스의 newProxyInstance()
스태틱 팩토리 메서드를 통해서만 생성이 가능하다.
팩토리 빈은 스프링을 대신해서 객체 생성로직을 담당하는 빈으로 FactoryBean 인터페이스를 구현해서 사용할 수 있다. FactoryBean의 getObject() 메서드에서 반환받고자 하는 빈 객체를 리턴하도록 구현하고, 스프링 빈 설정에서 해당 팩토리 빈 구현체를 등록하면 된다. 즉 다이나믹 프록시 객체의 경우, 이를 생성해주는 팩토리 빈을 구현해서 관리하면 된다.
위 구조에서 TransactionHandler를 생성하기 위한 팩토리 빈의 property에 실제 타겟 객체인 UserServiceImpl을 DI받을 수 있도록 설정해야한다.
프록시 팩토리 빈 방식의 장점과 한계
직전까지의 작업은 장점도 있지만 한계점도 존재한다.
- 장점
- 부가기능을 가진 프록시를 생성하는 팩토리빈을 한번 만들어두면 타겟의 타입에 상관없이 재사용할 수 있다.
- 프록시를 적용할 대상이 구현하고 있는 인터페이스를 모두 구현하지 않아도 된다.
- 부가기능 코드 중복 문제가 해결된다
- 한계
- 프록시를 통해 타겟에 부가기능을 제공하는건 메서드 단위로 발생하기 때문에, 한번에 여러개의 클래스에 공통적인 부가기능을 제공할 수 없다.
- 하나의 타겟에 여러개의 부가기능을 제공하고자 할 때, 팩토리빈 설정파일에 부가기능 개수 만큼 XML 설정 코드가 증가한다.
- TransactionHandler 객체가 프록시 팩토리 빈 개수만큼 생성된다. 동일 부가기능을 제공하지만 타겟오브젝트가 변경되면 새로운 객체 생성이 불가피하다.
6.4 스프링의 프록시 팩토리 빈
직전 장에서는 다이나믹 프록시를 사용하기 위한 팩토리 빈 방식의 한계를 확인할 수 있었고 이에 대한 개선점을 스프링을 통해서 제공받을 수 있다.
ProxyFactoryBean
스프링은 서비스 추상화를 프록시 기술에도 적용하고 있기 때문에, 일관된 방법으로 프록시를 만들 수 있도록 해주는 추상 레이어(추상화한 팩토리 빈)를 제공한다.
스프링 ProxyFactoryBean은 프록시를 생성해서 빈으로 등록하게 해주는 팩토리 빈이다. 직전에 직접 생성했던 프록시 팩토리 빈과 다른점은 ProxyFactoryBean에서는 프록시를 생성하는 작업만 담당하고, 실제 프록시를 통해서 제공해주는 부가기능은 MethodInterceptor를 구현한 별도의 빈에 둔다.
MethodInterceptor는 직전 장의 InvocationHandler와 비슷하지만 타겟 객체에 대한 정보를 ProxyFactoryBean을 통해서 같이 전달받기 때문에 타겟 객체 정보에 상관업싱 독립적으로 만들 수 있다. 따라서 MethodInterceptor는 타겟이 다른 여러 프록시에서 같이 사용할 수 있고 싱글톤 빈으로 등록이 가능하다.
아래 코드는 직전에 봤던 JDK 다이나믹 프록시 생성과 ProxyFactoryBean을 통한 프록시 생성을 구분한 것이다.
//ASIS JDK 다이나믹 프록시 생성
Target proxiedTarget = (Target)Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] { Target.class},
new Advice1(new Target()));
public Object invoke(Object proxy, Method method, Object[] args){
String ret = (String)method.invoke(target, args); // target객체를 알고 있어야함
return ret.doAddtFunction(); // 부가기능 처리
}
//TOBE 스프링 ProxyFactoryBean 사용
ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(new Target()); // 타겟 설정
pfBean.addAdvice(new Advice1()); // 부가기능을 담은 어드바이스 추가
pfBean.addAdvice(new Advice2());
Target proxiedTarget = (Target) pfBean.getObject();
static class Advice1 implements MethodInterceptor{ // 부가기능 어드바이스 객체
public Object invoke(MethodInvocation invocation){ // target객체를 몰라도 됌
String ret = (String)invocation.proceed();
return ret.doAddtFunction();
}
}
- 어드바이스는 타겟의 정보를 알 필요없는 순수한 부가기능으로 이해하면 된다. 위의 코드에서 MethodInvocation은 타겟 오브젝트의 메서드를 실행할 수 있는 기능이 있기 때문에, Advice1 객체는 실제 처리해주고자하는 부가기능만을 구현할 수 있다.
- 포인트컷을 처리하기 위해서 MethodInterceptor에는 순수한 어드바이스만 넣어두고 이를 적용할 메서드를 선택하는 기능은 전략패턴을 통해서 구현한다.
- 포인트컷은 부가기능의 적용대상 메서드를 선정하는 방법이다. 기존 MethodInterceptor는 타겟 객체에 대한 정보를 가지고 있을 필요가 없기 때문에 여러 프록시가 공유해서 사용할 수 있으므로 여기서 메서드명을 pattern을 통해서 식별해서 사용하는 방식은 불가하다.
- 스프링의 ProxyFactoryBean는 OCP 원칙을 지키도록 아래 구조의 다이나믹 프록시를 제공한다. 어드바이스와 포인트컷 모두 프록시에 DI로 주입되어서 사용된다.
다이나믹 프록시 생성 후, 프록시는 클라이언트로부터 요청을 받으면 포인트컷에 부가기능을 부여해야하는 메서드인지 체크한다. 이후 선정된 메서드만 어드바이스를 호출한다. 어드바이스는 타겟 객체정보를 들고 있지 않고, 프록시로부터 전달받은 MethodInterceptor 타입의 콜백 객체의 proceed() 메서드를 호출함으로써 타겟 객체 메서드 호출이 가능하다.
위 구조는 재사용 가능한 기능을 만들어두고 바뀌는 부분만 외부에서 주입해서 이를 사용하는 전형적인 템플릿/콜백 구조로 볼 수 있다.
ProxyFactoryBean 적용
이전에 JDK 다이나믹 프록시를 이용해서 구현한 TxProxyFactoryBean을 스프링이 제공하는 ProxyFactoryBean으로 대체할 수 있다.
- 어드바이스 생성
부가 기능을 담당하는 TransactionAdvice를 MethodInterceptor 인터페이스를 구현해서 만든다. 타겟 객체에 대한 정보를 알 필요가 없고 MethodInvocation의 proceed()를 통해서 타겟 객체 메서드 호출이 가능하다.
2. 스프링 XML 설정파일
- TransactionManager에 해당 어드바이스를 DI하도록 수정해준다.
- 포인트컷 빈을 등록한다.
- a,b에서의 어드바이스와 포인트컷을 담을 어드바이저 빈을 등록한다.
- 최종적으로 ProxyFactoryBean설정에 어드바이저를 property로 등록한다.
ProxyFactoryBean을 이용하도록 개선하면서 어드바이스와 포인트컷의 재사용이 가능해지고 아래의 확장 가능한 구조를 얻을 수 있었다.
'Spring' 카테고리의 다른 글
[토비 스프링3] 6. AOP (2) (0) | 2023.03.04 |
---|---|
[토비 스프링3] 5. 서비스 추상화 (0) | 2023.02.13 |
[토비 스프링3] 4. 예외 (0) | 2023.01.23 |
[토비 스프링 3] 3. 템플릿 (0) | 2023.01.08 |
[토비 스프링 3] 2. 테스트 (0) | 2020.06.09 |