ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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를 이용하여 편하게 위임
        • 타깃 코드에 손대지 않고, 클라이언트가 호출하는 방법도 변경하지 않은 채로 새로운 기능을 추가할 때 유용한 방법
        • 클라이언트 --> 여러 데코레이터(deco1 --> deco2) --> 타깃
      • 프록시 패턴
        • 프록시를 사용하는 방법 중에서 타깃에 대한 접근 방법을 제어하려는 목적을 가진 경우
        • 프록시 패턴의 프록시는 타깃의 기능을 확장하거나 추가하지 않고, 클라이언트가 타깃에 접근하는 방식을 변경
        • 타깃 오브젝트를 생성하기가 복잡하거나 당장 필요하지 않은 경우, 필요한 시점까지 오브젝트를 생성하지 않는 것이 좋지만, 타깃 오브젝트에 대한 레퍼런스가 미리 필요한 경우가 있을 때 프록시 패턴을 사용
          • 프록시의 메소드를 통해 타깃을 사용하려고 시도할 때, 프록시가 타깃 오브젝트를 생성하고 요청을 위임
          • 많은 작업이 진행되는 경우, 생성을 최대한 늦춤으로써 장점을 가짐
        • 리모팅 기술을 사용해 다른 서버에 존재하는 오브젝트를 사용하는 경우에도 장점
        • 특별한 상황에 타깃에 대한 접근권한을 제어할 때도 장점
          • 수정 가능한 오브젝트를 읽기 전용으로만 동작하게 해야하는 경우, 오브젝트 프록시를 만들어서 read 이외에 예외를 발생하도록 구현
            • Collections의 unmodifiableCollection()을 통해 만들어지는 오브젝트가 대표 예시
        • 클라이언트 --> 접근 제어 프록시 --> 여러 데코레이터 --> 타깃
    • 다이내믹 프록시(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() 스태틱 팩토리 메소드를 통해서만 생성 가능
      • 팩토리 빈
        • 스프링을 대신해서 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈
        • 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 접근 규약을 위반할 수 있기 때문
          • 하지만, 스태틱 메소드를 통해 오브젝트가 만들어져야 하는 중요한 이유가 있을 것이기 때문에 강제로 생성하면 위험
      • 팩토리 빈의 설정
        • FactoryBean 인터페이스를 구현한 클래스를 스프링 빈으로 만들어두면 getObject()라는 메소드가 생성해주는 오브젝트가 실제 빈의 오브젝트로 대치됨
        • 팩토리 빈이 만들어주는 빈 오브젝트가 아니라 팩토리 자체를 가져오고 싶은 경우, &를 빈 이름 앞에 붙여주면 팩토리 빈 자체를 반환함
          • @Test
            public void getFactoryBean() throws Exception {
                Object factory = context.getBean("&Message");
                assertThat(factory, is(MessageFactoryBean.class));
            }
      • 다이내믹 프록시를 만들어주는 팩토리 빈
        • 팩토리 빈은 다이내믹 프록시가 위임할 타깃 오브젝트인 UserServiceImpl에 대한 레퍼런스를 프로퍼티를 통해 DI 받아둬야함
          • TransactionHandler에게 타깃 오브젝트를 전달해줘야 하기 때문
          • TransactionHandler를 생성할 때 필요한 정보를 팩토리 빈의 프로퍼티로 설정해뒀다가 다이내믹 프록시를 만들면서 전달
      • 트랜잭션 프록시 팩토리 빈
        • 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);
                }
            }
    • 프록시 팩토리 빈 방식의 장점과 한계
      • 부가기능을 가진 프록시를 생성하는 팩토리 빈을 만들어두면 타깃의 타입에 상관없이 재사용
      • 프록시 팩토리 빈의 재사용
        • UserService 외에 트랜잭션 경계설정 기능을 부여해줄 필요가 있는 클래스가 있을 경우, 앞서 만들었던 TxProxyFactoryBean을 적용
        • 코드 한 줄 만들지 않고 기존 코드에 부가적인 기능을 추가할 수 있음
      • 프록시 팩토리 빈의 장점
        • 데코레이터 패턴이 적용된 프록시의 문제점 두 가지
          • 프록시를 적용할 대상이 구현하고 있는 인터페이스 구현 프록시 클래스를 일일이 만들어야함
          • 부가적인 기능이 여러 메소드에 반복적으로 나타나서 코드 중복 발생
        • 프록시 팩토리 빈은 위의 두 문제점을 해결
          • 팩토리 빈 + DI를 통해 다양한 타깃 오브젝트 적용 및 중복코드 제거
      • 프록시 팩토리 빈의 한계
        • 프록시를 통해 타깃에 부가기능을 제공하는 것은 메소드 단위로 일어남
        • 한 번에 여러 클래스에 공통 부가기능을 제공하기에 어려움
          • 데코레이터를 이용하여 여러 메소드에 부가기능을 한번에 제공하는 것이 가능했지만, 팩토리 빈에서는 불가능해짐
        • XML 설정 라인이 대폭 증가
          • 이 부분은 Spring Boot + Java Config로 인해 현재는 문제 X
        • 타깃과 인터페이스만 다른 비슷한 설정 반복
        • TransactionHandler 오브젝트가 프록시 팩토리 빈 개수만큼 만들어짐
    반응형

    댓글

Designed by Tistory.