상세 컨텐츠

본문 제목

[6.3] 다이나믹 프록시와 팩토리 빈

토비의스프링

by kwanghyup 2020. 11. 2. 20:55

본문

# 리플렉션 

 

리플렉션 API 중에서 메소드에 대한 정의를 담은 

Method인터페이스를 이용해 메소드를 호출하는 방법을 알아보자.

 

클래스 정보에서 특정이름을 가진 메소드 정보를 가져올 수 있다.

String의 length()메소드라면 다음과 같이 하면된다.

Method lengthMethod = String.class.getMethod("length");
// 스트링이 가진 메소드 중에서 "length"라는 이름을 갖고 있고 
// 파라미터는 없는 메소드의 정보를 가져오는 것이다.

 

이를 이용해 length메소드를 다음과 같이 실행할 수 있다.

int length = lengthMethod.invoke(name);

 

리플렉션 학습 테스트 

package springbook.learningtest.jdk;

import static org.junit.Assert.assertEquals;

import java.lang.reflect.Method;

import org.junit.Test;

public class ReflectionTest {
	
	@Test
	public void invokeMethod() throws Exception{
		String name = "Spring";
		
		assertEquals(name.length(), 6);
		
		Method lengthMethod = String.class.getMethod("length");
		assertEquals(lengthMethod.invoke(name), 6);
		
		assertEquals(name.charAt(0), 'S');
		
		Method charAtMethod = String.class.getMethod("charAt", int.class);
		assertEquals(charAtMethod.invoke(name, 0), 'S');	
	}	
}

 

# 프록시 클래스 

 

다이내믹 프록시를 이용한 프록시를 만들어보자. 

 

Hello인터페이스

package springbook.learningtest.jdk;

public interface Hello {
	String sayHello(String name);
	String sayHi(String name);
	String sayThankyou(String name);
}

 

이를 구현한 타킷 클래스는 다음과 같다. 

package springbook.learningtest.jdk;

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

}

 

HelloTarget 오브젝트를 사용하는 클라이언트 역할을 하는 간단한 테스트를 만들어보자.

package springbook.learningtest.jdk;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class HelloTest {
	
	@Test
	public void simpleProxy() {
		Hello hello = new HelloTarget();
		assertEquals(hello.sayHello("Toby"), "Hello Toby");
		assertEquals(hello.sayHi("Toby"), "Hi Toby");
		assertEquals(hello.sayThankyou("Toby"), "Thank You Toby");
	}
	
}

 

Hello인터페이스를 구현한 프록시를 만들자.

추가할 기능은 리턴하는 문자를 모두 대문자로 바꿔주는 것이다. 

 

HelloUppercase 프록시 클래스 

package springbook.learningtest.jdk;

public class HelloUppercase implements Hello{

	// 인터페이스를 통해서 타킷 오브젝트에 접근한다.
	Hello hello;
	
	public HelloUppercase(Hello hello) {
		this.hello = hello;
	}

	@Override
	public String sayHello(String name) {
		//위임과 부가기능 적용 
		return hello.sayHello(name).toUpperCase();
	}

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

	@Override
	public String sayThankyou(String name) {
		return hello.sayThankyou(name).toUpperCase();
	}
}

 

테스트 코드를 추가해서 프록시가 동작하는지 확인해보자.

@Test
public void proxyTest() {
	Hello proxiHello = new HelloUppercase(new HelloTarget());
	assertEquals(proxiHello.sayHello("Toby"), "HELLO TOBY");
	assertEquals(proxiHello.sayHi("Toby"), "HI TOBY");
	assertEquals(proxiHello.sayThankyou("Toby"), "THANK YOU TOBY");
}

 

이 프록시는 프록시 적용의 일반적인 문제점 두 가지를 모두 갖고 있다.

1. 모든 메소드를 구현해 위임하도록 코드를 만들어야 한다.

2. 부가기능인 리턴 값을 대문자로 바꾸는 기능이 모든 메소드에 중복되어 나타난다.

 

# 다이내믹 프록시 적용

 

다이내믹 프록시는 프록시 팩토리에 의해 런타임 시 다이내믹하게 만들어지는 오트젝트이다.

프록시 오브젝트는 타킷의 인터페이스와 같은 타입으로 만들어진다.

프록시 팩토리에게 인터페이스 정보만 제공해주면

해당 인터페이스를 구현한 클래스의 오브젝트를 자동으로 만들어준다.

 

다이내믹 프록시가 인터페이스 구현 클래스의 오브젝트는 만들어주지만

프록시로서 필요한 부가기능 제공코드는 직접 작성해야 한다.

 

부가기능은 InvocationHandler를 구현한 오브젝트에 담는다.

InvocationHandler인터페이스는 다음의 메소드를 가진다.

public Object invoke(Object proxy, Method method, Object[] args);

Method method : 리플렉션의 Method인터페이스이다.

Object[] args : 메소드를 호출할 때 전달되는 파라미터이다. 

 

 

다이내믹 프록시를 만들어보자. 

 

HelloUppercase클래스와 마찬가지로 모든 요청을 타깃에 위임하면서

리턴 값을 대문자로 바꿔주는 부가기능을 가진 InvocationHandler구현 클래스이다.

package springbook.learningtest.jdk;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class UppercaseHandler implements InvocationHandler{
	
	Hello target;
	
	public UppercaseHandler(Hello target) {
		this.target = target;
	}
	
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		// 인터페이스 메소드 호출에 모두 적용된다. 
		String ret = (String) method.invoke(target, args);
		return ret.toUpperCase();
	}
}

 

이제 이 Invocationhandler를 사용하고 Hello 인터페이스를 구현하는

프록시를 만들어보자. 

	@Test
	public void proxyTest() {
		//Hello proxiHello = new HelloUppercase(new HelloTarget());
		Hello proxiHello = (Hello) Proxy.newProxyInstance(
				getClass().getClassLoader(),
				new Class[] {Hello.class},
				new UppercaseHandler(new HelloTarget()));
		
		assertEquals(proxiHello.sayHello("Toby"), "HELLO TOBY");
		assertEquals(proxiHello.sayHi("Toby"), "HI TOBY");
		assertEquals(proxiHello.sayThankyou("Toby"), "THANK YOU TOBY");
	}

newProxyInsatance();

첫 번째 파라미터 : 다이내믹 프록시가 정의되는 클래스 로더 지정

두 번째 파라미터 : 다이내믹 프록시가 구현해야할 인터페이스 

세 번째 파라미터 : 부가기능과 위임관련 코드를 담고 있는 InvocationHandler 구현 오브젝트 

 

# 다이내믹 프록시의 확장 

 

InvocaitonHandler방식의 또 한가지 장점은 타깃의 종류에 상관없이도 적용가능하다는 점이다.

어떤 종류의 인터페이스를 구현한 타깃이든 상관없이 재사용할 수 있고,

메소드의 리턴 타입이 스트링인 경우에만 대문자로 결과로 바꿔주도록

UppercaseHandler를 바꿔 줄 수 있다.

 

public class UppercaseHandler implements InvocationHandler{
	
	// 어떤 종류의 인터페이스를 구현한 타깃에도 적용가능하다. 
	Object target;
	
	public UppercaseHandler(Object target) {
		this.target = target;
	}
	
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		Object ret = method.invoke(target, args);
		
		//String 타입일 때만 toUpperCase()메소드를 적용한다.
		if(ret instanceof String) return ((String)ret).toUpperCase(); 
		else return ret;
	}
}

 

호출하는 메소드의 이름, 파라미터의 개수와 타입, 리턴 타입 등의 정보를 가지고 

부가적인 기능을 적용할 메소드를 선택할 수 있다.

 

메소드의 이름이 say로 시작하는 경우에만 대문자로 바꾸는 기능을 적용하고 싶다면 다음과 같이 한다.

package springbook.learningtest.jdk;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class UppercaseHandlerMethodName implements InvocationHandler{

	Object target;
	
	public UppercaseHandlerMethodName(Object target) {
		this.target = target;
	}

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

 

테스트를 만들어서 확인한다.

@Test 
public void proxyMethodNameTest() {
	Hello proxiHello = (Hello) Proxy.newProxyInstance(
			getClass().getClassLoader(),
			new Class[] {Hello.class},
			new UppercaseHandlerMethodName(new HelloTarget()));
	assertEquals(proxiHello.sayHello("Toby"), "HELLO TOBY");
	assertEquals(proxiHello.sayHi("Toby"), "HI TOBY");
	// sayThankyou메소드에는 부가기능이 적용되지 않아야 한다. 
	assertEquals(proxiHello.sayThankyou("Toby"), "Thank You Toby");
}

 

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

 

UserServiceTx를 다이내믹 프록시 방식으로 변경해보자.

 

## 트랜잭션 InvocationHandler

 

다이내믹 프록시를 위한 트랜잭션 부가기능 

package springbook.user.transaction;
//...
public class TransactionHandler implements InvocationHandler{
	
	private Object target;
	private PlatformTransactionManager transactionManager;
	private String pattern;
	
	public void setTarget(Object target) {
		this.target = target;
	}

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

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

	
	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();
		}
	}
	
	
	@Override
	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);
		}
	}
		
}

UserServiceImpl 외에 트랜잭션 적용이 필요한 어떤 타깃 오브젝트에도 적용할 수 있다.

 

Method.invoke()를 이용해 타깃 오브젝트의 메소드를 호출할 때에는

타깃 오브젝트에서 발생하는 예외가 InvocationTargetException으로 한 번 포장된서 전달된다.

롤백을 적용하기 위한 예외는 InvocationTargetException으로 받은 후 

getTargetException()메소드로 중첩되어 있는 예외를 가져와야 한다.

 

## TransactionHandler와 다이내믹 프록시를 이용한 테스트 

 

UserServcieTest - upgradeAllOrNothing()에 적용해보자.

 

다이내믹 프록시를 이용한 트랜잭션 테스트 

@Test
public void upgradeAllorNothing() throws Exception {
	UserServiceImpl testUserService = new TestUserService(users.get(3).getId());
	testUserService.setUserDao(this.userDao); 
	testUserService.setMailSender(mailSender);
	
	//InovcationHandler 구현 객체 생성 
	TransactionHandler txHandler = new TransactionHandler();
	txHandler.setTarget(testUserService);
	txHandler.setTransactionManager(transactionManager);
	txHandler.setPattern("upgradeLevels");
	
	// 프록시 객체 생성 
	UserService txUserService = (UserService) Proxy.newProxyInstance(
			getClass().getClassLoader(),
			new Class[] {UserService.class},
			txHandler);
	//...

}

 

테스트 수행하여 확인하자.

 

# 다이나믹 프록시를 위한 팩토리 빈 

 

TransactionHandler와 다이내믹 프록시를 스프링의 DI를 통해 

사용할 수 있도록 만들어야 할 차례다.

 

그런데 문제는 DI 대상이 되는 다이내믹 프록시 오브젝트는 

일반적인 스프링 빈으로 등록할 방법이 없다는 것이다.

 

스프링은 지정된 클래스 이름을 가지고 리플렉션을 이용해서 해당 클래스의 오브젝트를 만든다.

클래스의 이름을 갖고 있다면 다음과 같은 방법으로 새로운 오브젝트를 생성할 수 있다.

Class의 newInstance()메소드는 해당 클래스의 파라미터가 없는 생성자를 호출하고

그 결과 생성되는 오브젝트를 돌려주는 리플렉션 API이다.

Date now = (Date) Class.forName("java.util.Date").newInstance();

 

 

스프링은 내부적으로 리플렉션 API를 이용하여 빈 정의에 나오는 클래스 이름을 가지고 오브젝트를 생성한다.

문제는 다이내믹 프록시 오브젝트는 이런 식으로 프록시 오브젝트가 생성되지 않는다는 점이다.

 

사전에 프록시 오브젝트의 클래스 정보를 미리 알아내서 스프링의 빈에 정의할 방법이 없다.

다이내믹 프록시는 Proxy 클래스의 newProxyInstance()라는 

스태틱 팩토리 메소드를 통해서만 만들 수 있다.

 

## 팩토리 빈 

 

팩토리빈을 만드는 가장 간단한 방법은 스프링의 FactoryBean인터페이스를 구현하는 것이다.

 

팩토리빈의 동작원리를 확인할 수 있도록 만들어진 학습테스트를 하나 살펴보자.

다음의 Message 클래스를 만들자.

package springbook.learningtest.jdk;

public class Message {
	
	String text;

	// 외부에서 생성자를 통해서 오브젝트를 만들 수 없다. 
	private Message(String text) {
		this.text = text;
	}
	
	public String getText() {
		return this.text;
	}
	
	// 생성자 대신에 사용할 수 있는 스태택 팩토리 메소드를 제공한다.
	public static Message newMessage(String text) {
		return new Message(text);
	}
}

 

이 클래스를 직접 스프링 빈으로 등록해서 사용할 수 없다.

 

Message클래스의 오브젝트를 생성해주는 팩토리 빈 클래스를 만들어 보자.

package springbook.learningtest.spring.factorybean;

import org.springframework.beans.factory.FactoryBean;

public class MessageFactoryBean implements FactoryBean<Message>{

	String text; 
	
	// 오브젝트를 생성할 때 필요한 정보를 팩토리 빈의 프로퍼티로 설정해서 
	// 대신 DI 받게 한다. 주입된 정보는 오브젝트 생성 중에 사용된다. 
	public void setText(String text) {
		this.text = text;
	}
	
	// 실제 빈으로 사용될 오브젝트를 직접생성한다.
	@Override
	public Message getObject() throws Exception {
		return Message.newMessage(this.text);
	}

	@Override
	public Class<? extends Message> getObjectType() {
		return Message.class;
	}
	
	// 싱글톤 여부를 알려 주는 메소드이다. 
	// 이 팩토리빈은 매번 요청할 때마다 새로운 오브젝트를 만들므로 false로 설정한다.
	@Override
	public boolean isSingleton() {
		return false;
	}
}

 

## 팩토리 빈의 설정 방법 

 

설정파일을  FactoryBeanTest-context.xml 이름으로 만들자. 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

	<bean id="message" class="springbook.learningtest.spring.factorybean.MessageFactoryBean">
		<property name="text" value="Factory Bean"/>
	</bean>
	
</beans>

message 빈의 오브젝트 타입은 MessageFactoryBean이 아니라 Message타입이다.

이 타입은 getObjectType()메소드가 돌려주는 타입으로 결정된다. 

또한 getObject() 메소드가 생성해주는 오브젝트가 messge빈의 오브젝트가 된다. 

 

학습테스트를 통해 이를 확인해보자. 

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/FactoryBeanTest-context.xml")
public class FactoryBeanTest {

	@Autowired
	ApplicationContext context;
	
	@Test
	public void getMessageFromFactoryBean() {
		Object message = context.getBean("message");
		assertEquals(Message.class, message.getClass()); // 타입확인
		assertEquals(((Message)message).getText(), "Factory Bean"); // 설정과 기능 확인 
	}
}

 

팩토리 빈 자첼르 가져오고싶은 경우도 있다.

&를 빈 이름앞에 붙여주면 팩토리 빈 자체를 돌려준다.

@Test
public void getFactoryBean() {
	Object factory = context.getBean("&message");
	assertEquals(factory.getClass(),MessageFactoryBean.class);
}

 

## 다이내믹 프록시를 만들어주는 팩토리 빈 

 

팩토리 빈을 사용하면 다이내믹 프록시 오브젝트를 스프링 빈으로 만들어 줄 수 있다.

팩토리 빈의 getObject()메소드에 다이내믹 프록시 오브젝트를 만들어주는 코드를 넣으면된다.

 

다이내믹 프록시를 직접 만들어서 UserService에 적용해봤던

upgradeAllOrNothing() 테스트의 코드를 팩토리 빈을 만들어서 

getObject()안에 넣어 주기만 하면된다.

 

## 트랜잭션 프록시 팩토리 빈 

 

다음은 TransactionHandler를 이용하는 다이내믹 프록시를 생성하는 팩토리이다.

package springbook.user.service;

import java.lang.reflect.Proxy;

import org.springframework.beans.factory.FactoryBean;
import org.springframework.transaction.PlatformTransactionManager;

import springbook.user.transaction.TransactionHandler;

//생성할 오브트 타입을 지정할 수도 있지만 범용적으로 사용하기 위해 Object로 지정했다. 
public class TxProxyFactoryBean implements FactoryBean<Object>{

	Object target; 
	
	PlatformTransactionManager transactionManager;
	
	String pattern;
	
	// 다이내믹 프록시를 생성할 때 필요하다.
	// UserService외의 인터페이스를 가진 타깃에도 적용할 수 있다.
	Class<?> serviceInterface;
	
	
	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 void setServiceInterface(Class<?> serviceInterface) {
		this.serviceInterface = serviceInterface;
	}

	// DI 받은 정보를 이용해서 TransactionHanlder를 사용하는 다이내믹 프록시를 생성한다.
	@Override
	public Object getObject() throws Exception {
		TransactionHandler txHandler = new TransactionHandler();
		txHandler.setTarget(target);
		txHandler.setTransactionManager(transactionManager);
		txHandler.setPattern(pattern);
		Object proxiedObj = Proxy.newProxyInstance(
				getClass().getClassLoader(), 
				new Class[] {serviceInterface},
				txHandler);
		return proxiedObj;
	}

	// 팩토리 빈이 생성하는 오브젝트 타입은 DI 받은 인터페이스 타입에 따라 달라진다.
	// 다양한 타입의 프록시 오브젝트 생성에 재사용할 수 있다. 
	@Override
	public Class<?> getObjectType() {
		return serviceInterface;
	}
	
	@Override
	public boolean isSingleton() {
		return false;
	}
}

 

이제 UserServcieTx빈 설정을 대신해서 userService라는 이름으로 

TxProxyFactoryBean 팩토리를 빈으로 등록한다. 

UserServiceTx클래스는 제거해도 상관없다. 

<bean id="userService" class="springbook.user.service.TxProxyFactoryBean">
	<property name="target" ref="userServiceImpl"/>
	<property name="transactionManager" ref="transactionManager"/>
	<property name="serviceInterface" value="springbook.user.service.UserService"/>
	<property name="pattern" value="upgradeLevels"/>
</bean>

serviceInterface는 Class타입이다.

Class타입은 value를 이용해 클래스 또는 인터페이스 이름을 넣어주면된다.

그러면 스프링이 Class 오브젝트로 자동변환해준다.

 

## 트랜잭션 프록시 팩토리 빈 테스트 

 

TxProxyFactoryBean의 트랜잭션을 지원하는 프록시를 

바르게 만들어주는지를 확인하는 게 목적이므로 빈으로 등록된

TxProxyFactoryBean을 직접 가져와서 프록시를 만들어보면된다.

public class UserServiceTest {
//...

	// 팩토리 빈을 가져오려면 애플리케이션 컨텍스트가 필요하다. 
	@Autowired
	ApplicationContext context;

//...
    
	@Test
	@DirtiesContext 
	public void upgradeAllorNothing() throws Exception {
		
		UserServiceImpl testUserService = new TestUserService(users.get(3).getId());
		testUserService.setUserDao(this.userDao); 
		testUserService.setMailSender(mailSender);
		
		// 팩토리 빈 자체를 가져와야 하므로 빈 이름에 &를 반드시 넣어야 한다. 
		TxProxyFactoryBean txProxyFactoryBean = 
				context.getBean("&userService", TxProxyFactoryBean.class);
		txProxyFactoryBean.setTarget(testUserService);
		// 변경된 타깃 설정을 이용해서 트랜잭션 다이내믹 프록시 오브젝트를 생성한다. 
		UserService txUserService = (UserService) txProxyFactoryBean.getObject();
		
		userDao.deleteAll();
		for(User user : users) userDao.add(user);
		
		try {
			txUserService.upgradeLevels();
			fail("TestUserServiceException exptected");
		} catch (TestUserServiceException e) {
			// TODO: handle exception
		}
			
		checkLevelUpgrade(users.get(1), false);
	}
    
}

테스트를 수행하여 확인한다.

 

# 프록시 팩토리 빈 방식의 장점과 한계 

 

##프록시 팩토리 빈의 재사용 

 

## 프록시 팩토리 빈의 장점

 

## 프록시 팩토리 빈의 한계 

관련글 더보기

댓글 영역