[토비의 스프링] 6장 AOP (1)

6장은 양이 많아서 두번에 나눠서 정리!

프록시, 데코레이터 

0. 이전코드

트랜잭션코드를 분리하고 싶은 욕구가 든다.

public class UserService {
    public void upgradeLevels() {
        TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            List<User> users = userDao.getAll();
            for (User user : users) {
                if (canUpgradeLevel(user)) {
                    upgradeLevel(user);
                }
            }
            this.transactionManager.commit(status);
        } catch (RuntimeException e) {
            this.transactionManager.rollback(status);
            throw e;
        }
    }
}

1. 프록시와 DI를 이용한 트랜잭션 코드 분리

프록시패턴 - 프록시가 타겟의 접근방법을 제어하는 용도

데코레이터패턴 - 프록시가 타겟에 부가기능을 추가하는 용도

public class UserServiceTx implements UserService {
	UserService userService; // 타깃
	PlatformTransactionManager transactionManager; // 부가기능

	...

	public void add(User user) {
		this.userService.add(user); // 위임
	}

	public void upgradeLevels() {
		TransactionStatus status = this.transactionManager
				.getTransaction(new DefaultTransactionDefinition());
		try {

			userService.upgradeLevels();

			this.transactionManager.commit(status);
		} catch (RuntimeException e) {
			this.transactionManager.rollback(status);
			throw e;
		}
	}
}

 

UserServiceImpl 을 타깃으로넣고 트랜잭션 부가기능을 추가하고 컨텍스트에서 UserService를 가져오면 UserServiceTx를 가져오게끔해서 트랜잭션 코드 분리를 성공했다.

 

이것은 코틀린에서 위임패턴을 지원해주는 꿀팁 ( 필요한 메서드만 재정의하고 나머지는 구현을 target에게 위임한다 )

 

하지만 아쉬운점이 있다.

1. 위임 코드 작성하기 귀찮음

매번 프록시클래스를 작성해줘야한다. 여러개의 클래스에 같은 기능을 주고 싶으면 AAAServiceTx, BBBServiceTx... 계속 추가해야한다.

코틀린 Tip
class UserServiceTx(
    private val target: UserService,
) : UserService by target {
    override fun upgradeLevels() {
        // transaction code..
        target.upgradeLevels()
    }
}

 

2. 같은 기능을 여러 메서드에 하려면 코드중복이 발생함

가령 UserService의 add 에도 트랜잭션을 추가해야한다면 proxy 클래스에서 메서드마다 작성해줘야햔다.

2. 리플레션 이용한 다이내믹 프록시 적용

스프링엔 리플렉션을 사용해 프록시를 쉽게 만들어주는 방법이 있다.

public class TransactionHandler implements InvocationHandler {
	Object target; // 타깃
	PlatformTransactionManager transactionManager;
	String pattern; // 부가기능 발동 조건

	...

	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		if (method.getName().startsWith(pattern)) {
			return invokeInTransaction(method, args);
		} else {
			return method.invoke(target, args);
		}
	}

	private Object invokeInTransaction(Method method, Object[] args)
			throws Throwable {
		TransactionStatus status = this.transactionManager
				.getTransaction(new DefaultTransactionDefinition());
		try {
			Object ret = method.invoke(target, args);
			this.transactionManager.commit(status);
			return ret;
		} catch (InvocationTargetException e) {
			this.transactionManager.rollback(status);
			throw e.getTargetException();
		}
	}
}


// 만약 생성한다면
TransactionHandler txHandler = new TransactionHandler();
txHandler.set(new TransactionStatus())
txHandler.setTarget(new UserServiceImpl())
...

UserService txUserService = (UserService) Proxy.newInstance(
	getClass().getClassLoader(),
    new Class[] { UserService.class },
    new TransactionHandler()
)

 

부가기능을 InvocationHandler에 구현하고 그것을 인자로 넘겨서 프록시 객체를 만들어 낼 수 있다. 이렇게 해서 모든 메서드가 invoke 를 타게되면서 메서드에 같은 부가기능을 구현할때 중복 코드를 만들지 않을 수 있게 되었다.

 

하지만 또 아쉬운점이 있다.

생성방식이 복잡해서 쉽게 애플리케이션 컨텍스트에 등록할수가 없음 => 자유롭게 DI 해서 쓸수가 없다.

 

3. 팩토리 빈 사용

위의 문제점을 해결하기 위해서 스프링에선 팩토리패턴을 사용해서 복잡한 객체를 생성하고 Bean으로 등록해줄수 있는 FactoryBean 이라는 인터페이스를 제공한다.

public class TxProxyFactoryBean implements FactoryBean<Object> {
	Object target;
	PlatformTransactionManager transactionManager;
	String pattern;
	Class<?> serviceInterface;
	
	// setters
    ...

	// 호출시 새로 프록시를 만든다
	public Object getObject() throws Exception {
		TransactionHandler txHandler = new TransactionHandler();
		txHandler.setTarget(target);
		txHandler.setTransactionManager(transactionManager);
		txHandler.setPattern(pattern);
		return Proxy.newProxyInstance(
			getClass().getClassLoader(),new Class[] { serviceInterface }, txHandler);
	}

	public Class<?> getObjectType() {
		return serviceInterface;
	}
    
    // getObject는 매번 새로운 객체를 만들어준다는 뜻
	public boolean isSingleton() {
		return false;
	}
}

 

이렇게 FactoryBean 으로 감싸서 애플리케이션 컨텍스트에 등록해줄수 있다.

 

많이 좋아졌지만 또 문제점이 있다.

1. 여러개의 부가기능을 주고싶으면 더 복잡해진다

<!-- 트랜잭션이 필요한 서비스마다 이 코드가 필요 -->
<bean id="userService" class="springbook.user.service.TxProxyFactoryBean">
    <property name="target" ref="userServiceImpl" />
    <!-- 여러 개의 프로퍼티 설정이 서비스 마다 반복 -->
    <property name="transactionManager" ref="transactionManager" />
    <property name="performanceCheck" ref="transactionManager" />
    <property name="securityManager" ref="transactionManager" />
    ...
    <property name="pattern" value="upgradeLevels" />
    <property name="serviceInterface" value="springbook.user.service.UserService" />
</bean>

 

 

4. ProxyFactoryBean

스프링은 프록시를 빈에 등록하는 방법을 추상화한 방법을 제공한다. 어드바이스와 포인트컷(옵션)을 만들어서 ProxyFactoryBean에 넘겨주면 된다.

public class TransactionAdvice implements MethodInterceptor {
	PlatformTransactionManager transactionManager;

	public void setTransactionManager(PlatformTransactionManager transactionManager) {
		this.transactionManager = transactionManager;
	}

	public Object invoke(MethodInvocation invocation) throws Throwable {
		TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
		try {
			Object ret = invocation.proceed();
			this.transactionManager.commit(status);
			return ret;
		} catch (RuntimeException e) {
			this.transactionManager.rollback(status);
			throw e;
		}
	}
}

 

이전과 다르게  타깃의 대한 정보가 MethodInvocation 을 통해 추상화 되어 타깃을 몰라도 됨으로 코드 공유가 가능해진다.

 

어드바이스

타깃에 종속적이지 않은 부가기능 구현 방법이다. 타겟에 종속적이지 않기 때문에 코드를 공유할수 있다.

MethodHandler 가 템플릿 MethodInvocation이 콜백

 

포인트컷

어떤 메서드에 어드바이스를 적용할지 결정하는 방법

 

<!-- 공유가능 -->

<bean id="transactionAdvice" class="springbook.user.service.TransactionAdvice">
    <property name="transactionManager" ref="transactionManager" />
</bean>

<bean id="transactionPointcut" class="org.springframework.aop.support.NameMatchMethodPointcut">
    <property name="mappedName" value="upgrade*" />
</bean>

<bean id="transactionAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
    <property name="advice" ref="transactionAdvice" />
    <property name="pointcut" ref="transactionPointcut" />
</bean>

<!-- 서비스 -->

<bean id="userService" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target" ref="userServiceImpl" />
    <property name="interceptorNames">
        <list>
            <value>transactionAdvisor</value>
        </list>
    </property>
</bean>

 

이전에는  <property name="transactionManager" ref="transactionManager" /> 같은 부분이 서비스 마다 반복되었다면 지금은 서비스마다 advisor 만 추가해주면 되서 중복을 많이 줄일수있다.

 

6.5장부터 다음 글에 계속!