개요#

6.1. 트랜잭션 코드의 분리#

6.1.1. 메소드 분리#

  • 트랜잭션 경계설정과 비즈니스 로직이 공존하는 메소드
public void upgradeLevels() throws Exception {
	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;
	}
}
  • 비즈니스 로직과 트랜잭션 경계설정의 분리
public void upgradeLevels() throws Exception {
	TransactionStatus status = 
		this.transactionManager.getTransaction(new DefaultTransactionDefinition());
	try {
		upgradeLevelsInternal();
		this.transactionManager.commit(status);
	} catch (RuntimeException e) {
		this.transactionManager.rollback(status);
		throw e;
	}
}

private void upgradeLevelsInternal() {
	List<User> users = userDao.getAll();
	for (User user : users) {
		if (canUpgradeLevel(user)) {
			upgradeLevel(user);
		}
	}
}

6.1.2. DI를 이용한 클래스의 분리#

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

  • UserService 클래스를 사용하는 코드는 클라이언트 코드이다.
  • UserService는 구체 클래스라서 클라이언트와는 강한 결합으로 묶여있다.
  • UserService 를 인터페이스로 만들고 별도의 구체 클래스를 만든다.
그림 6-1 그림 6-2 그림 6-3

UserService 인터페이스 도입#

  • 앞서 본 UserService 를 UserService 인터페이스와 UserServiceImpl 구체 클래스로 분리한다.
  • 트랜잭션 관련코드는 분리해서 처리할 예정이라 모두 제거한다.
public interface UserService {
	void add(User user);
	void upgradeLevels();
}
public class UserServiceImpl implements UserService {
	private UserDao userDao;
	private MailSender mailSender;
	
	public void upgradeLevels() {
		List<User> users = userDao.getAll();
		for (User user : users) {
			if (canUpgradeLevel(user)) {
				upgradeLevel(user);
			}
		}
	}
	...

분리된 트랜잭션 기능#

  • 트랜잭션 처리를 담은 UserServiceTx를 만든다.
  • 그외에는 위임하는 코드로 처리한다.
public class UserServiceTx implements UserService {
	UserService userService;

	public void setUserService(UserService userService) {
		this.userService = userService;
	}

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

	public void upgradeLevels() {
		userService.upgradeLevels();
	}
}
  • UserServiceTx에 트랜잭션 경계설정이라는 부가적인 작업을 부여해보자.
  • 트랜잭션이 적용된 UserServiceTx
public class UserServiceTx implements UserService {
	UserService userService;
	PlatformTransactionManager transactionManager;

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

	public void setUserService(UserService userService) {
		this.userService = userService;
	}

	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;
		}
	}
}

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

그림 6-4
<bean id="userService" class="springbook.user.service.UserServiceTx">  
	<property name="transactionManager" ref="transactionManager" />
	<property name="userService" ref="userServiceImpl" />  
</bean>
<bean id="userServiceImpl" class="springbook.user.service.UserServiceImpl">
	<property name="userDao" ref="userDao" />
	<property name="mailSender" ref="mailSender" />
</bean>

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

@Test
public void upgradeAllOrNothing() {
	TestUserService testUserService = new TestUserService(users.get(3).getId());
	testUserService.setUserDao(userDao);
	testUserService.setMailSender(mailSender);
	
	UserServiceTx txUserService = new UserServiceTx();
	txUserService.setTransactionManager(transactionManager);
	txUserService.setUserService(testUserService);
	 
	userDao.deleteAll();			  
	for(User user : users) userDao.add(user);
	
	try {
		txUserService.upgradeLevels();   
		fail("TestUserServiceException expected"); 
	}
	catch(TestUserServiceException e) { 
	}
	
	checkLevelUpgraded(users.get(1), false);
}

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

  • 비즈니스 로직을 담당하고 있는 UserServiceImpl 의 코드를 작성할때는 트랜잭션과 같은 기술적인 내용에는 전혀 신경쓰지 않아도 된다.
  • 비즈니스 로직에 대한 테스트를 손쉽게 만들어낼수 있다.

6.2. 고립된 단위 테스트#

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

그림 6-4
  • UserService는 UserDao, MailSender, PlatformTransactionManager 등의 오브젝트에 의존한다.
  • UserDao 는 DataSource와 DB및 네트워크에 의존한다.
  • 이처럼 특정 클래스를 테스트하기 위해서는 의존하는 많은 오브젝트를 고려할 필요가 있고 그만큼 복잡도는 늘어나고 오래 걸린다.

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

테스트를 위한 UserServiceImpl 고립#

고립된 단위 테스트 활용#

UserDao 목 오브젝트#

테스트 수행 성능의 향상#

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

6.2.4. 목 프레임워크#

Mockito 프레임워크#

UserDao mockUserDao = mock(UserDao.class);

when(mockUserDao.getAll()).thenReturn(this.users);

verify(mockUserDao, times(2)).update(any(User.class)));

6.3. 다이나믹 프록시와 팩토리 빈#

6.3.1. 프록시와 프록시 패턴, 데코레이터 패턴#

  • 전략패턴 적용을 통한 부가기능 구현의 분리
그림 6-7
  • 트랜잭션이라는 기능은 사용자 관리 비즈니스 로직과는 성격이 다르기 때문에 아예 그 적용 사실 자체를 밖으로 분리할 수 있다.
  • 부가기능과 핵심기능의 분리
그림 6-8
  • UserServiceTx는 UserServiceImpl 의 코드를 사용하는 형태로 분리했다.
  • 핵심기능은 부가기능을 가진 클래스 존재 자체를 모른다.
  • 클라이언트가 핵심기능을 가진 클래스를 직접 사용하면 부가기능을 적용할수 없다.
  • 클라이언트는 인터페이스를 통해서만 핵심기능을 사용하게 되고 부가기능 자신도 같은 인터페이스를 구현한 뒤에 자신이 그 사이에 끼여들어야 한다.
  • 핵심기능 인터페이스의 적용
그림 6-8
  • 이렇게 마치 자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 대리자, 대리인과 같은 역할을 한다고 해서 프록시라고 부른다.
  • 프록시를 통해 최종적으로 요청을 위임받아 처리하는 실재 오브젝트를 타겟 target 또는 실체 real subject 라고 부른다.
  • 프록시와 타겟
그림 6-8
  • 프록시의 특징은 타겟과 같은 인터페이스를 구현했다는 점과 프록시가 타겟을 제어할수 있는 위치에 있다는 것이다.
  • 프록시의 사용목적
    1. 클라이언트가 타겟에 접근하는 방법을 제어하기 위함
    2. 타겟에 부가적인 기능을 부여하기 위함

데코레이터 패턴#

  • 데코레이터 패턴은 타겟에 부가적인 기능을 런타임 시 다이내믹하게 부여해주기 위해 프록시를 사용하는 패턴
  • 즉 컴파일 시점에 정해지는 건 아니다.
  • 마치 제품이나 케익등을 여러겹으로 포장하고 그 위에 장식을 붙이는 것처럼 실제 내용물은 동일하지만 부가적인 효과를 줄수 있다.
  • 데코레이터 패턴에서는 같은 인터페이스를 구현한 타겟과 여러개의 프록시를 사용할 수 있다.
  • 데코레이터 패턴 적용 예
그림 6-11
  • 자바 IO 패키지의 InputStream과 OutputStream 구현 클래스는 데코레이터 패턴을 사용한 대표적인 예
InputStream is = new BufferedInputStream(new FileInputStream("a.txt"));
  • UserServiceTx가 UserServiceImpl 을 호출하는 형태로 데코레이터 패턴이다.
<bean id="userService" class="springbook.user.service.UserServiceTx">  
	<property name="transactionManager" ref="transactionManager" />
	<property name="userService" ref="userServiceImpl" /><!-- DI를 통한 다이내믹한 구성 -->
</bean>
<bean id="userServiceImpl" class="springbook.user.service.UserServiceImpl">
	<property name="userDao" ref="userDao" />
	<property name="mailSender" ref="mailSender" />
</bean>	
  • 데코레이터 패턴은 인터페이스를 통해 위임하는 방식이기 때문에 어느 데코레이터에서 타겟으로 연결될지 코드레벨에서는 알수가 없다.
  • 구성하기 따라서 여러개의 데코레이터를 적용할 수도 있다.

프록시 패턴#

  • 일반적으로 사용하는 프록시 : 클라이언트와 사용 대상 사이에 대리 역할을 맡은 오브젝트를 두는 방법을 총칭
  • 디자인 패턴에서 프록시 : 프록시를 사용하는 방법 중에서 타겟에 대한 접근 방법을 제어하려는 목적을 가진 경우
  • 프록시 패턴의 프록시는 타겟의 기능을 확장하거나 추가하지 않고 클라이언트가 타겟에 접근하는 방식을 변경해준다.
  • 원격 오브젝트를 이용하는 경우에도 프록시를 사용하면 편리하다.
  • 특별한 상황에서 타겟에 대한 접근권한을 제어하기 위해 프록시 패턴을 사용한다.

6.3.2. 다이내믹 프록시#

  • 프록시는 기존 코드에 영향을 주지 않으면서 타겟의 기능을 확장하거나 접근방법을 제어할 수 있는 유용한 방법이다.
  • 대개는 프록시를 만드는 일을 상당히 번거롭게 생각한다.

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

  • 프록시는 다음의 두가지 기능으로 구성한다.
    1. 타겟과 같은 메소드를 구현하고 있다가 메소드가 호출되면 타겟 오브젝트로 위임한다.
    2. 지정된 요청에 대해서는 부가기능을 수행한다.
public class UserServiceTx implements UserService {
	UserService userService;	// 타겟 오브젝트
	...
	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;
		}
	}
}
  • 프록시의 역할은 위임과 부가작업으로 구분할 수 있다.
  • 프록시를 만들기 번거로운 이유
    1. 타겟의 인터페이스를 구현하고 위임하는 코드를 작성하기 번거롭다. 부가기능이 필요없는 메소드도 일일히 구현해야 해서 메소드가 많을 경우 부담스러울수밖에 없다.
    2. 부가기능 코드가 중복될 가능성이 높다. 메소드마다 비슷하게 트랜잭션을 관리하는 코드가 들어간다면 트랜잭션 관리 코드가 중복으로 나타난다.
  • 인터페이스 메소드의 구현과 위임 기능 문제는 JDK 다이내믹 프록시를 사용해서 해결할 수 있다.

리플렉션#

  • 다이내믹 프록시는 리플렉션 기능을 이용해서 프록시를 만들어준다.
  • 리플렉션 학습 테스트
리스트 6-17

프록시 클래스#

interface Hello {
	String sayHello(String name);
	String sayHi(String name);
	String sayThankYou(String name);
}

public class HelloTarget implements Hello {
	public String sayHello(String name) {
		return "Hello " + name;
	}

	public String sayHi(String name) {
		return "Hi " + name;
	}

	public String sayThankYou(String name) {
		return "Thank You " + name;
	}
}

@Test
public void simpleProxy() {
	Hello hello = new HelloTarget();
	assertThat(hello.sayHello("Toby"), is("Hello Toby"));
	assertThat(hello.sayHi("Toby"), is("Hi Toby"));
	assertThat(hello.sayThankYou("Toby"), is("Thank You Toby"));
}
public class HelloUppercase implements Hello {
	Hello hello;
	
	public HelloUppercase(Hello hello) {
		this.hello = hello;
	}

	public String sayHello(String name) {
		return hello.sayHello(name).toUpperCase();
	}

	public String sayHi(String name) {
		return hello.sayHi(name).toUpperCase();
	}

	public String sayThankYou(String name) {
		return hello.sayThankYou(name).toUpperCase();
	}	
}

Hello proxiedHello = new HelloUppercase(new HelloTarget());
assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
assertThat(proxiedHello.sayThankYou("Toby"), is("THANK YOU TOBY"));
  • HelloUppercase 는 프록시 적용의 일반적인 문제점 두가지를 가진다.
    1. 인터페이스의 모든 메소드를 구현해 위임하도록 코드를 만들어야 한다.
    2. 부가기능인 리턴값을 대문자로 바꾸는 기능이 모든 메소드에 중복되어 나타난다.

다이내믹 프록시 적용#

  • 다이내믹 프록시의 동작방식
그림 6-13
  • 다이내믹 프록시는 프록시 팩토리에 의해 런타임 시 다이내믹하게 만들어지는 오브젝트
  • 프록시 팩토리에서 인터페이스 정보만 제공해주면 해당 인터페이스를 구현한 클래스의 오브젝트를 자동으로 만들어준다.
  • 다이내믹 프록시가 인터페이스 구현 클래스의 오브젝트는 만들어주지만 프록시로서 필요한 부가기능 제공 코드는 직접 작성해야 한다.
  • 부가기능은 프록시 오브젝트와 독립적으로 InvocationHandler 를 구현한 오브젝트에 담는다.
  • InvocationHandler 인터페이스는 invoke 메소드 한개를 가진다.
public Object invoke(Object proxy, Method method, Object[] args)
  • InvocationHandler 를 통한 요청 처리 구조
그림 6-14
public class UppercaseHandler implements InvocationHandler {
	Hello target;

	private UppercaseHandler(Hello target) {
		this.target = target;
	}

	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		Object ret = method.invoke(target, args);
		return ret.toUpperCase();
	}
}
Hello proxiedHello = (Hello)Proxy.newProxyInstance(	// 생성된 다이내믹 프록시 오브젝트는 Hello 인터페이스를 구현하고 있으므로 Hello타입으로 캐스팅해도 안전하다. 
	getClass().getClassLoader(),	// 동적으로 생성되는 다이내믹 프록시 클래스의 로딩에 사용할 클래스 로더
	new Class[] { Hello.class},		// 구현할 인터페이스
	new UppercaseHandler(new HelloTarget()));	// 부가기능과 위임코드를 담은 InvocationHandler
  • 첫번째 파라미터는 클래스 로더를 제공해야 한다.
  • 두번째 파라미터는 다이내믹 프록시가 구현해야 할 인터페이스이다.
  • 마지막 파라미터로는 부가기능과 위임 관련 코드를 담고 있는 InvocationHandler 구현 오브젝트를 제공해야 한다.
  • 다이내믹 프록시를 사용하면 리플렉션 API도 사용하고 복잡한 다이내믹 프록시 생성 방법을 사용하고 코드의 양도 그다지 줄어보이지 않는다. 그런데도 장점이 있을까?

다이내믹 프록시의 확장#

  • Hello 인터페이스의 메소드가 3개가 아닌 30개 처럼 엄청나게 많다고 생각해보자. 이런 경우 직접 모든 메소드를 구현하는 것보다는 다이내믹 프록시 방식이 장점이 있다.
  • 앞서 UppercaseHandler는 리턴타입이 String이라고 가정한다. 리플렉션 API를 사용할때는 리턴타입에 대해서 확인하는게 좋다.
  • 특정 메소드에 대해서만 부가적인 작업을 진행하고자 할때는 메소드 이름을 확인한다.
public class UppercaseHandler implements InvocationHandler {
	Object target;

	private UppercaseHandler(Object target) {
		this.target = target;
	}

	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		Object ret = method.invoke(target, args);
		if (ret instanceof String && method.getName().startsWith("say")) {
			return ((String)ret).toUpperCase();
		}
		else {
			return ret;
		}
	}
}

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

트랜잭션 InvocationHandler#

public class TransactionHandler implements InvocationHandler {
	Object target;
	PlatformTransactionManager transactionManager;
	String pattern;

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

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

	public void setPattern(String pattern) {
		this.pattern = 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();
		}
	}
}

Add new attachment

Only authorized users are allowed to upload new attachments.
« This page (revision-10) was last changed on 24-Nov-2014 22:43 by DongGukLee