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 - 호출에 대한 기대를 명세, 내용에 따라 동작하도록 프로그래밍 된 객체