You will be fine

<토비의스프링> 6.1~6.3 AOP

by BFine
반응형

6.1 트랜잭션 코드 분리

  • 메소드 분리

    • 비즈니스 로직과 얽혀 있는게 보기 좋지 않다

        public void upgradeLevels() throws Exception {
           TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
      
            try {
                List<User> users = userDao.getAll(); // 1
                for (User user : users) {
                    if (canUpgradeLevel(user)) {
                        upgradeLevel(user);
                    }
                }
      
                this.transactionManager.commit(status);
            } catch (Exception e) {
                this.transactionManager.rollback(status);
                throw e;
            }
        }
      
    • 로직 1번부분은 메서드로 분리 가능하다

    • but 트랜잭션 코드가 없는 것 처럼 깔끔하게 하고싶다

    • 트랜잭션 경계설정을 담당 하는 코드를 외부로 빼내자

  • DI 적용을 이용한 트랜잭션 분리

    • p.406의 구조처럼 UserService 인터페이스를 만들고 관심사를 분리하여 구현

        public class UserServiceTx implements UserService {
          UserService userService;
             PlatformTransactionManager transactionManager;
      
            public void setTransactionManager(PlatformTransactionManager transactionManager) {
                this.transactionManager = transactionManager;    
            }
            //DI
            public void setUserService(UserService userService) {
                this.userService = userService;
            }
      
            public void upgradeLevels() {
                TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
      
                try {
                                // 기능을 위임한다.
                    userService.upgradeLevels();
      
                    this.transactionManager.commit(status);
                } catch (RuntimeException e) {
                    this.transactionManager.rollback(status);
                    throw e;
                }
            }
        }
    • 로직을 이용하려 할때

      1. 먼저 트랜잭션을 담당하는 객체가 사용돼서 트랜잭션 작업을 진행
      2. 이후 비즈니스 로직이 관련된 작업을 수행
    • 장점

      • UserServiceImpl 코드를 작성할때 트랜잭션을 신경쓰지않아도됨
      • 비즈니스 로직에 대한 테스트를 손쉽게 만들어냄

6.2 고립된 단위테스트

  • 복잡한 의존관계 속 테스트

    • p.414 그림 처럼 복잡하게 의존이 얽혀있음
    • 테스트 대상을 다른클래스, 서버에 종속되지 않도록 고립시켜야함
    • Mock 객체 이외에 필요하지 않은 의존 객체와 서비스를 모두 제거
  • 단위테스트와 통합테스트

    • 테스트 클래스를 의존 객체나 외부 사용하지 않도록 고립하여 테스트 하는 것을 단위테스트
    • 두개 이상의 다른 객체가 연동하여 테스트, 외부 DB 등 참여하는 테스트는 통합테스트
  • Mock 프레임워크

    • 목 클래스를 일일이 준비 할 필요 X

      // 스테틱 메소드 호출
      UserDao mockUserDao = mock(UserDao.class)
      
      //스텁 기능 추가, getAll이 호출됐을때 users가 리턴
      when(mockUserDao.getAll()).thenReturn(this.users);
      
      // 몇번 호출됐는지 검증
      verify(mockUserDao, time(2)).update(any(User.class));

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

  • 프록시

    • 위의 문제는 핵심기능을 가진 클래스를 직접 사용하면 부가기능 적용기회가 없어짐

    • 부가기능은 마치 핵심기능을 가진 클래스인 것 처럼 꾸며야함

        클라이언트 --> 핵심기능 인터페이스  
                            부가기능  ---> 핵심기능 인터페이스
                                                 핵심기능 
    • 사용하는 대상인 것 처럼 위장해서 요청을 받아주는 것을 프록시 (대리자) , 실제 객체는 타깃

    • 프록시 사용목적

      1. 클라이언트가 타깃에 접근하는 방법 제어
      2. 타깃에 부가기능 부여
  • 데코레이터 패턴

    • 타깃에 부가적인 기능을 런타임시에 동적으로 부여해주기 위해 프록시를 사용

    • 프록시가 한개로 제한되지 않고 고정시킬 필요도 없음

      ⇒ 같은 인터페이스를 구현한 타깃과 여러 개의 프록시 사용 가능
    • 다음 위임 대상은 인터페이스로 선언, 생성자나 수정자를 통해 외부에서 런타임시 주입

  • 프록시패턴

    • 프록시는 클라이언트와 사용 대상사이에 대리역활을 맡은 객체를 두는 방법

    • 프록시패턴은 프록시 사용 방법 중에 타깃에 대한 접근 방법을 제어

    • 타깃의 기능을 확장하거나 추가하지 않는다

      ⇒ 대신 클라이언트가 타깃에 접근하는 방식을 변경
      Collections.unmodifiableCollection()// 접근권한 제어
      // 특정레이어로 넘어가는 경우 읽기 전용 등
    • 클라이언트에게 타깃의 레퍼런스 대신 프록시를 넘겨주고 사용할때 프록시가 타깃 생성

    • 프록시는 만들거나 접근해야하는 타깃 클래스 정보를 알아야함

  • 다이나믹 프록시

    • reflect 패키지에 프록시를 손쉽게 만들어주는 클래스가 있음

    • 프록시가 만들기 번거러운 이유

      1. 타깃 인터페이스를 만들고 위임하는 코드를 만들기 번거로움

        ⇒ 부가기능 필요없는 메소드도 만들어야하고, 타깃 메소드 변경시 바꿔줘야함

        1. 부가 코드 중복 가능성 발생
  • 리플랙션

    • 자바의 코드 자체를 추상화 해서 접근하도록 만든 것

    • 자바의 모든 클래스는 자체 구성정보를 담은 Class타입 객체를 가지고 있음

       ⇒ 메타정보 확인, 객체 조작 가능
  • 프록시 클래스

    • 예시

        public class HelloUppercase implements Hello {
            Hello hello; // 프록시 추가될수 있으니 인터페이스로 접근 
      
            public HelloUppercase(Hello hello) {
                this.hello = hello
            }
      
            public String sayHello(String name) {
                return hello.sayHello(name).toUpperCase(); // 부가기능
            }
              public String sayHelloHi(String name) {
                return hello.sayHelloHi(name).toUpperCase(); // 부가기능
            }
        }
      
        Hello proxHello = new HelloUppercase(new HelloTarget());
      
    • 문제점

      • 인터페이스의 모든 메소드를 구현해 위임
      • 부가기능이 모든 메소드에 중복
  • 다이나믹 프록시 적용(p.441 그림)

    • 프록시 팩토리에 의해 런타임시 다이나믹 하게 만들어짐

    • 클라이언트는 다이나믹 프록시 객체를 타깃 인터페이스에 사용

    • invokeHandler

        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(), 
                        // 다이나믹 프록시가 정의되는 클래스 로더 지정
                new Class[] { Hello.class }, 
                        //구현할 인터페이스, 하나이상
                new UppercaseHandler(new HelloTarget())); 
                        //부가기능 위임 기능
        }
  • 트랜잭션 InvocationHandler

      public class TransactionHandler implements InvocationHandler {
          private Object target;
          private PlatformTransactionManager transactionManager;
          private String pattern;
    
          public void setXXX(Object target) {
               . . .
          }
    
          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);
              }
          }
    
          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);
                  return e.getTargetException();
              }
          }
  • 다이나믹 프록시를 위한 팩토리 빈

    • 다이나믹 프록시 객체는 일반적인 스프링 빈으로는 등록 안됨

    • 스프링은 지정된 클래스 이름을 가지고 리플렉션 하여 클래스와 객체를 만듬

    • 위의 방법 이외에 빈을 만들 수 있는 여러가지 방법 제공

      • 팩토리 빈을 이용하여 인터페이스를 구현

          public interface FactoryBean<T> {
              T getObject() throws Exception; 
                      //빈 오브젝트를 생성
              Class<? extends T> getObjectType(); 
                      //생성되는 오브젝트 타입
              boolean isSingleton(); 
                      //항상 싱글톤 오브젝트인지 알려줌
          }
      • TEST.

          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.next); // 스태틱
              }
        
              public Class<? extends Message> getObjectType() {
                  return Message.class;
              }
        
              public boolean isSingleton() {
                  return false;
              }
          }
      • 팩토리빈 설정방법

        • message 빈의 오브젝트타입이 Message 타입
        • 팩토리 빈 자체를 가져오고 싶은 경우 getBean("&message") &를 붙이면 된다.
      • 다이나믹 프록시를 만들어주는 팩토리빈

        • 팩토리빈의 getObject 메소드에 다이나믹 프록시 만들어주는 코드 설정
        • 스프링 빈에는 팩토리빈과 UserServiceImpl 만 반으로 등록
          • 팩토리빈은 타깃에 대한 레퍼런스를 DI로 받아야함
      • 트랜잭션 프록시 팩토리빈

          public class TxProxyFactoryBean implements FactoryBean<Object> {
              Object target;
              PlatformTransactionManager transactionManager;
              String pattern;
              Class<?> serviceInterface;//다이나믹 프록시 생성시 필요
        
              public void setServiceInterface(Class<?> serviceInterface) {
                  this.serviceInterface = serviceInterface;
              }
              // 팩토리빈 인터페이스 구현 메서드
              public Object getObject() throws Exception {
                  TransactionHandler txHandler = new TransactionHandler();
                  txHandler.setTarget(targer);
                  txHandler.setTransactionManager(transactionManager);
                  txHandler.setPattern(pattern);
                  return Proxy.newProxyInstance(
                          getClass().getCalssLoader(),
                                       new Class[] { serviceInterface },
                          txHandler);
              }   
          }
        
          TEST
        
          TxProxyFactoryBean txbean = context.getBean("&userService",TxProxyFactoryBean.class)
          // 
          txbean.setTarget(testUserService);
          UserService txUserService = (UserService) txbean.getObject();
          // 트랜잭션 다이내픽 프록시 객체를 다시 생성한다.
  • 프록시 팩토리빈 방식의 장점과 한계

    • 재사용

      • TxProxyFactoryBean은 코드 수정없이 다양한 클래스에 적용
    • 장점

      1. 타깃 인터페이스를 구현하는 클래스를 일일히 만드는 번거로움 제거
      2. 모든 메서드에 부가기능 중복제거
    • 한계

      • 한번에 여러개의 클래스에 부가기능을 제공 불가

      • 하나의 타깃에 여러 부가기능 적용시 문제

        • 프록시 팩토리빈 설정이 부가기능 개수만큼 붙어야함

          ⇒ XML 설정이 길어짐

      • TransactionHandler 오브젝트가 팩토리빈 개수만큼 만들어짐 (타깃이 달라질 경우)

반응형

블로그의 정보

57개월 BackEnd

BFine

활동하기