Java & Spring/토비의 스프링 3.1

6장) 6.3 다이내믹 프록시와 팩토리 빈

Zin0_0 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 오브젝트가 프록시 팩토리 빈 개수만큼 만들어짐
반응형