토비의 스프링 정리
토비의 스프링 - 6.3 다이내믹 프록시와 팩토리 빈
ksb-dev
2022. 10. 7. 22:06
6.3.1 프록시
- 전략 패턴 적용을 통한 부가기능 구현의 분리
- 부가적 기능을 위임을 통해 분리
- 기능을 사용하는 코드는 핵심 코드와 함께 남아 있음
- 부가기능과 핵심기능의 분리
- 트랜잭션은 비즈니스 로직과 성격이 다르기 때문에 분리 가능
- 트랜잭션을 UserServiceTx로 분리하여 UserServiceImpl에는 비즈니스 로직만 남음
- 핵심기능 인터페이스 적용
- 부가기능과 핵심기능을 같은 인터페이스를 구현하게 만듦
- 클라이언트는 인터페이스를 통해서만 핵심기능 사용
- 부가기능 코드를 먼저 거쳐 비즈니스 로직 부분을 핵심기능에 위임함
- 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받기 때문에 프록시(Proxy)라도 부름
6.3.2 프록시 패턴 설명
- 부가기능과 핵심기능은 같은 인터페이스를 구현
- 프록시를 통해 최종적으로 요청을 위임받아 처리하는 오브젝트를 타깃(Target) or 실체(Real Subject)라고 함
- 프록시 사용 목적 두 가지
- 클라이언트가 타깃에 접근 방법 제어
- 타깃에 부가기능 부여
6.3.3 데코레이터 패턴 설명
- 타깃에 부가 기능을 런타임시에 다이내믹하게 부여하기 위해 프록시를 사용하는 패턴
- 코드상에 어떤 방법과 순서로 프록시와 타깃이 연결되어 사용되는지 정해지지 않음
- 새로운 기능을 추가할 때 유용함
- 데코레이터 패턴은 프록시의 개수를 제한받지 않음
- 따라서, 여러 부가 기능을 부여할 수 있음
- 데코레이터는 위임하는 대상에도 인터페이스로 접근하기 때문에 런타임시 주입받을 수 있음
- InputStream 구현 클래스는 데코레이터 패턴이 사용된 대표적인 예시임
- InputStream을 구현하는 FileInputStream에 버퍼 읽기 기능을 제공하는 BufferedInputStream이라는 데코레이터를 적용한 예시임
//InputStream : 인터페이스
//FileInputStream : 타깃
//BufferedInputStream : 데코레이터
InputStream is = new BufferedInputStream(new FileInputStream("a.txt"));
- 인터페이스를 통한 데코테이터 정의와 주입은 스프링의 DI 방식을 이용하면 편리함
- UserServiceTx 클래스로 선언 된 UserService 빈은 데코레이터임
- UserServiceTx는 UserService 타입의 오브젝트를 DI 받아 비즈니스 기능은 위임하지만, 그 과정에서 트랜잭션 경계설정 기능을 부여함
<beans
.../>
<!--데코레이터-->
<bean id="userService" class="com.ksb.spring.UserServiceTx">
<property name="transactionManager" ref="transactionManager"/>
<property name="userService" ref="userServiceImpl"/>
</bean>
<!--타깃-->
<bean id="userServiceImpl" class="com.ksb.spring.UserServiceImpl">
<property name="userDao" ref="userDao"/>
<property name="mailSender" ref="mailSender"/>
</bean>
<beans/>
6.3.4 프록시 패턴
- 일반적으로 사용하는 프록시와 프록시 패턴은 구분할 필요가 있음
- 일반적 프록시는 클라이언트와 사용자 사이의 대리인 역할
- 프록시 패턴은 타깃에 대한 접근 방법을 제어
- 타깃의 기능을 확장 및 추가를 하지 않지만, 클라이언트가 타깃에 접근하는 방식을 변경함
- 클라이언트가 타깃을 사용할 때, 프록시가 타깃 오브젝트를 생성하고 요청을 위임함
- 다른 서버의 오브젝트를 사용할 때 프록시 패턴 사용
- 원격 오브젝트에 대한 프록시를 만들어두고, 클라이언트는 마치 로컬에 존재하는 오브젝트를 쓰는 것 처럼 프록시를 사용할 수 있음
- 💡 이유는 프록시와 원격 오브젝트는 같은 인터페이스를 구현하기 때문임
- 또는, 특별한 상황에서 타킷에 대한 접근권한을 제어하기 우해 프록시 패턴 사용
- 프록시를 거치게 하여 특정 메소드를 사용하려고 할 때, 예외를 던져 막을 수 있음
- 구조적으로 프록시와 데코레이터는 유사함
- 다만, 프록시는 코드에서 자신이 만들거나 접근할 타깃 클래스의 정보를 아는 경우가 많음. 하지만, 프록시 패턴이라도 인터페이스를 통해 위임하게 할 수 있음
- 앞으로 타깃과 동일한 인터페이스를 구현하고, 클라이언트와 타깃 사이에 존재하며, 부가 또는 접근 제어를 담당하는 오브젝트를 모두 프록시라 하겠음
6.3.5 프록시 패턴의 문제점
- 프록시는 기존 코드에 영향을 주지 않고, 타깃의 기능 확장 또는 접근 방법을 제어할 수 있는 유용한 방법임
- 하지만, 매번 새로운 클래스를 정의해야 함
- 또한, 인터페이스에 구현해야 하는 메소드가 많을 경우 일일이 모든 메소드를 구현해야 함
- UserServiceTx에서 트랜잭션이라는 부가기능을 수행하고, 비즈니스로직은 UserServiceImpl에 위임함
- 부가기능 사이에 위임하는 부분이 존재하기 때문에, 부가기능을 재활용하는 것이 어려움
- 때문에, 다른 메소드에서 동일한 트랜잭션 부가기능을 필요하면 똑같이 구현해야 해서 코드 중복이 발생함
- 목 프레임워크 처럼 java.lang.reflect 패키지 안에 프록시를 쉽게 만들도록 하는 클래스들이 있음
6.3.6 리플렉션
- 다이내믹 프록시는 리플렉션 기능을 이용해서 프록시를 만들어줌
- 리플렉션은 자바의 코드 자체를 추상화해서 접근하도록 만든것임
- 자바의 모든 클래스는 클래스 자체에 구성정보를 담은 Class 타입의 오브젝트를 하나씩 가지고 있음
- “클래스이름.class” 또는 getClass()를 통해 Class 타입의 오브젝트를 가져올 수 있음
- 클래스 오브젝트를 이용하면 클래스 코드에 대한 메타정보를 가져오거나 오브젝트를 조작할 수 있음
- 클래스 이름, 상속한 클래스, 타입, 필드 등의 정보를 알 수 있음
- 리플렉션 API 중에서 메소드에 대한 정의를 담은 Method 인터페이스를 이용하면 메소드를 호출할 수 있음
6.3.7 Method 인터페이스
- 메소드에 대한 자세한 정보를 담고 있음
- 스트링이 가진 메소드 중에서 “length”라는 이름을 갖고 있고, 파라미터가 없는 메소드 정보를 가져오는 코드는 다음과 같음
Method lengthMethod = String.class.getMethod("length");
- 특정 오브젝트의 메소드를 실행할 수 있음
- Method 인터페이스에 정의된 invoke()를 사용하면 됨
- invoke()는 메소드를 실행시킬 대상 오브젝트(obj)와 파라미터 목록(args)를 받아 메소드를 호출한 뒤에 그 결과를 Object 타입으로 돌려줌
public Object invoke(Object obj, Object... args)
int length = lengthMethod.invoke(name) // int length = name.length();
public class ReflectionTest {
@Test
public void invokeMethod() throws Exception {
String name = "Spring";
//length()
assertThat(name.length(), is(6));
Method lengthMethod = String.class.getMethod("length");
assertThat((Integer)lengthMethod.invoke(name), is(6));
//charAt()
assertThat(name.charAt(0), is('S'));
Method charAtMethod = String.class.getMethod("charAt", int.class);
assertThat((Character)charAtMethod.invoke(name, 0), is('S'));
}
}
6.3.8 프록시 클래스
- 프록시를 적용할 간단한 타킷 클래스와 인터페이스를 만들것임
- 해당 코드는 인터페이스의 모든 메소드 구현 및 위임과, 부가기능이 모든 메소드에 중복돼서 나타남
- 즉, 프록시 적용의 일반적인 두 가지의 문제점 모두 보유
public interface Hello {
String sayHello(String name);
String sayHi(String name);
String sayThankYou(String name);
}
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;
}
}
public class HelloUppercase implements Hello {
Hello hello;
public HelloUppercase(Hello hello) {
this.hello = hello;
}
//toUpperCase()가 모든 메소드에서 중복. 즉, 부가기능이 중복됨
@Override
public String sayHello(String name) {
return this.hello.sayHello(name).toUpperCase();
}
@Override
public String sayHi(String name) {
return this.hello.sayHi(name).toUpperCase();
}
@Override
public String sayThankYou(String name) {
return this.hello.sayThankYou(name).toUpperCase();
}
}
public class ReflectionTest {
...
@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"));
//프록시를 통한 타깃 접근
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"));
}
}
6.3.9 다이내믹 프록시
- 다이내믹 프록시는 프록시 팩토리에 의해 런타임 시 다이내믹하게 만들어지는 오브젝트
- 다이내믹 프록시는 기존의 프록시처럼 타깃의 인터페이스와 동일한 인터페이스 구현
- 프록시 팩토리에게 인터페이스 정보만 제공하면 해당 인터페이스를 구현한 클래스의 오브젝트 자동 생성함
- 부가기능은 InvocationHandler를 구현한 오브젝트에서 직접 작성함
- InvocationHandler는 invoke() 메소드만 가지는 인터페이스임
- invoke()는 리플렉션의 Method 인터페이스를 파라미터로 받음
- 다이내믹 프록시 오브젝트는 클라이언트의 모든 요청을 리플렉션 정보로 변환해서 invoke() 메소드에 전달
- 모든 요청이 invoke()로 집중되기 때문에 중복되는 기능 효과적 제공가능
- 리플랙션은 Method와 파라미터 정보가 있으면 메소드를 호출할 수 있음
Method charAtMethod = String.class.getMethod("charAt", int.class);
assertThat((Character)charAtMethod.invoke(name, 0), is('S'));
- 리플렉션에는 메소드와 파라미터 정보(메타정보)를 모두 갖기있기 때문에 타깃 오브젝트 메소드 호출 가능
- 즉, InvocationHandler구현 오브젝트가 타깃 오브젝트 레퍼런스를 가지고 있으면 리플렉션으로 간단히 위임 코드를 만들수 있음
6.3.10 다이내믹 프록시 적용
- 다이내믹 프록시로부터 요청을 전달받으려면 InvocationHandler를 구현 해야함
- 메소드는 invoke() 하나 뿐임
- 프록시가 제공하려는 부가기능인 리턴 값을 대문자 변경
- 다이내믹 프록시 생성은 Proxy 클래스의 newProxyInstance() 스태픽 팩토리 메소드를 사용하면 됨
- 첫 번째 파라미터는 클래스 로더 제공
- 두 번째 파라미터는 프록시가 구현해야 할 인터페이스. 다이내믹 프록시는 한 번에 하나 이상의 인터페이스를 구현할 수 있어 배열 사용
- 세 번째 파라미터는 InvocationHandler 구현 오브젝트
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(); //부가기능
}
}
public class ReflectionTest {
@Test
public void simpleProxy(){
...
//프록시를 통한 타깃 접근
Hello proxiedHello = (Hello) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] {Hello.class},
new UppercaseHandler(new HelloTarget())
);
assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
assertThat(proxiedHello.sayThankYou("Toby"), is("THANK YOU TOBY"));
}
}
- 위에서 구현한 UppercaseHandler는 하나의 메소드만 구현하면서 재사용할 수 있음.
- 즉, 기존 프록시 패턴의 수 많은 메소드 구현과 기능 중복을 해결함
6.3.11 다이내믹 프록시의 확장
- 다이내믹 프록시를 사용함으로써 인터페이스의 모든 메소드를 구현안해도 됨
- 다이내믹 프록시가 만들어질 때 인터페이스의 메소드가 자동적으로 추가되고, 부가기능은 invoke()에서 처리됨
- UppercaseHandler는 모든 메소드의 리턴 타입이 스트링이라 가정하고 있음
- 만약 스트링이 아닌 리턴타입을 갖는 메소드가 추가되면 런타임 시 캐스팅 오류가 발생
- 리플렉션은 매우 유연하고 막강해 주의 깊게 사용해야 함
- 따라서, Method를 이용한 타깃 오브젝트의 메소드 호출후 리턴타입을 확인하는 작업 필요
- 또한, InvocationHandler 는 타깃의 종류 상관없이 적용 가능함
- 어쩌피 리플렉션의 Method 인터페이스를 이용해 타깃 메소드를 호출하기 때문임
- 즉, 타깃의 타입을 Hello가 아닌 Object로 지정해도 됨
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);
if(ret instanceof String)
return (String)((String) ret).toUpperCase();
else
return ret;
}
}
- 리턴 타입 뿐만이 아니라, 메소드 이름으로 조건을 걸 수 있음
- Method 파라미터에서 메소드 이름을 가져와 확인하면 됨
public class UppercaseHandler implements InvocationHandler {
...
@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("say"))
return (String)((String) ret).toUpperCase();
else
return ret;
}
}
6.3.12 다이내믹 프록시를 이용한 트랜잭션 부가기능
- 기존의 UserServiceTx는 인터페이스의 메소드를 모두 구현해야 함
- 또한, 트렌잭션이 필요한 메소드마다 트랜잭션 부가기능 코드가 중복되서 나타남
- 다이내믹 프록시를 사용하여 위 두 가지 문제를 해결
- InvocationHandler를 구현한 TransactionHandler는 요청을 위임할 타깃을 Object 타입으로 DI로 제공받음
- 따라서 UserServiceImpl 외에 트랜잭션이 필요한 곳을 타깃으로 할 수 있음
- pattern 변수를 통해 해당 패턴으로 시작하는 조건을 걸 수 있음
- 리플렉션에서 발생하는 예외가 발생하면 InvocationTargetException로 포장되서 던져지기 때문에 해당 예외를 받은 후 getTargetException() 으로 예외를 가져와야 함
public class TransactionHandler implements InvocationHandler {
private Object target;
private PlatformTransactionManager transactionManager;
private 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;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if(method.getName().startsWith(pattern)){
return invokeTransaction(method, args);
}else{
return method.invoke(target, args);
}
}
private Object invokeTransaction(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();
}
}
}
public class UserServiceTest {
...
@Test
public void upgradeAllOrNothing() {
UserServiceImpl.TestUserService testUserService =
new UserServiceImpl.TestUserService(users.get(3).getId());
testUserService.setUserDao(this.userDao);
testUserService.setMailSender(this.mailSender);
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
);
userDao.deleteAll();
for (User user : users) userDao.add(user);
try {
txUserService.upgradeLevels(); //txHandler를 통한 upgradeLevels()실행
fail("TestUserServiceException expected");
} catch (UserServiceImpl.TestUserServiceException e) {
}
checkLevelUpgraded(users.get(0), false);
}
}
6.3.13 다이내믹 프록시를 위한 팩토리 빈
- TransactionHandler와 다이내믹 프록시를 스프링 DI를 통해 사용할 수 있게 해야함
- 스프링 빈은 일반적으로 클래스 이름과 프로퍼티로 정의됨
- 스프링은 지정된 클래스 이름을 가지고 리플렉션을 이용해서 해당 클래스의 오브젝트를 만듦
- Class의 newInstance()는 클래스의 파라미터가 없는 생성자를 호출하고, 그 결과 생성되는 오브젝트를 돌려주는 리플렉션 API임
Date now = (Date) Class.forName("java.util.Date").newInstance();
- 스프링은 내부적으로 리플렉션 API를 이용해서 빈 정의에 나오는 클래스 이름을 가지고 빈 오브젝트를 생성함
- 다이내믹 프록시는 위 방법으로 프록시 오브젝트가 생성되지 않음
- 클래스 자체도 내부적으로 다이내믹하게 새로 정의해서 사용해 다이내믹 프록시 오브젝트의 클래스가 어떤 클래스인지 알지 못함
- 때문에, 일반적인 방법으로 다이내믹 프록시는 스프링 빈에 등록할 방법이 없음
- 다이내믹 프록시는 Proxy 클래스의 스태틱 메소드인 newProxyInstance()만 사용해 만들 수 있음
6.3.14 팩토리 빈
- 사실 스프링은 클래스의 정보를 가지고 빈을 만들 수 있는 여러가지 방법을 제공하고 있음
- 이에 대한 대표적인 예시가 팩토리 빈임
- 팩토리 빈이란, 스프링을 대신해서 오브젝트의 생성로직을 담당하는 특별한 빈임
- 팩토리 빈을 만드는 방법은 여러가지가 있지만, 가장 간단한 방법은 FactoryBean이라는 인터페이스를 구현하는 것임
- FactoryBean를 구현한 클래스를 스프링 빈으로 등록하면 팩토리 빈으로 동작함
//스프링 내부에 존재하기 때문에 구현X
package org.springframework.beans.factory;
public Interface FactoryBean<T>{
T getObject() throws Exception; //빈 오브젝트를 생성해서 돌려줌
Class<? extends T> getObjectType(); //생성되는 오브젝트의 타입을 알려줌
boolean isSingleton(); //getObject()가 싱글톤인지 알려줌
}
6.3.15 다이내믹 프록시를 만들어주는 팩토리 빈
- 다이내믹 프록시 빈은 Proxy의 newProxyInstance()를 통해서만 생성이 가능
- 때문에, 다이내믹 프록시 오브젝트는 일반적으로 스프링에 등록 불가
- 대신, 팩토리 빈을 사용하면 다이내믹 프록시 오브젝트를 스프링 빈에 등록 가능
- getObject()에 프록시 오브젝트를 만들어주는 코드를 넣으면 됨
- 스프링 빈에 팩토리 빈과 UserServiceImpl만 등록
- 팩토리 빈에 다이내믹 프록시가 위임할 타깃인 UserServiceImpl의 레퍼런스를 프로퍼티를 통해 DI 받아야 함
- 다이내믹 프록시와 함께 생성할 TransactionHandler에게 타깃 오브젯트를 전달해야하기 때문임
6.3.16 트랜잭션 프록시 팩토리 빈
- 범용적으로 사용하기 위해 FactoryBean의 타입을 Object로 할당
- 다이내믹 프록시를 생성할 때 구현하는 인터페이스를 serviceInterface 변수로 두고, Class<?>를 통해 UserService 외의 인터페이스를 가진 타깃에도 적용할 수 있게 함
- xml에서 Class<?> 타입은 value로 넣으면 됨
- 그러면 스프링이 Class 오브젝트로 자동 변환함
public class TxProxyFactoryBean implements FactoryBean<Object> {
Object target;
PlatformTransactionManager transactionManager;
String pattern;
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;
}
@Override
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
);
}
@Override
public Class<?> getObjectType() {
return serviceInterface;
}
@Override
public boolean isSingleton() {
return false;
}
}
<beans
...>
<bean id="userService" class="com.ksb.spring.TxProxyFactoryBean">
<property name="target" ref="userServiceImpl"/>
<property name="transactionManager" ref="transactionManager"/>
<property name="pattern" value="upgradeLevels"/>
<property name="serviceInterface" value="com.ksb.spring.UserService"/>
</bean>
</beans>
6.3.17 트랜잭션 프록시 팩토리 빈 테스트
- 추후에 내용 보강
public class UserServiceTest {
@Autowired
ApplicationContext context;
@Test
@DirtiesContext
public void upgradeAllOrNothing() throws Exception {
UserServiceImpl.TestUserService testUserService =
new UserServiceImpl.TestUserService(users.get(3).getId());
testUserService.setUserDao(this.userDao);
testUserService.setMailSender(this.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 expected");
} catch (UserServiceImpl.TestUserServiceException e) {
}
checkLevelUpgraded(users.get(0), false);
}
}
6.3.18 설정 변경을 통한 트랜잭션 기능 부가
- TransactionHandler를 이용하는 다이내믹 프록시를 생성해주는 TxProxyFactoryBean은 코드의 수정 없이도 다양한 클래스에 적용할 수 있음
- 타깃 오브젝트에 맞는 프로퍼티 정보를 설정해서 빈으로 등록해주기만 하면 됨
- 하나 이상의 TxProxyFactoryBean을 동시에 빈으로 등록해도 상관없음
6.3.19 프록시 팩토리 빈 방식의 장점과 한계
- 장점
- 타깃 인터페이스를 구현하는 클래스를 일일이 만들지 않아도 됨
- 부가기능 코드 중복 제거
- 한계
- 부가기능은 메소드 단위로만 동작하기 때문에 여러 클래스에 공통적인 부가기능을 제공하는 방법은 불가능하여 프록시 팩토리 빈의 설정이 중복될 가능성이 높음
- TransactionHandler 오브젝트가 프록시 팩토리 빈 개수만큼 만들어짐