포스팅 시작하겠습니다.
6.1 트랜잭션 코드의 분리
지난 챕터에서 우리는 서비스 추상화를 이용해 UserService에서 특정 트랜잭션 기술에 종속적이지 않은 깔끔한 코드를 만들었다.
그러나 여전히 비즈니스 로직을 수행해야만 하는 UserService에 트랜잭션 기술 코드가 들어있다.
이를 해결하기 위해 데코레이터 패턴을 적용했다.
UserService 인터페이스를 만들고 이를 구현한 비즈니스 로직을 수행하는 UserServiceImpl 클래스와 트랜잭션을 수행할 UserServiceTx 구현클래스를 작성했다.
UserServiceTx가 UserServiceImpl을 인스턴스 변수로 받아 트랜잭션 경계 설정 코드를 진행하고 비즈니스 로직 관련 부분은 UserServiceImpl 메서드를 호출하면서 위임한다.
UserService 인터페이스를 도입해 클라이언트와 UserService간의 결합도를 낮췄으므로 아래와 같은 코드가 가능했다.
클라이언트는 비즈니스 로직만 올바르게 수행되면 UserService의 인터페이스에 의존하므로
UserServiceTx의 메서드를 호출하든 UserServiceImpl의 메서드가 호출하든 상관이 없다.
public interface UserService {
void add(User user);
void upgradeLevels();
}
public class UserServiceTx implements UserService {
private final UserService userService; //UserServiceImpl을 인스턴스 변수로 넣는다.
private final PlatformTransactionManager transactionManager;
public UserServiceTx(UserService userService, PlatformTransactionManager transactionManager) {
this.userService = userService;
this.transactionManager = transactionManager;
}
@Override
public void add(User user) {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try{
userService.add(user);
transactionManager.commit(status);
}
catch(Exception e){
transactionManager.rollback(status);
throw e;
}
}
@Override
public void upgradeLevels() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try{
userService.upgradeLevels();
transactionManager.commit(status);
}
catch(Exception e){
transactionManager.rollback(status);
throw e;
}
}
}
public class UserServiceImpl implements UserService {
@Override
public void upgradeLevels(){
//진짜 비즈니스 로직 수행
}
@Override
public void add(User user) {
//진짜 비즈니스 로직 수행
}
}
스프링의 빈 생성코드만 조금 다듬으면 작업은 마무리가 된다.
트랜잭션 경계 설정 코드 분리의 장점
- 첫 번째 장점은 이제 비즈니스 로직을 담당하고 있는 UserServiceImpl의 코드를 작성할 때는 트랜잭션과 같은 기술적인 내용에는 전혀 신경쓰지 않아도 된다.
- 두 번째 장점은 비즈니스 로직에 대한 테스트를 손쉽게 만들어낼 수 있다.
6.2 고립된 단위 테스트
지금 모든 테스트에서는 UserDao가 실제 DB를 의존하고 있다.
우리는 이전 5장에서 실제 메일 서버에 의존하지 않는 목 오브젝트 MockMailSender을 만들어 테스트를 수행했다.
UserDao 목 오브젝트를 만들어서 DB에 의존하지 않고 테스트를 수행할 수 있도록 코드를 변경할 것이다.
UserDao 인터페이스를 구현하는 MockUserDao 클랙스를 만들고 UserServiceImpl에 DI하면 된다.
그러면 이제 UserService 테스트는 완전히 고립돼서 테스트만을 위해 독립적으로 동작하는 테스트 대상을 사용할 것이기 때문에 스프링 컨테이너에서 빈을 가져올 필요가 없다. 또한 DB 연동 시간도 없어졌다.
즉, 테스트 수행 속도가 굉장히 빨라졌다.
고립된 테스트를 만들려면 목 오브젝트 작성과 같은 약간의 수고가 필요하지만 그 보상은 충분히 기대할만하다.
단위 테스트와 통합 테스트
- 단위 테스트: 테스트 대상 클래스를 목 오브젝트 등의 테스트 대역을 이용해 의존 오브젝트나 외부의 리소스를 사용하지 않도록 고립시켜서 테스트하는 것.
- 통합 테스트: 두 개 이상의, 성격이나 계층이 다른 오브젝트가 연동하도록 만들어 테스트하거나, 외부의 DB나 파일, 서비스 등의 리소스가 참여하는 테스트.
테스트 가이드 라인
- 항상 단위 테스트를 먼저 고려한다.
- 하나의 클래스나 성격과 목적이 같은 긴밀한 클래스 몇 개를 모아서 외부와의 의존관계를 모두 차단하고 필요에 따라 스텁이나 목 오브젝트 등의 테스트 대역을 이용하도록 테스트를 만든다.
- 외부 리소스를 사용해야만 가능한 테스트는 통합 테스트로 만든다.
- 단위 테스트로 만들기 어려운 코드는 대표적인 것이 DAO다. DAO 테스트는 외부 리소스를 사용하므로 통합 테스트로 분류된다.
- DAO 테스트가 충분히 검증되면 DAO를 이용하는 코드는 DAO 역할을 스텁이나 목 오브젝트로 대체해서 테스트할 수도 있다.
- 여러 개의 단위가 의존관계를 가지고 동작할 때는 통합 테스트가 필요하다. 다만 단위 테스트를 충분히 거쳤다면 통합 테스트의 부담은 상대적으로 줄어든다.
- 단위 테스트를 만들기가 너무 복잡하다고 판단되는 코드는 처음부터 통합 테스트를 고려한다. 이때도 통합 테스트에 참여하는 코드 중에서 가능한 한 많은 부분을 미리 단위 테스트로 검증하는게 유리하다.
- 스프링 테스트 컨텍스트 프레임워크를 이용하는 테스트는 통합 테스트다.
스프링이 지지하고 권장하는 깔끔하고 유연한 코드를 만들다보면 테스트도 그만큼 만들기 쉬워지고, 테스트는 다시 코드의 품질을 높여주고, 리팩토링과 개선에 대한 용기를 주기도 할 것이다.
목 프레임워크
단위 테스트가 많은 장점이 있고 가장 우선시해야 할 테스트 방법인 건 사실이지만 작성이 번거롭다는 문제점이 있다.
특히 목 오브젝트를 만드는 일이 가장 큰 짐이다.
다행히도 이런 번거로운 목 오브젝트를 편리하게 작성하도록 도와주는 다양한 목 오브젝트 지원 프레임워크가 있다.
Mockito 프레임워크
Mockito 프레임워크는 사용하기도 편리하고, 코드도 직관적다.
Mockito 오브젝트는 다음의 네 단계를 거쳐서 사용한다.
- 인터페이스를 이용해 목 오브젝트를 만든다.
- 목 오브젝트가 리턴할 값이 있으면 이를 지정해준다. 메서드가 호출되면 예외를 강제로 던지게 만들 수 있다.
- 테스트 대상 오브젝트에 DI해서 목 오브젝트가 테스트 중에 사용되도록 만든다.
- 테스트 대상 오브젝트를 사용한 후에 목 오브젝트의 특정 메서드가 호출됐는지, 어떤 값을 가지고 몇 번 호출됐는지를 검증한다.
아래는 Mockito 프레임워크를 사용한 테스트 코드다.
@Test
void mockUpgradeLevels() throws Exception {
UserDao mockUserDao = mock(UserDao.class);
MailSender mockMailSender = mock(MailSender.class);
UserServiceImpl userService = new UserServiceImpl(mockUserDao, new UserLevelUpgradePolicyImpl(mockUserDao, mockMailSender));
when(mockUserDao.getAll()).thenReturn(this.users);
userService.upgradeLevels();
/**
* 목 오브젝트의 기능을 통해 어떤 메서드가 몇 번 호출됐는지, 파라미터가 무엇인지 확인할 수 있다.
*/
verify(mockUserDao, times(2)).update(any(User.class));
verify(mockUserDao, times(2)).update(any(User.class));
verify(mockUserDao).update(users.get(1)); //users.get(1)을 파라미터로 update가 호출된적이 있는지를 확인함.
assertThat(users.get(1).getLevel()).isSameAs(SILVER);
verify(mockUserDao).update(users.get(3));
assertThat(users.get(3).getLevel()).isSameAs(GOLD);
ArgumentCaptor<SimpleMailMessage> mailMessageArg = ArgumentCaptor.forClass(SimpleMailMessage.class);
verify(mockMailSender, times(2)).send(mailMessageArg.capture());
List<SimpleMailMessage> mailMessages = mailMessageArg.getAllValues();
assertThat(mailMessages.get(0).getTo()[0]).isEqualTo(users.get(1).getEmail());
assertThat(mailMessages.get(1).getTo()[0]).isEqualTo(users.get(3).getEmail());
}
6.3 다이내믹 프록시와 팩토리 빈
프록시는 자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 것을 대리자, 대리인과 같은 역할을 한다.
프록시를 통해 최종적으로 요청을 위임받아 처리하는 실제 오브젝트를 타깃 또는 실체라고 부른다.
우리가 위에서 살펴본 UserServiceTx도 일종의 프록시며 UserServiceImpl은 타깃이다.
프록시는 기존 코드에 영향을 주지 않으면서 타깃의 기능을 확장하거나 접근 방법을 제어할 수 있는 유용한 방법이다.
그럼에도 불구하고 많은 개발자는 타깃 코드를 직접 고치지 말지 번거롭게 프록시를 만들지 않겠다고 생각한다.
프록시를 만드는 일이 번거롭기 때문이다. 일일이 새로운 클래스를 정의해야 하고, 인터페이스의 구현해야할 메서드는 많으면 모든 메서드를 일일히 구현해서 위임하는 코드를 넣어야하기 때문이다.
그렇다면 일일이 모든 인터페이스를 구현해서 클래스를 새로 정의하지 않고도 편리하게 만들어서 사용하는 방법은 없을까?
물론 있다. 이제부터 다이내믹 프록시에 대해 알아보자.
다이내믹 프록시 적용
다이내믹 프록시는 프록시 팩토리에 의해 런타임 시 다이내믹하게 만들어지는 오브젝트다.
다이내믹 프록시 오브젝트는 타깃의 인터페이스와 같은 값으로 만들어진다.
클라이언트는 다이내믹 프록시 오브젝트를 타깃 인터페이스를 통해 사용할 수 있다.
이 덕분에 프록시를 만들 때 인터페이스를 모두 구현해가면서 클래스를 정의하는 수고를 덜 수 있다.
프록시 팩토리에게 인터페이스 정보만 제공해주면 해당 인터페이스를 구현한 클래스의 오브젝트를 자동으로 만들어주기 때문이다.
다이내믹 프록시를 이용한 트랜잭션 부가기능
이제 UserServiceTx를 다이내믹 프록시 방식으로 구현한다.
요청할 타깃과 트랜잭션 매니저, 적용할 메서드 패턴을 DI로 제공받아야 한다.
public class TransactionHandler implements InvocationHandler {
private Object target;
private PlatformTransactionManager transactionManager;
private String pattern;
public TransactionHandler(Object target, PlatformTransactionManager transactionManager, String pattern) {
this.target = target;
this.transactionManager = transactionManager;
this.pattern = pattern;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().startsWith(pattern)){
return invokeInTransaction(method, args);
}
return method.invoke(target, args);
}
private Object invokeInTransaction(Method method, Object[] args) throws Throwable {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try{
Object ret = method.invoke(target, args);
transactionManager.commit(status);
return ret;
}
catch(InvocationTargetException e){
transactionManager.rollback(status);
throw e.getTargetException();
}
}
}
아래는 TransactionHandler을 적용한 테스트코드의 일부다.
@Test
void upgradeAllOrNothing() throws Exception{
Transaction txHandler = new TransactionHandler(testUserService, transactionManager, "upgradeLevels");
UserService txUserService = (UserService)Proxy.nextProxyInstance(
getClass().getClassLoader(), new Class[] {UserService.class}, txHandler);
//다른 로직은 생략
}
다이내믹 프록시를 위한 팩토리 빈
이제 TransactionHandler와 다이내믹 프록시를 스프링의 DI를 통해 사용할 수 있도록 만들어야 한다.
문제는 DI의 대상이 되는 다이내믹 프록시 오브젝트는 일반적인 스프링의 빈으로는 등록할 방법이 없다.
사실 스프링은 클래스 정보를 가지고 디폴트 생성자를 통해 오브젝트를 만드는 방법 외에도 빈을 만들 수 있는 여러 가지 방법을 제공한다.
대표적으로 팩토리 빈을 이용한 빈 생성 방법을 들 수 있다.
팩토리 빈이란 스프링을 대신해서 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈을 말한다.
팩토리 빈을 만드는 방법에는 여러 가지가 있는데, 가장 간단한 방법은 스프링의 FactoryBean이라는 인터페이스를 구현하는 것이다.
public interface FactoryBean<T> {
T getObject() throws Exception; //빈 오브젝트를 생성해서 돌려준다.
Class<?> getObjectType(); //생성되는 오브젝트의 타입을 알려준다.
boolean isSingleton(); // getObject()가 돌려주는 오브젝트가 항상 같은 싱글톤 오브젝트인지 알려준다.
}
FactoryBean 인터페이스를 구현한 클래스를 스프링의 빈으로 등록하면 팩토리 빈으로 동작한다.
팩토리 빈을 사용하면 다이내믹 프록시 오브젝트를 스프링의 빈으로 만들어줄 수가 있다.
팩토리 빈의 getObject() 메서드에 다이내믹 프록시 오브젝트를 만들어주는 코드를 넣으면 되기 때문이다.
스프링 빈에는 팩토리 빈과 UserServiceImpl만 빈으로 등록한다.
팩토리 빈은 다이내믹 프록시가 위임할 타깃 오브젝트인 UserServiceImpl에 대한 레퍼런스를 프로퍼티를 통해 DI 받아둬야 한다.
다이내믹 프록시와 함께 생성할 TransactionHandler에게 타깃 오브젝트를 전달해줘야하기 때문이다.
그 외에도 다이내믹 프록시나 TransactionHandler을 만들 때 필요한 정보는 팩토리 빈의 프로퍼티로 설정해뒀다가 다이내믹 프록시를 만들면서 직접 전달해줘야 한다.
public class TxProxyFactoryBean implements FactoryBean<Object> {
private Object target;
private PlatformTransactionManager platformTransactionManager;
private String pattern;
private Class<?> serviceInterface;
public TxProxyFactoryBean(Object target, PlatformTransactionManager platformTransactionManager, String pattern, Class<?> serviceInterface) {
this.target = target;
this.platformTransactionManager = platformTransactionManager;
this.pattern = pattern;
this.serviceInterface = serviceInterface;
}
@Override
public Object getObject() throws Exception {
TransactionHandler transactionHandler = new TransactionHandler(target, platformTransactionManager, pattern);
return Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[]{serviceInterface},
transactionHandler
);
}
@Override
public Class<?> getObjectType() {
return serviceInterface;
}
@Override
public boolean isSingleton() {
return false; //getObject가 매번 같은 객체를 리턴하지 않는다라는 뜻.
}
}
아래는 프록시 팩토리 빈을 적용한 테스트 코드 일부다. & + 빈이름 구조면 빈 팩토리 스프링 빈 객체를 가져온다.
@Test
void upgradeAllOrNothing() throwsException{
TxProxyFactoryBean factoryBean = ac.getBean("&txProxyFactoryBean", TxProxyFactoryBean.class);
factoryBean.setTarget(new UserServiceImpl(userDao, testUserService));
UserService userServiceTx = (UserService)factoryBean.getObject();
//테스트 로직은 생략
}
프록시 팩토리 빈 방식의 장점과 한계
프록시 팩토리 빈의 재사용
- TransactionHandler를 이용하는 다이내믹 프록시를 생성해주는 TxProxyFactoryBean은 코드의 수정 없이도 다양한 클래스에 적용할 수 있다.
- 타깃 오브젝트에 맞는 프로퍼티 정보를 설정해서 빈으로 등록해주기만 하면 된다.
- 하나 이상의 TxProxyFactoryBean을 동시에 빈으로 등록해도 상관없다.
- 팩토리 빈이기 때문에 각 빈의 타입은 타깃 인터페이스와 일치한다.
프록시 팩토리 빈 방식의 장점
- 다이내믹 프록시를 이용하면 타깃 인터페이스를 구현하는클래스를 일일이 만드는 번거로움을 제거할 수 있다.
- 하나의 핸들러 메서드를 구혀하는 것만으로도 수많음 메서드에 부가기능을 부여해줄 수 있으니 부가기능코드의 중복 문제도 사라진다.
- 다이내믹 프록시에 팩토리 빈을 이용한 DI까지 더해주면 번거로운 다이내믹 프록시 생성 코드도 제거할 수 있다.
프록시 팩토리 빈의 한계
- 프록시를 통해 타깃에 부가기능을 제공하는 것은 메서드 단위로 일어난다.
- 하나의 클래스 안에 존재하는 여러 개의 메서드에 부가 기능을 한 번에 제공하는 건 어렵지 않게 가능했다.
- 만약 한 번에 여러 개의 클래스에 공통적인 부가기능을 제공하는 일은 지금까지 살펴본 방법으로는 불가능하다.
- 하나의 타깃에 여러 개의 부가기능을 적용하려고 할 때도 문제다.
- 또 한가지 문제점은 TransactionHandler 오브젝트가 프록시 팩토리 빈 개수만큼 많아진다는 점도 문제다.
6.4 스프링 프록시 팩토리 빈
프록시 팩토리 빈의 한계를 스프링은 매우 세련되고 깔끔한 방식으로 해법을 제공한다.
ProxyFactoryBean
스프링은 트랜잭션 기술과 메일 발송 기술에 적용했던 서비스 추상화를 프록시 기술에도 동일하게 적용하고 있다.
스프링은 일관된 방법으로 프록시를 만들 수 있게 도와주는 추상 레이어를 제공한다.
생성된 프록시는 스프링의 빈으로 등록돼야 한다.
스프링은 프록시 오브젝트를 생성해주는 기술을 추상화한 팩토리 빈을 제공해준다.
스프링의 ProxyFactoryBean은 프록시를 생성해서 빈 오브젝트를 등록하게 해주는 팩토리 빈이다.
ProxyFactoryBean이 생성해서 프록시에서 사용할 부가기능은 MethodInterceptor 인터페이스를 구현해서 만든다.
MethodInterceptor은 invoke() 메서드에서 ProxyFactoryBean으로부터 타깃 오브젝트에 대한 정보까지 함께 제공받는다.
따라서 MethodInterceptor은 타깃 오브젝트에 상관없이 독립적으로 만들어질 수 있으며, 타깃이 다른 여러 프록시에서 함께 사용할 수 있고 싱글톤 빈으로 등록 가능하다.
스프링 프록시 팩토리 빈의 학습 테스트 예제다.
public class DynamicProxyTest {
@Test
void proxyFactoryBean(){
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
proxyFactoryBean.setTarget(new HelloTarget());
proxyFactoryBean.addAdvice(new UppercaseAdvise());
Hello hello = (Hello)proxyFactoryBean.getObject();
assertThat(hello.sayHello("Toby")).isEqualTo("HELLO TOBY");
assertThat(hello.sayHi("Toby")).isEqualTo("HI TOBY");
assertThat(hello.sayThankYou("Toby")).isEqualTo("THANK YOU!! TOBY");
}
static class UppercaseAdvise implements MethodInterceptor{
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
String result = (String) invocation.proceed();
return result.toUpperCase(Locale.ROOT);
}
}
interface Hello{
String sayHello(String name);
String sayHi(String name);
String sayThankYou(String name);
}
static class HelloTarget implements Hello{
@Override
public String sayHello(String name) {
return "Hello " + name;
}
@Override
public String sayHi(String name) {
return "Hi " + name;
}
@Override
public String sayThankYou(String name) {
return "Thank you!! " + name;
}
}
}
MethodInterceptor처럼 타깃 오브젝트에 적용하는 부가 기능을 담는 오브젝트를 스프링에서는 어드바이스라고 부른다.
스프링은 메서드 선정 알고리즘을 담은 오브젝트를 포인트컷이라고 부른다.
어드바이스와 포인트컷은 모두 프록시에 DI로 주입돼서 사용된다.
아래는 포인트컷을 주입한 학습 테스트다.
@Test
void pointcutAdvisor(){
ProxyFactoryBean pf = new ProxyFactoryBean();
pf.setTarget(new HelloTarget());
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedName("sayH*");
pf.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvise()));
Hello proxyHello = (Hello)pf.getObject();
assertThat(proxyHello.sayHello("Toby")).isEqualTo("HELLO TOBY");
assertThat(proxyHello.sayHi("Toby")).isEqualTo("HI TOBY");
assertThat(proxyHello.sayThankYou("Toby")).isEqualTo("Thank you!! Toby");
}
어드바이저 = 포인트컷(메서드 선정 알고리즘) + 어드바이스(부가기능)
위에서 학습한 내용을 바탕으로 ProxyFactoryBean과 TransactionAdvice를 적용해서 실습을 진행했다.
어드바이스와 포인트 컷의 재사용
ProxyFactoryBean은 스프링의 DI와 템플릿/콜백 패턴, 서비스 추상화 등의 기법이 모두 적용된 것이다.
그 덕분에 독립적이며, 여러 프록시가 공유할 수 있는 어드바이스와 포인트컷으로 확장 기능을 분리할 수 있었따.
이제 UserService외에 새로운 비즈니스 로직을 담은 서비스 클래스가 만들어져도 이미 만들어둔 TransactionAdvice를 그대로 재사용할 수 있다.
6.5 스프링 AOP
아직 한 가지 해결할 문제가 남아 있다.
부가기능의 적용이 필요한 타깃 오브젝트마다 거의 비슷한 내용의 ProxyFactoryBean 빈 설정정보를 추가해야 한다.
새로운 타깃이 등장했다고 해서 코드를 손댈 필요는 없어졌지만, 설정은 매번 복사해서 붙이고 target 프로퍼티의 내용을 수정해줘야 한다.
이러한 중복을 더 이상 제거할 방법이 없을까?
빈 후처리기를 이용한 자동 프록시 생성기
스프링은 컨테이너로서 제공하는 기능 중에서 변하지 않는 핵심적인 부분 외에는 대부분 확장할 수 있도록 확장 포인트를 제공해준다.
그중에서 관심을 가질 만한 확장 포인트는 바로 BeanPostProcessor 인터페이스를 구현해서 만드는 빈 후처리기다.
빈 후처리기는 이름 그대로 스프링 빈 오브젝트로 만들어지고 난 후에, 빈 오브젝트를 다시 가공할 수 있게 해준다.
실습에서는 스프링이 제공하는 빈 후처리기 중의 하나인 DefaultAdvisorAutoProxyCreator를 살펴보았다.
이름을보면 알 수 있듯이 어드바이저를 이용한 자동 프록시 생성기다.
빈 후처리기를 스프링에 적용하는 방법은 간단한데, 빈 후처리기 자체를 빈으로 등록하면 된다.
스프링은 빈 후처리기가 빈으로 등록되어 있으면 빈 오브젝트가 생성될 때마다 빈 후처리기에 보내서 후처리 작업을 요청한다.
빈 후처리기는 빈 오브젝트의 파라미터를 강제로 수정할 수도 있고 별도의 초기화 작업을 진행할 수도 있다.
즉 스프링이 설정을 참고해서 만든 오브젝트가 아닌 다른 오브젝트를 빈으로 등록시키는 것이 가능하다.
이를 잘 이용하면 스프링이 생성하는 빈 오브젝트의 일부를 프록시로 포장하고, 프록시를 빈으로 대신 등록할 수도 있다.
바로 이것이 자동 프록시 생성 빈 후처리기다.
빈 후처리기를 사용한 프록시 자동 생성 로직
- DefaultAdvisorAutoProxyCreator를 빈으로 등록하면 스프링은 빈 오브젝트를 만들 때마다 후처리기에 빈을 보낸다.
- DefaultAdvisorAutoProxyCreator는 빈으로 등록된 모든 어드바이저 내의 포인트컷을 이용해 전달받은 빈이 프록시 적용 대상인지 확인한다.
- 프록시 적용 대상이면 그때는 내장된 프록시 생성기에게 현재 빈에 대한 프록시를 만들게 하고, 만들어진 프록시에 어드바이저를 연결해준다.
- 빈 후처리기는 프록시가 생성되면 원래 컨테이너가 전달해준 빈 오브젝트 대신 프록시 오브젝트를 컨테이너에게 돌려준다.
- 컨테이너는 최종적으로 빈 후처리기가 돌려준 오브젝트를 빈으로 등록하고사용한다.
우리의 코드에 DefaultAdvisorAutoProxyCreator를 빈으로 등록해 ProxyFactoryBean 빈 설정정보를 매번 추가하는 문제를 해결했다.
적용하고자 하는 어드바이저는 스프링 빈으로 등록해야 한다.
그러면 어드바이저를 이용하는 DefaultAdvisorAutoProxyCreator에에 의해 자동 수집되고, 프록시 대상 선정 과정에 참여하며,
자동 생성된 프록시에 다이내믹하게 DI 돼서 동작하는 어드바이저가 된다.
우리는 이제 비즈니스 로직을 담은 UserService에 정말 깔끔하게 트랜잭션을 적용했다.
AOP란 무엇인가?
UserService에 트랜잭션을 적용해 온 과정은 다음과 같다.
- 트랜잭션 서비스 추상화
- 프록시와 데코레이터 패턴
- 다이내믹 프록시와 프록시 팩토리 빈
- 자동 프록시 생성 방법과 포인트 컷
결국 지금까지 해온 모든 작업은 핵심기능에 부여되는 부가 기능을 효과적으로 모듈화하는 방법을 찾는 것이였고,
어드바이스와 포인트 컷을 결합한 어드바이저가 단순하지만 이런 특성을 가진 모듈의 원시적인 형태로 만들어지게 됐다.
전통적인 객체지향 기술의 설계 방법으로는 독립적인 모듈화가 불가능한 트랜잭션 경계설정과 같은 부가 기능을 어떻게 모듈화할 것인가를 연구해온 사람들은, 이 부가기능 모듈화 작업은 기존의 객체지향 설계 패러다임과는 구분되는 새로운 특성이 있다고 생각했다.
그래서 이런 부가기능 모듈을 객체지향 기술에서 주로 사용하는 오브젝트와는 다르게 특별한 이름으로 부르기 시작했다.
그것을 바로 애스펙트라고 한다.
이렇게 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스펙트라는 독특한 모듈로 만들어서 설계하고 개발하는 방법을
애스펙트 지향 프로그래밍(Aspect Orented Programming)또는 AOP라고 부른다.
AOP는 결국 애플리케이션을 다양한 측면에서 독립적으로 모델링하고, 설계하고, 개발할 수 있도록 만들어주는 것이다.
아래에서 스프링 AOP에 대한 더 다양한 내용을 확인하실 수 있습니다.
https://devdebin.tistory.com/223
https://devdebin.tistory.com/221
https://devdebin.tistory.com/222
참고 자료
토비의 스프링 3.1 Vol. 1 (6장 AOP)
댓글