콘텐츠로 이동

2021 06 29

2021-06-29

토비의 스프링 6장. AOP

  • 6.1 트랜잭션 코드의 분리 - UserService는 비즈니스 로직만 나두고, 트랜잭션에 관련된 코드는 클래스 밖으로 뽑아내보자 - 이후 이를 간접적으로 접근토록 함 (DI) - UserServiceTest ---> UserServiceTx ---> UserServiceImpl 의 의존관계 설정 - 트랜잭션 경계설정 코드를 분리함으로써,,, 1. 비즈니스 로직을 담당하는 UserServiceImpl의 코드 작성시, 트랜잭션 걱정 필요 없음 2. 비즈니스 로직에 대한 테스트를 손쉽게 만들 수 있음
  • 6.2 고립된 단위 테스트 - UserService를 테스트 하려고 보니까 의존관계 따라 등장하는 객체, 서비스, 환경 등이 모두 합쳐져 테스트 대상이 됨 - 테스트 준비가 어렵고, 동일한 결과 보장이 어려우며, 테스트 작성&실행 싸이클 느려짐 - 따라서 테스트를 의존 대상으로 부터 분리해 "테스트를 위한 대역"을 사용하자 - UserDao에 의존적인 UserService의 테스트 메서드
        @Test
        @DirtiesContext
        public void upgradeLevelsWithMailSending() {
            //DB 테스트 데이터 준비
            users.forEach(user -> userDao.add(user));
    
            //메일 발송 여부 확인을 위해 목 오브젝트 DI
            final MockMailSender mockMailSender = new MockMailSender();
            userServiceImpl.setMailSender(mockMailSender);
    
            //테스트 대상 실행
            userServiceImpl.upgradeLevels();
    
            //DB에 저장된 결과 확인        
            checkLevelUpgraded(users.get(0), false);
            checkLevelUpgraded(users.get(1), true);
            checkLevelUpgraded(users.get(2), false);
            checkLevelUpgraded(users.get(3), true);
            checkLevelUpgraded(users.get(4), false);
    
            //목 오브젝트를 이용한 결과 확인
            final List<String> requests = mockMailSender.getRequests();
            assertThat(requests.size()).isEqualTo(2);
            assertThat(requests.get(0)).isEqualTo(users.get(1).getEmail());
            assertThat(requests.get(1)).isEqualTo(users.get(3).getEmail());
        }
    
      - UserDao를 테스트 대상의 코드가 정상적으로 수행되도록 도와주기만 하는 "스텁"이 아니라, 부가적인 검증까지 하는 "목"으로 만듦
          - Mock 객체 만들 때, 사용하지 않는 메서드는 UnsupportedOperationException 던져주자. 
      ```java
          @Test
          @DirtiesContext
          public void upgradeLevelsWithMailSendingOptionWithMockDao() {
              //DB 테스트 데이터 준비
              final MockUserDao mockUserDao = new MockUserDao(this.users);
    
              //메일 발송 여부 확인을 위해 목 오브젝트 DI
              final MockMailSender mockMailSender = new MockMailSender();
              userServiceImpl.setMailSender(mockMailSender);
              userServiceImpl.setUserDao(mockUserDao);
    
              //테스트 대상 실행
              userServiceImpl.upgradeLevels();
    
              //DB에 저장된 결과 확인
              final List<User> updated = mockUserDao.getUpdated();
              assertThat(updated.size()).isEqualTo(2);
              checkUserAndLevel(updated.get(0), "성훈", Level.SILVER);
              checkUserAndLevel(updated.get(1), "영준", Level.GOLD);
    
              //목 오브젝트를 이용한 결과 확인
              final List<String> requests = mockMailSender.getRequests();
              assertThat(requests.size()).isEqualTo(2);
              assertThat(requests.get(0)).isEqualTo(users.get(1).getEmail());
              assertThat(requests.get(1)).isEqualTo(users.get(3).getEmail());
          }
    
          private void checkUserAndLevel(User updatedUser, String expectedName, Level expectedLevel) {
              assertThat(updatedUser.getName()).isEqualTo(expectedName);
              assertThat(updatedUser.getLevel()).isEqualTo(expectedLevel);
          }
      ```
    

    - 의존 관계가 너무 많은 테스트라면, Mock 프레임워크를 사용해보자! (Mockito) - UserDao mockUserDao = mock(UserDao.class); - mock(mockUserDao.getAll()).thenReturn(this.users); - verify(mockUserDao, times(2)).update(any(User.class)); - 6.3 다이나믹 프록시와 팩토리 빈 - 부가 기능의 분리... 핵심 기능은 부가 기능을 가진 클래스의 존재 자체를 모름 - 프록시: 클라이언트가 사용하려고 하는 실제 대상인 것 처럼 위장해 클라이언트의 요청을 받아주는 것 - 특징 - 타깃과 같은 인터페이스를 구현함 - 타깃을 제어할 수 있는 위치에 있음 - 목적 - 클라이언트가 타깃에 접근하는 방법 제어 - 타깃에 부가적인 기능을 부여함 - 데코레이터 패턴 - 타깃에 부가적인 기능을 런타임 시 다이나믹하게 부여해주기 위해 프록시를 사용하는 패턴 - 하나의 타깃에 여러 개의 프록시를 사용할 수 있음 - DI를 통해 데코레이터 기능을 구현해주자! - 인터페이스를 통해 위임하는 구조이기에 어느 데코레이터에서 타깃으로 연결될지 코드레벨에서는 미리 알 수 없음 - 프록시 패턴 - 프록시를 사용하는 방법 중 타겟에 대한 접근 방법을 제어 - 프록시: 클라이언트와 사용 대상 사이에 대리 역할을 맡은 오브젝트를 두는 방법의 총칭 - 사용 예시 - 타깃 오브젝트를 생성하기가 복잡하거나, 당장 필요하지 않은 경우 꼭 필요한 시점까지 생성 안함 - 그저 레퍼런스가 필요하니, 이를 만들어 넘긴다 - Collection의 UnmodifiableCollection() - 다이나믹 프록시 - 프록시의 역할은 "위임"과 "부가작업" - 다이나믹 프록시는 프록시 팩토리에 의해 런타임 시 다이나믹하게 만들어지는 오브젝트 - 다이나믹 프록시 오브젝트는 타깃의 인터페이스와 같은 타입으로 만들어짐 - 클라이언트는 다이나믹 프록시 오브젝트를 타깃 인터페이스를 통해 사용 가능 - 다이나믹 프록시가 인터페이스 구현 클래스의 오브젝트는 만들어 주지만, 프록시로서 필요한 부가기능 제공 코드는 직접 작성해야 함 - 다이나믹 프록시로부터 요청 전달 받으려면 InvocationHandler를 구현해야함 (어떤 타깃의 종류라도 상관 없음!)

    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)ret).toUpperCase();
            }
            return ret;
        }
    }
    

          - 리플렉션을 사용하여 트랜잭션을 적용한 경우, 예외가 InvocationTargetException으로 포장되어 출력됨
      - 이후 다이나믹 프록시는 Proxy.newProxyInstance()로 생성!
          ```java
          Hello dynamicProxyHello = (Hello) Proxy.newProxyInstance(
                  getClass().getClassLoader(),
                  new Class[] {Hello.class},
                  new UppercaseHandler(new HelloTarget()));
          ```
    

    - 팩토리 빈 - DI의 대상이 되는 다이나믹 프록시 오브젝트는 일반적인 스프링 빈으로는 등록할 방법이 없다! - 내부적으로 다이나믹하게 새로 정의해서 사용하기 때문임 - FactoryBean<?>을 구현하여 팩토리 빈을 만들어주자!

    public class MessageFactoryBean implements FactoryBean<Message> {
        String text;
    
        public MessageFactoryBean(String text) {
            this.text = text;
        }
    
        @Override
        public Message getObject() {
            return Message.newMessage(text);
        }
    
        @Override
        public Class<?> getObjectType() {
            return Message.class;
        }
    
        @Override
        public boolean isSingleton() {
            return false;
        }
    }
    

      - 그리고 이걸 빈 등록을 해주면 놀랍게도 getObject()에 해당하는 친구가 빈으로 뚝딱 나옴
          ```java
          @Bean
          public MessageFactoryBean message() {
              return new MessageFactoryBean("Factory Bean");
          }
          ```
    
      - 어플리케이션 컨텍스트에서 해당 메서드로 찾으면 getObject로 빈 자체가, &메서드를 통해 찾으면 팩토리 빈 그 자체가 반환
          ```java
          @Test
          public void getMessageFromFactoryBean() {
              Object message = context.getBean("message");
              assertThat(message).isInstanceOf(Message.class);
              assertThat(((Message)message).getText()).isEqualTo("Factory Bean");
          }
    
          @Test
          public void getFactoryBean() {
              final Object factory = context.getBean("&message");
              assertThat(factory).isInstanceOf(MessageFactoryBean.class);
          }
          ```
    

    - 프록시 팩토리 빈 방식 - 장점 - 다양한 클래스에 적용 가능! - 프록시 기법 아주 빠르고 효과적이게 적용 가능 - DI를 통해 효과적인 프록시 팩토리 사용이 가능했음 - 한계 - 메서드 단위를 벗어나면? - 클래스에 공통기능을 부여하려면? - 하나의 타깃에 여러 부가기능을 적용하려고 한다면?

Test Double

  • 참고: https://woowacourse.github.io/javable/post/2020-09-19-what-is-test-double/
  • 테스트 더블: 테스트를 진행하기 어려운 경우 이를 대신해 테스트를 진행할 수 있도록 만든 객체
  • Dummy - 인스턴스화 된 객체는 필요하지만 기능은 필요하지 않은 경우 - Dummy 객체의 메서드가 호출되었을 시 정상 동작 보장 X - 객체는 전달 되지만 사용 X - 동작하지 않아도 테스트에는 영향을 미치지 않는 객체
    - ex. 인터페이스 구현하여 아무일도 안함
  • Fake - 복잡한 로직이나 객체 내부에서 필요로 하는 다른 외부 객체 동작을 단순화하여 구현한 객체 - 동작의 구현 O, but 실제 프로덕션에 적합 X - 동작은 하지만 실제 사용되는 객체처럼 정교하게 동작은 X - ex. Repository 테스트를 일급 컬렉션을 만들어서 수행하기
  • Stub - Dummy 객체가 실제로 동작하는 것 처럼 보이게 만든 객체 - 인터페이스가 최소한으로 구현된 상태 - 테스트에서 호출된 요청에 대해 "미리 준비해둔 결과" 제공 - 프로그래밍된 내용에 대해서만 준비된 결과를 제공하는 객체 - ex. Mockito의 when(함수).thenReturn(결과)
  • Spy - Stub의 역할을 가지며, 호출된 내용에 대해 약간의 정보 기록 - 동작을 지정할 수 있음 - ex. Mockito의 verify()
  • Mock - 호출에 대한 기대를 명세, 내용에 따라 동작하도록 프로그래밍 된 객체