5 minute read

트랜잭션 코드의 문제

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class EmployeeService {
    
    public void resetLevels() {

        TransactionStatus status = this.transactionManager
        	.getTransaction(new DefaultTransactionDefinition());

        try {
        	List<User> users = userDao.getAlll();

        	for(User user : users)
        		resetLevel(user)

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

트랜잭션은 비즈니스로직을 수행할때 ACID의 보장을 위해 사용된다. 하지만 비즈니스 로직을 수행하는 코드를 작성할때마다 트랜잭션 코드가 거의 유사한 형태임에도 불구하고 를 재작성해야 하는 번거러움이 있다. 또한 비즈니스로직은 자바/코틀린 기반의 순수한 코드인경우가 많지만 트랜잭션 코드는 기술스택에 의존적이다.

현재 위코드의 경우 쉽게 변하지 않는 비즈니스 코드가 쉽게 변할수 있는 트랜잭션 코드에 의존하고 있으므로 의존관계 역전원칙을 위배하고 있으며, 트랜잭션 코드가 변경되면 코드가 수정되므로 개방폐쇄원칙 또한 위배하고 있다. 또한 코드의 수정사유가 트랜잭션 로직변경, 비즈니스로직 수정 두가지 이므로 단일책임원칙또한 위배하고 있다.

이 트랜잭션코드를 클래스 밖으로 빼보자

DI를 통해 트랜잭션 분리

가정 먼저 EmployeeService의 인터페이스를 분리했다.

import java.sql.SQLException;

public interface EmployeeService {
    void resetLevels()
}

그다음 분리된 인터페이스를 이용하여 트랜잭션을 처리하기 위한 구현체, 비즈니스를 처리하기 위한 구현체 총 2개의 클래스를 코딩한다. 클라이언트가 먼저 트랜잭션구현체에 요청을 보내고 트랜잭션구현체에서 비즈니스 구현체에 요청을 보내도록 의존성 주입을 해주도록 한다. 이를 통해 트랜잭션 코드를 비즈니스로부터 분리할수 있다.

public class EmployeeServiceTransactionImpl implements EmployeeService {

	// 비즈니스 구현체가 주입
	private EmployeeService business;

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

        try {
        	business.resetLevels()
            this.transactionManager.commit(status);
        } catch (Exception e) {
            this.transactionManager.rollback(status);
            throw e;
        }
	}
}

먼저 트랜잭션 구현체를 코딩한다. 클라이언트는 인터페이스를 통해 트랜잭션 구현체를 호출하고, 트랜잭션 구현체에 주입되어있는 비즈니스 구현체에 다시 요청을 하는 구조이다.

public class EmployeeServiceBusinessImpl implements EmployeeService {

	// 비즈니스 구현체가 주입
	private EmployeeService business;

	public void resetLevels() {
		List<User> users = userDao.getAlll();

        for(User user : users)
        	resetLevel(user)
	}
}

프록시, 프록시패턴, 데코레이터 패턴

위의 트랜잭션 구현체 같이 요청을 대신 중계하는 역활을 하는 계층을 프록시라고 한다. 프록시패턴은 타깃 오브젝트에 대한 접근 제어를 하는 패턴이다. 실제 객체에 대한 액세스를 제어하거나(가상 프록시), 클라이언트가 실제 객체에 대한 액세스를 허용할지 여부를 결정하거나(보호 프록시), 객체가 다른 주소 공간에 있는 경우 해당 객체에 대한 액세스를 제공하는 원격 프록시이다.

데코레이터 패턴은 타깃의 기능을 확장하고 추가하는 패턴이다. 따라서 타깃에 대한 데코레이터는 프록시패턴과 비교했을때 여러개일 가능성이 높다.

다이나믹 프록시 (InvocationHandler)

하지만 일일히 모든 비즈니스코드에 대해 DI을 이용한 프록시 클래스 생성은 번거롭다. 자바에는 reflect 패키지를 통해 프록시를 쉽게 만들수 있도록 지원하는 클래스들이 있다. 자바가 지원하는 InvocationHandler를 이용하여 트랜잭션 로직 클래스를 작성해본다.

public class EmployeeServiceTransactionHandler implements InvocationHandler {

	// 프록시의 타겟
	private EmployeeService target;

	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

		TransactionStatus status = this.transactionManager
        	.getTransaction(new DefaultTransactionDefinition());

        try {
        	method.invoke(target, args)

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

이제 InvocationHandler를 활용하여 프록시를 만들수 있다.

EmployeeService service = proxy.newProxyInstance(
	getClass().getClassLoader(),

	# 사용할 클래스(인터페이스)
	new Class[] { EmployeeService.class },

	new EmployeeServiceTransactionHandler(
		new EmployeeServiceBusinessImpl()
	)
)

InvocationHandler를 활용하여 프록시 클래스를 생성하면, 해당 클래스에 한해 메소드가 추가되더라도 코드를 추가할 필요가 없다.

현재 EmployeeServiceTransactionHandler는 EmployeeService에 한정하여 모든 메소드에 트랜잭션을 걸수 있는데 보다 넓은 범위의 클래스에 적용하고 싶다면 아래와 같이 타겟을 Object 클래스로 받으면 된다.

public class TransactionHandler implements InvocationHandler {

	// 타겟을 object로 받는다.
	private Object target;

	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

		TransactionStatus status = this.transactionManager
        	.getTransaction(new DefaultTransactionDefinition());

        try {
        	method.invoke(target, args)

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

팩토리 빈

다이나믹 프록시를 이용해 생성한 프록시클래스는 어떻게 스프링 빈으로 등록할까? 스프링 빈은 클래스이름을 가지고 리플랙션을 통해 오브젝트를 만드는데, 말그대로 동적으로 결정이 되기 때문에 스프링컨테이너가 어떤 타겟을 대상으로 프록시를 만들어야 할지 판단을 못한다. (또한 스태틱 매소드를 빈으로 등록할수 없다.)

이문제점의 해결을 위해 스프링 컨테이너에선 팩토리빈을 제공한다. 트랜잭션 구현체를 제공하는 펙토리를 다음과 같이 코딩할수 있다.

public class EmployeeServiceFactoryBean implements Factory<EmployeeService> {

	// 타겟을 object로 받는다.
	private Object target;
	private PlatformTransactionManager manager;
	private Class<?> serviceInterface;
	private String pattern;

	public void setTarget(Object target) {
		this.target = target;
	}

	public void setPattern(String pattern) {
		this.pattern = target;
	}

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

	public void setServiceInterfase(Class<?> serviceInterface) {
		this.serviceInterface = serviceInterface;
	}

	public Object getObject() throws Exception {
		TransactionHandler handler = new TransactionHandler();

		handler.setTarget(target);
		handler.setTransactionManager(manager);
		handler.setPattern(pattern);

		return Proxy.newProxyInstance(
			getClass.getClassLoader(), new Class[] { serviceInterface},
			handler
		)
	}

	// is singleton bean
	public boolean isSingleTon() {
		return false;
	}

	public Object invoke(Object proxy, Method, method, Object[] args) throws Throwable {

		TransactionStatus status = this.transactionManager
        	.getTransaction(new DefaultTransactionDefinition());

        try {
        	method.invoke(target, args)

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

이렇게 만든 팩토리빈은 다음과 같은 빈 예시 설정을 통해 프록시 클래스를 몇번이고 재활용하며 찍어낼수 있다.

<bean id="memberService" class="your.package.EmployeeServiceFactoryBean">
	<property name="target" ref="EmployeeServiceBusinessImpl" />
	<property name="pattern" ref="resetLevels" />
	<property name="transactionManager" ref="transactionManager" />
	<property name="serviceInterface" ref="your.package.EmployeeService"/>
</bean>

하지만 여러 클래스에 적용하려면 여러개의 팩토리빈 설정이 필요하기 때문에 한계점이 있다.

스프링의 프록시팩토리빈과 MethodInterceptor

public class LowerCaseAdvice implements MethodInterceptor {
	public Object invoke(MethodInvocation invocation) throws Throwable {
		String ret = (String)invocation.proceed();
		return ret.toLowerCase();
	}
}

public class DynamicProxyTest {
	ProxyFactoryBean pfBean = new ProxyFactoryBean();

	pfBean.setTarget(new EmployeeServiceBusinessImpl());
	pfBean.addAdvice(new LowerCaseAdvice());
	EmployeeService service = (EmployeeService) pfBean.getObject();
}

스프링의 프록시 팩토리빈은 프록시를 생성해 빈 오브젝트의 등록을 돕는 팩토리빈이다.

InvocationHandler와 달리 MethodInterceptor의 구현체에는 타킷오브젝트가 등장하지 않고 타겟오브젝트의 정보가 담긴 MethodInvocation 오브젝트가 전달된다. 따라서 MethodInterceptor는 부가기능의 제공에만 집중할수 있다.

MethodInterceptor는 탬플릿 MethodInvocation 콜백으로 동작한다. 따라서 부가기능이 담긴 MethodInterceptor는 오브젝트를 싱글톤으로 두고 공유하여 활용이 가능하다.

이렇게 부가기능을 담은 오브젝트를 어드바이스라고 부른다.

프록시팩토리빈은 여러 인터셉터(MethodInterceptor)를 추가할수 있으므로, 하나의 프록시팩토리빈으로 여러개의 부가기능을 제공하는 프록시를 만들수 있다.

프록시팩토리빈은 디폴트로 jdk 다이나믹 프록시 기반이지만 cglib 프레임워크를 통해 바이트코드를 조작해 프록시를 만들기도 한다.

포인트컷

MethodIntercepter를 통해 부가기능을 분리했다. 그렇다면 클래스내에서 어떻게 메소드를 선정하여 적용할수 있을까?

스프링에선 프록시팩토리빈에서 필요로 하는 메소드 선정 알고리즘을 담은 오브젝트를 포인트컷이라고 부른다.

포인트컷 인터페이스의 경우 메소드 매처 뿐만아니라 클래스 필터도 적용할수 있다. 포인트컷으로 어드바이스가 적용될 클래스를 먼저 선택한 다음 클래스의 메소드를 선택해 어드바이스를 적용할수 있다.

이를 통해 포인트컷과 어드바이스를 조합한 객체인 어드바이서를 만들어 자동 팩토리빈 생성기의 도움을 받으면 프록시를 쉽게 찍어낼수 있다.

다음은 포인트컷까지 활용한 예졔이다.

public class LowerCaseAdvice implements MethodInterceptor {
	public Object invoke(MethodInvocation invocation) throws Throwable {
		String ret = (String)invocation.proceed();
		return ret.toLowerCase();
	}
}

public class DynamicProxyTest {
	ProxyFactoryBean pfBean = new ProxyFactoryBean();

	// 포인트컷
	NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
	pointcut.setMappedName("reset*")
	pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new LowerCaseAdvice()))
	pfBean.setTarget(new EmployeeServiceBusinessImpl());

	EmployeeService service = (EmployeeService) pfBean.getObject();
}

NameMatchMethodPointcut 의 경우 클래스지정 기능이 없기 때문에 팩토리빈을 수동으로 만들어서 타깃 클래스를 직접 지정한다.

프록시팩토리빈에서 어드바이스, 포인트컷을 독립시키는 전략패턴을 통해 OCP를 지키는것을 도와준다.

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

AspectJ 처럼 프록시 방식이 아니라 바이트코드를 조작하여 AOP를 제공하기도 한다.

바이트코드를 직접 조작하면 오브젝트 생성, 필드값의 조회조작, 스태틱 초기화등의 다양한 작업에 부가기능을 제공할수 있다.

AOP 의 구성요소

  • 타깃: 부가기능을 부여할 대상
  • 어드바이스: 부가기능을 담당하는 오브젝트
  • 조인포인트: 어드바이스가 적용될 위치 (클래스, 메소드)
  • 포인트컷: 어드바이스를 적용할 조인포인트를 선정하는 모듈

AOP의 주의사항

  • 프록시 방식의 AOP는 같은 타깃 오브젝트 내의 메소드 호출시 적용되지 않는다.

AOP의 의의

AOP를 통해 포인트컷과 어드바이스를 분리하여 개방패쇄원칙, 의존관계역전원칙을 지킬수 있고 재사용성과 유지보수를 높힌다.

Leave a comment