-
6장) 6.3 다이내믹 프록시와 팩토리 빈Java & Spring/토비의 스프링 3.1 2021. 7. 16. 18:06반응형
6장 AOP
6.3 다이내믹 프록시와 팩토리 빈
- 프록시와 프록시 패턴, 데코레이터 패턴
- 프록시
- 트랜잭션은 비즈니스 로직과는 성격이 다르기 때문에 분리, 독립
- UserServiceTx, UserServiceImpl
- 핵심 기능(비즈니스 로직)을 담은 클래스를 부가 기능을 가진 트랜잭션 클래스에서 이용
- 클라이언트가 핵심 기능을 가진 클래스를 직접 사용하지 않도록, 부가 기능을 담은 클래스가 핵심 기능인 것 처럼 위장 ~> 핵심 기능 클래스를 이용
- 실제 대상인 것 처럼 위장해서 요청을 받아주는 대리자 역할을 한다고 해서 프록시라고 부름
- 요청을 위임받아 실제 처리하는 오브젝트를 타깃 or 실체(real subject) 라고 부름
클라이언트 --> 프록시 --> 타깃
- 사용 목적
- 클라이언트가 타깃에 접근하는 것을 제어하기 위함
- 부가적인 기능을 부여하기 위함
- 트랜잭션은 비즈니스 로직과는 성격이 다르기 때문에 분리, 독립
- 데코레이터 패턴
- 타깃에 부가적인 기능을 런타임 시 다이나믹하게 부여하기 위해 프록시를 사용하는 패턴
- 컴파일 시점에서 프록시와 타깃이 연결되어 사용되는지 정해져있지 않음
- 프록시가 한 개로 제한되지 않고, 프록시가 타깃을 직접 사용하도록 고정시킬 필요 X
- 인터페이스를 구현한 타겟과 여러 프록시를 사용할 수 있음 (단계적 위임)
- 데코레이터 프록시의 다음 위임 대상은 인터페이스로 선언하고 생성자나 수정자 메소드를 통해 위임 대상을 외부에서 런타임 시에 주입받도록 구현해야함
- 자바 IO 패키지의 InputStream과 OutputStream 구현 클래스가 대표적인 예시
InputStream is = new BufferedInputStream(new FileInputStream("a.txt"));
- 스프링의 DI를 이용하여 편하게 위임
- 자바 IO 패키지의 InputStream과 OutputStream 구현 클래스가 대표적인 예시
- 타깃 코드에 손대지 않고, 클라이언트가 호출하는 방법도 변경하지 않은 채로 새로운 기능을 추가할 때 유용한 방법
클라이언트 --> 여러 데코레이터(deco1 --> deco2) --> 타깃
- 타깃에 부가적인 기능을 런타임 시 다이나믹하게 부여하기 위해 프록시를 사용하는 패턴
- 프록시 패턴
- 프록시를 사용하는 방법 중에서 타깃에 대한 접근 방법을 제어하려는 목적을 가진 경우
- 프록시 패턴의 프록시는 타깃의 기능을 확장하거나 추가하지 않고, 클라이언트가 타깃에 접근하는 방식을 변경
- 타깃 오브젝트를 생성하기가 복잡하거나 당장 필요하지 않은 경우, 필요한 시점까지 오브젝트를 생성하지 않는 것이 좋지만, 타깃 오브젝트에 대한 레퍼런스가 미리 필요한 경우가 있을 때 프록시 패턴을 사용
- 프록시의 메소드를 통해 타깃을 사용하려고 시도할 때, 프록시가 타깃 오브젝트를 생성하고 요청을 위임
- 많은 작업이 진행되는 경우, 생성을 최대한 늦춤으로써 장점을 가짐
- 리모팅 기술을 사용해 다른 서버에 존재하는 오브젝트를 사용하는 경우에도 장점
- 특별한 상황에 타깃에 대한 접근권한을 제어할 때도 장점
- 수정 가능한 오브젝트를 읽기 전용으로만 동작하게 해야하는 경우, 오브젝트 프록시를 만들어서 read 이외에 예외를 발생하도록 구현
- Collections의 unmodifiableCollection()을 통해 만들어지는 오브젝트가 대표 예시
- 수정 가능한 오브젝트를 읽기 전용으로만 동작하게 해야하는 경우, 오브젝트 프록시를 만들어서 read 이외에 예외를 발생하도록 구현
클라이언트 --> 접근 제어 프록시 --> 여러 데코레이터 --> 타깃
- 프록시
- 다이내믹 프록시(Dynamic Proxy)
- 프록시를 만드는 일이 번거로워서 등장
- 일일이 프록시 클래스를 정의하지 않고 몇 가지 API를 이용해 프록시처럼 동작하는 오브젝트를 다이나믹하게 생성
- 프록시 구성과 프록시 작성의 문제점
- 프록시의 기능
- 타깃과 같은 메소드를 구현하고 있다가 메소드가 호출되면 타깃 오브젝트로 위임
- 지정된 요청에 대한 부가기능 수행
- 프록시 구현이 번거로운 이유
- 타깃의 인터페이스를 구현하고 위임하는 코드를 작성하기 번거로움
- 타깃 인터페이스의 메소드가 추가되거나 변경될 때마다 함께 수정해야함
- 부가기능 코드가 중복될 가능성이 높음
- 트랜잭션의 경우 DB 사용하는 로직에 적용될 확률이 높음
- 위의 경우, 트랜잭션 기능을 제공하는 유사한 코드가 여러 메소드에 중복됨
- 타깃의 인터페이스를 구현하고 위임하는 코드를 작성하기 번거로움
- 프록시의 기능
- 리플렉션
- 자바 코드를 추상화해서 접근하도록 만든 것
- 다이내믹 프록시는 리플렉션 기능을 이용해서 프록시를 생성
- 자바의 모든 클래스는 그 자체의 구성정보를 담은 Class 타입의 오브젝트를 하나씩 가지고 있음
- 클래스이름.class or 오브젝트의 getClass() 메소드로 호출 가능
- 특정 클래스 정보에서 특정 이름을 가진 메소드 정보를 가져올 수 있음
Method lengthMethod = String.class.getMethod("length");
- 메소드 실행은
invoke()
메소드를 사용 int length = lengthMethod.invoke(name);
- 프록시 클래스
- 다이나믹 프록시를 이용한 프록시 생성
- 다이내믹 프록시 동작 방식
- 프록시 팩토리에 의해 런타임 시 다이내믹하게 만들어짐
- 프록시에서 필요한 부가기능 제공 코드는 직접 작성
- 부가 기능은 프록시 오브젝트와 독립적으로 InvocationHandler를 구현한 오브젝트에 담음
-
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; } }
-
public class UppercaseHandler implements InvocationHandler { Hello target; public UppercaseHandler(Hello target) { this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String ret = (String)method.invoke(target, args); return ret.toUpperCase(); } }
-
Hello proxiedHello = (Hello)Proxy.newProxyInstance( getClass().getClassLoader(), // 다이나믹 프록시 오브젝트는 Hello 인터페이스를 구현하고 있으므로 캐스팅 세이프 new Class[] { Hello.class }, // dynamic proxy 클래스 로딩에 사용될 클래스 로더 new UppercaseHandler(new HelloTarget()) // invocationHandler );
- 첫 번째 파라미터로 다이내믹 프록시가 정의되는 클래스 로더 지정
- 두 번째 파라미터로 다이내믹 프록시가 구현해야 할 인터페이스 지정(한 번에 하나 이상의 인터페이스를 구현할 수 있기 때문에 배열을 사용)
- 마지막 파라미터로 부가기능과 위임 관련 코드를 담고 있는 InvocationHandler 구현 오브젝트 제공
- 다이내믹 프록시의 확장
- Hello 인터페이스의 메소드가 증가하는 경우, 다이내믹 프록시에 자동으로 포함되고, 부가 기능은 invoke() 메소드에서 처리됨
- 타깃의 타입에 상관없이 InvocationHandler를 통해 적용 가능
-
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); return ret instanceof String ? (String)ret.toUpperCase() : ret; } }
-
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 메소드를 선별해서 부가기능을 적용하는 invoke Object ret = method.invoke(target, args); return ret instanceof String && method.getName().startsWith("say") ? (String)ret.toUpperCase() : ret; }
- 프록시를 만드는 일이 번거로워서 등장
- 다이내믹 프록시를 이용한 트랜잭션 부가기능
- 앞서 작성했던 UserServiceTx를 다이내믹 프록시 방식으로 수정
- 트랜잭션 InvocationHandler
-
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; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return Method.getName().startsWith(pattern) ? invokeInTransaction(method, args) : method.invoke(target, args); } private Object invokeInTransaction(Method method, Object[] args) throws Throwable { TransactionStatus status = this.getTransaction(new DefaultTransactionManager()); try { Object ret = method.invoke(target, args); this.transactionManager.commit(status); return ret; } catch (InvocationTargetException exception) { this.transactionmanager.rollback(status); throw exception.getTargetException(); } } }
- 요청을 위임할 타깃을 DI로 제공받고, Object로 선언
- UserServiceImpl 외에 트랜잭션 적용이 필요한 어떤 타깃 오브젝트에도 적용 가능
- UserServiceTx와 마찬가지로 트랜잭션 추상화 인터페이스인 PlatformTransactionManager를 DI 받음
- 타깃 오브젝트의 모든 메소드에 무조건 트랜잭션이 적용되지 않도록 트랜잭션을 적용할 메소드 이름의 패턴을 DI 받음
- InvocationHandler의 invoke() 메소드는 적용할 대상을 선별해서 트랜잭션을 적용하고, 아니라면 부가기능 없이 타깃 오브젝트의 메소드를 호출해서 결과를 리턴
-
- TransactionHandler와 다이내믹 프록시를 이용하는 테스트
-
@Test public void upgradeAllOrNothing() throws Exception { ... 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를 통해 사용하도록 수정
- 스프링은 내부적으로 리플렉션 API를 이용해서 빈 정의에 나오는 클래스 이름을 가지고 빈 오브젝트를 생성
- 다이내믹 프록시 오브젝트는 리플렉션을 통해 오브젝트 생성 X
- Proxy 클래스의 newProxyInstance() 스태틱 팩토리 메소드를 통해서만 생성 가능
- 스프링은 내부적으로 리플렉션 API를 이용해서 빈 정의에 나오는 클래스 이름을 가지고 빈 오브젝트를 생성
- 팩토리 빈
- 스프링을 대신해서 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈
- FactoryBean 인터페이스를 구현
-
public interface FactoryBean<T> { T getObject() throws Exception; // 빈 오브젝트를 생성해서 반환 Class<? extends T> getObjectType(); // 생성되는 오브젝트 타입 반환 bollean isSingleton(); // getObject()가 돌려주는 오브젝트가 항상 싱글톤인지 여부 반환 }
-
public class Message { String text; private Message(String text) { this.text = text; } public String getText() { return text; } public static Message newMessage(String text) { return new Message(text); } } public class MessageFactoryBean implements FactoryBean<Message> { String text; public void setText(String text) { this.text = text; } public Message getObject() throws Exception { return Message.newMessage(this.text); } public Class<? extends Message> getObjectType() { return Message.class; } public boolean isSingleton() { return false; } }
- 스프링은 private 생성자를 가진 클래스를 빈으로 등록해주면 리플렉션을 이용해 오브젝트로 만들어줌
- 리플렉션이 private 접근 규약을 위반할 수 있기 때문
- 하지만, 스태틱 메소드를 통해 오브젝트가 만들어져야 하는 중요한 이유가 있을 것이기 때문에 강제로 생성하면 위험
- 스프링은 private 생성자를 가진 클래스를 빈으로 등록해주면 리플렉션을 이용해 오브젝트로 만들어줌
- 팩토리 빈의 설정
- FactoryBean 인터페이스를 구현한 클래스를 스프링 빈으로 만들어두면 getObject()라는 메소드가 생성해주는 오브젝트가 실제 빈의 오브젝트로 대치됨
- 팩토리 빈이 만들어주는 빈 오브젝트가 아니라 팩토리 자체를 가져오고 싶은 경우,
&
를 빈 이름 앞에 붙여주면 팩토리 빈 자체를 반환함-
@Test public void getFactoryBean() throws Exception { Object factory = context.getBean("&Message"); assertThat(factory, is(MessageFactoryBean.class)); }
-
- 다이내믹 프록시를 만들어주는 팩토리 빈
- 팩토리 빈은 다이내믹 프록시가 위임할 타깃 오브젝트인 UserServiceImpl에 대한 레퍼런스를 프로퍼티를 통해 DI 받아둬야함
- TransactionHandler에게 타깃 오브젝트를 전달해줘야 하기 때문
- TransactionHandler를 생성할 때 필요한 정보를 팩토리 빈의 프로퍼티로 설정해뒀다가 다이내믹 프록시를 만들면서 전달
- 팩토리 빈은 다이내믹 프록시가 위임할 타깃 오브젝트인 UserServiceImpl에 대한 레퍼런스를 프로퍼티를 통해 DI 받아둬야함
- 트랜잭션 프록시 팩토리 빈
-
public class TxProxyFactoryBean implements FactoryBean<Object> { Object target; PlatformTransactionManager transactionManager; String pattern; // 이상 3개는 TransactionHandler 생성시 필요 Class<?> serviceInterface; // 다이내믹 프록시 생성시 필요 // set properties 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; } public boolean isSingleton() { return false; // getObject()가 매번 같은 오브젝트를 리턴하지 않음 } }
-
- 트랜잭션 프록시 팩토리 빈 테스트
- 수동 DI를 통해 직접 다이내믹 프록시를 만들었던 코드에 팩토리 빈 적용
- 타깃 Object에 대한 레퍼런스를 TransactionHandler 오브젝트가 가지고 있으므로, factory의 getObject에 의한 DI가 아닌, FactoryBean을 직접 가져와서 프록시를 생성하도록 수정
-
public class UserServiceTest { @Test @DirtiesContext public void upgradeAllOrNothing() throws Exception { TestUserService testUserService = new TestUserService(users.get(3).getId()); testUserService.setUserDao(userDao); testUserService.setMailSender(mailSender); TxProxyFactoryBean txProxyFactoryBean = context.getBean("&userService", TxProxyFactoryBean.class); txProxyFactoryBean.setTarget(testUserService); UserService txUserService = (UserService) txProxyFactoryBean.getObject(); userDao.deleteAll(); users.forEach(user -> userDao.add(user)); try { txUserService.upgradeLevels(); fail("TestUserServiceException expected"); } catch (TestUserServiceException e) { } checkLevelUpgraded(users.get(1), false); } }
- 수동 DI를 통해 직접 다이내믹 프록시를 만들었던 코드에 팩토리 빈 적용
- TransactionHandler와 다이내믹 프록시를 스프링 DI를 통해 사용하도록 수정
- 프록시 팩토리 빈 방식의 장점과 한계
- 부가기능을 가진 프록시를 생성하는 팩토리 빈을 만들어두면 타깃의 타입에 상관없이 재사용
- 프록시 팩토리 빈의 재사용
- UserService 외에 트랜잭션 경계설정 기능을 부여해줄 필요가 있는 클래스가 있을 경우, 앞서 만들었던 TxProxyFactoryBean을 적용
- 코드 한 줄 만들지 않고 기존 코드에 부가적인 기능을 추가할 수 있음
- 프록시 팩토리 빈의 장점
- 데코레이터 패턴이 적용된 프록시의 문제점 두 가지
- 프록시를 적용할 대상이 구현하고 있는 인터페이스 구현 프록시 클래스를 일일이 만들어야함
- 부가적인 기능이 여러 메소드에 반복적으로 나타나서 코드 중복 발생
- 프록시 팩토리 빈은 위의 두 문제점을 해결
- 팩토리 빈 + DI를 통해 다양한 타깃 오브젝트 적용 및 중복코드 제거
- 데코레이터 패턴이 적용된 프록시의 문제점 두 가지
- 프록시 팩토리 빈의 한계
- 프록시를 통해 타깃에 부가기능을 제공하는 것은 메소드 단위로 일어남
- 한 번에 여러 클래스에 공통 부가기능을 제공하기에 어려움
- 데코레이터를 이용하여 여러 메소드에 부가기능을 한번에 제공하는 것이 가능했지만, 팩토리 빈에서는 불가능해짐
- XML 설정 라인이 대폭 증가
- 이 부분은 Spring Boot + Java Config로 인해 현재는 문제 X
- 타깃과 인터페이스만 다른 비슷한 설정 반복
- TransactionHandler 오브젝트가 프록시 팩토리 빈 개수만큼 만들어짐
반응형'Java & Spring > 토비의 스프링 3.1' 카테고리의 다른 글
6장) 6.5 스프링 AOP (0) 2021.07.23 6장) 6.4 스프링의 프록시 팩토리 빈 (0) 2021.07.16 6장) 6.1 트랜잭션 코드의 분리 ~ 6.2 고립된 단위 테스트 (0) 2021.07.11 5장) 5.3 서비스 추상화와 단일 책임 원칙 ~ 5.5 정리 (0) 2021.07.02 5장) 5.2 트랜잭션 서비스 추상화 (0) 2021.06.26 - 프록시와 프록시 패턴, 데코레이터 패턴