-
6장) 6.1 트랜잭션 코드의 분리 ~ 6.2 고립된 단위 테스트Java & Spring/토비의 스프링 3.1 2021. 7. 11. 17:32반응형
6장 AOP
- 스프링에 적용된 가장 인기있는 AOP 적용 대상은 선언적 트랜잭션 기능
6.1 트랜잭션 코드의 분리
- 메소드 분리
- 기존에 작성했던 UserService는 트랜잭션 경계설정 코드와 비즈니스 로직 코드 간에 서로 주고받는 정보가 없음
- 비즈니스 로직 코드에서 직접 DB를 사용하지 않기 때문
- upgradeLevels 메소드에서 시작된 트랜잭션 정보는 트랜잭션 동기화 방법을 통해 DAO가 알아서 활용
- 완벽하게 독립된 코드
- 기존에 작성했던 UserService는 트랜잭션 경계설정 코드와 비즈니스 로직 코드 간에 서로 주고받는 정보가 없음
- DI를 이용한 클래스의 분리
- DI 적용을 이용한 트랜잭션 분리
- DI는 실제 사용할 오브젝트 클래스 정체를 감추고 인터페이스로 간접 접근하도록 사용
- 구현 클래스를 외부에서 변경 가능
- UserService를 인터페이스로 만들고 기존 코드를 구현 클래스로 수정
- 클라이언트와 결합이 약해지고 직접 구현 클래스에 의존하지 않아 유연한 확장 가능
-
// 기존 구조 Client -----> UserService // 수정할 구조 Client -----> UserService <---- UserServiceImpl
-
- 런타임 시 DI를 통해 적용하는 방법을 쓰는 이유는 일반적으로 구현 클래스를 바꾸며 사용하기 위함
- 한 번에 두 개의 UserService 인터페이스 구현 클래스를 동시에 사용한다면 ??
- 트랜잭션 경계설정 로직과 비즈니스 로직으로 구현 클래스를 구현
-
Client ----> UserService <----------- UserServiceImpl | | \------ UserServiceTx
- UserServiceTx는 트랜잭션 경계설정 책임만 갖고, 비즈니스 로직을 담고있는 UserServiceImpl 구현체에게 실질적인 로직 처리 작업 위임
- 한 번에 두 개의 UserService 인터페이스 구현 클래스를 동시에 사용한다면 ??
- DI는 실제 사용할 오브젝트 클래스 정체를 감추고 인터페이스로 간접 접근하도록 사용
- UserService 인터페이스 도입
-
public interface UserService { void add(User user); void upgradeLevels(); }
-
public class UserServiceImpl implements UserService { UserDao userDao; MailSender mailSender; public void upgradeLevels() { userDao.getAll() .filter(user -> canUpgradeLevel(user)) .forEach(user -> upgradeLevel(user)); } }
- upgradeLevelsInternal()로 분리했던 코드를 다시 원래대로 upgradeLevels에 추가
-
- 분리된 트랜잭션 기능
- 비즈니스 트랜잭션 처리를 담은 UserServiceTx 생성
-
@AllArgsConstructor public class UserServiceTx implements UserService { UserService userService; PlatformTransactionManager transactionManager; public void add(User user) { this.userService.add(user); // DI 받은 UserService에 기능 위임 } public void upgradeLevels() { TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); try { userService.upgradeLevels(); // DI 받은 UserService에 기능 위임 this.transactionManager.commit(status); } catch (RuntimeException e) { this.transactionManager.rollback(status); throw e; } } }
- 트랜잭션 적용을 위한 DI 설정
- 의존 관계
- Client ----> UserServiceTx ----> UserServiceImpl
- 의존 관계
- 트랜잭션 분리에 따른 테스트 수정
- @Autowired
- 기본적으로 타입이 일치하는 빈을 찾아 주입
- 타입으로 하나의 빈을 결정할 수 없는 경우는 필드 이름을 이용해서 찾아 주입
- 따라서, 기존 코드대로 UserService에 @Autowired를 걸어두면, 아이디가 userService인 빈이 주입됨
- 기본적으로 타입이 일치하는 빈을 찾아 주입
- UserServiceTest는 두 개의 빈이 필요
- Mock 객체로 만든 MailSender 구현체를 UserServiceImpl에 직접 DI 해줘야함
-
//UserServiceTest @Autowired UserServiceTx; @Autowired UserServiceImpl @Test public void upgradeLevels() throws Exception { // 메일링 검증 ... MockMailSender mockMailSender = new MockMailSender(); userServiceImpl.setMailSender(mockMailSender); } @Test public void upgardeAllOrNothig() { // 트랜잭션 검증 TestUserService testUserService = TestUserService(users.get(3).getId()); ... UserServiceTx txUserService = new UserServiceTx(); txUserService.setTranscationManager(transactionManager); txUserSerive.setUserService(testUserService); ... }
- @Autowired
- 트랜잭션 경계 코드 분리의 장점
- 비즈니스 로직을 담당하는 코드는 트랜잭션과 같은 기술적인 내용에 신경쓰지 않아도 됨
- 언제든 트랜잭션 도입이 가능
- 비즈니스 로직에 대한 테스트를 쉽게 만들 수 있음
- 비즈니스 로직을 담당하는 코드는 트랜잭션과 같은 기술적인 내용에 신경쓰지 않아도 됨
- DI 적용을 이용한 트랜잭션 분리
6.2 고립된 단위 테스트
- 가능한 작은 단위로 쪼개서 테스트하는 것이 가장 좋음
- 테스트가 실패했을 때 원인을 찾기 쉬움
- 테스트 의도와 내용이 분명해지고 만들기 쉬움
- 복잡한 의존관계 속의 테스트
- UserService의 경우, 세 가지 타입의 의존 오브젝트가 필요
- UserDao, MailSender, PlatformTransactionManager
- UserServiceTest는 UserService는 비즈니스 로직을 테스트
- 위의 세 의존관계를 갖는 오브젝트들이 테스트 중 같이 실행되는 문제점 존재
- 위의 세 오브젝트들이 다른 많은 리소스에 의존하고 있어 더 큰 문제
- 환경이 달라지면 다른 테스트 결과 발생
- 수행속도가 느리고, UserService의 책임이 아닌 문제점을 파악할 가능성이 큼
- UserService의 경우, 세 가지 타입의 의존 오브젝트가 필요
- 테스트 대상 오브젝트 고립시키기
- 테스트 대역 사용(Test Double)
- 테스트 스텁, 목 오브젝트
- 테스트를 위한 UserServiceImpl 고립
- 이전에 구현한 MockUserDao는 정상적으로 수행되도록 도와주는 스텁의 기능에 더불어 부가적인 검증 기능까지 가진 목 오브젝트로 생성
- 테스트 메소드 검증 방법이 필요하기 때문
- MockUserDao, MockMailSender, UserServiceTx를 이용하여 고립시키기
- 이전에 구현한 MockUserDao는 정상적으로 수행되도록 도와주는 스텁의 기능에 더불어 부가적인 검증 기능까지 가진 목 오브젝트로 생성
- 고립된 단위 테스트 활용
-
@Test public void upgradeLevels() throws Expcetion { userDao.deleteAll(); users.forEach(user -> userDao.add(user)); // DB Test Data Set-Up MockMailSender mockMailSender = new MockMailSender(); userServiceImpl.setMailSender(mockMailSender); // Mock Object DI userService.upgradeLevels(); checkLevelUpgraded(user.get(0), false); checkLevelUpgraded(user.get(1), true); checkLevelUpgraded(user.get(2), false); checkLevelUpgraded(user.get(3), true); checkLevelUpgraded(user.get(4), false); // DB에 저장된 결과 확인 List<String> request = mockMailSender.getRequests(); assertThat(request.size(), is(2)); assertThat(request.get(0), is(user.get(1).getEmail())); assertThat(request.get(1), is(user.get(3).getEmail())); // 결과 확인 }
-
- UserDao 목 오브젝트
- 실제 UserDao와 DB까지 의존하고있는 테스트도 목 오브젝트를 만들어서 적용하기
-
public void upgardeLevels() { userDao.getAll() // 스텁으로서 역할 필요 .filter(user -> canUpgradeLevel(user)) .forEach(user -> upgradeLevel(user)); } protected void upgradeLevel(User user) { user.upgradeLevel(); userDao.update(user); // 목 오브젝트로서 역할 필요 sendUpgradeEmail(); }
-
@RequiredContructor static class MockUserDao implements UserDao { private final List<User> users; private List<User> updated = new ArrayList<>(); public List<User> getUpdated() { return this.updated; } public List<User> getAll() { return this.users; } public void update(User user) { updated.add(user;) } public void add(User user) { throw new UnsupportedOperationException(); } public void deleteAll(User user) { throw new UnsupportedOperationException(); } public User get(String id) { throw new UnsupportedOperationException(); } public int getCount() { throw new UnsupportedOperationException(); } }
- 사용하지 않을 메소드는 UnsupportedOperationException을 던져서 지원하지 않는 기능임을 명시해주기
- null로 반환하거나 빈 메소드로 둬도 되지만, 안전한 사용성을 위해 위처럼 적용
- 사용하지 않을 메소드는 UnsupportedOperationException을 던져서 지원하지 않는 기능임을 명시해주기
-
@Test public void upgradeLevels() throws Exception { UserServiceImpl userServiceImpl = new UserServiceImpl(); MockUserDao mockUserDao = new MockUserDao(this.users); userServiceImpl.setUserDao(mockUserDao); // mailSender 주입 및 upgradeLevels 실행 List<User> updated = mockUserDao.getUpdated(); // 목 오브젝트 리턴 assertThat(updated.size(), is(2)); checkUserAndLevel(update.get(0), "zin0", Level.SILVER); checkUserAndLevel(updated.get(1), "jinyoung", Level.GOLD); // mailSender 검증 } private void checkUserAndLevel(User updated, String expectedId, Level expectedLevel) { assertThat(updated.getId(), is(expectedId)); assertThat(updated.getLevel(), is(expectedLevel)); }
- 테스트 수행 성능의 향상
- DB를 이용하는 테스트와 목 오브젝트를 이용하는 테스트 수행시간에는 큰 차이가 존재
- 진행 과정에서 수많은 DB 업데이트가 일어날수록 테스트 수행시간에 큰 차이를 보임
- 고립된 테스트를 만들기 위해서는 번거롭지만 목 오브젝트 작성하면, 시간 투자 대비 큰 효율을 볼 수 있음
- DB를 이용하는 테스트와 목 오브젝트를 이용하는 테스트 수행시간에는 큰 차이가 존재
- 테스트 대역 사용(Test Double)
- 단위 테스트와 통합 테스트
- 단위 테스트
- 하나의 단위에 초점을 맞춘 테스트(클래스, 메소드 등이 단위가 됨)
- 테스트 대역을 이용해 의존 오브젝트나 외부 리소스를 사용하지 않도록 고립시켜 테스트
- 통합 테스트
- 두 개 이상의 성격이나 계층이 다른 오브젝트가 연동하도록 테스트
- 외부의 리소스를 사용하는 테스트
- 단위 테스트 vs 통합 테스트
- 항상 단위 테스트를 먼저 고려
- 단위 테스트를 만들기 너무 복잡하면 통합테스트를 고려하지만, 가능한 많은 부분을 단위 테스트로 검증
- 그러기 위해서는 기능 분리가 잘 된 코드를 작성하는 것이 우선
- 단위 테스트를 만들기 너무 복잡하면 통합테스트를 고려하지만, 가능한 많은 부분을 단위 테스트로 검증
- 테스트 대역을 이용하도록 테스트를 구축
- 외부 리소스를 사용해야만 하는 테스트는 통합 테스트로 구축
- DAO는 DB까지 연동하는 테스트로 만드는 것이 효과적
- DB라는 외부 리소스를 사용하기 때문에 통합 테스트로 분류
- 코드 레벨은 하나의 기능 단위를 테스트
- DAO 테스트로 충분히 검증하면, DAO를 이용하는 코드는 DAO 역할을 스텁 or 목 오브젝트로 대체 가능
- 단위 테스트를 충분히 거치면 통합 테스트의 부담이 감소
- 스프링 테스트 컨텍스트 프레임워크를 이용하는 테스트는 통합 테스트
- 직접 코드 레벨의 DI를 사용하면서 단위 테스트를 하는게 좋지만, 추상적인 레벨을 테스트하는 경우 스프링 테스트 컨텍스트 프레임워크를 이용하여 통합 테스트 작성
- 항상 단위 테스트를 먼저 고려
- 테스트하기 편한 코드는 깔끔하고 좋은 코드가 될 수 있고 리팩토링과 개선에 좋은 영향을 미침
- 단위 테스트
- 목 프레임 워크
- 단위 테스트는 목 오브젝트를 만들거나 스텁을 만들어야함
- 번거로움
- Mockito 프레임워크
- 목 클래스를 준비할 필요 없음
- 인터페이스를 이용해 목 오브젝트 생성
- 리턴 값을 지정하거나 강제로 예외를 던지게 설정
- DI로 테스트 도중 사용되도록 설정
- 특정 메소드가 호출됐는지, 어떤 값을 가지고 몇 번 호출됐는지 검증
-
@Test public void mockUpgradeLevels() throws Exception { UserServiceImpl userServiceImpl = new UserServiceImpl(); UserDao mockUserDao = mock(UserDao.class); when(mockUserDao.getAll()).thenReturn(this.users); userServiceImpl }
- 목 클래스를 준비할 필요 없음
- 단위 테스트는 목 오브젝트를 만들거나 스텁을 만들어야함
반응형'Java & Spring > 토비의 스프링 3.1' 카테고리의 다른 글
6장) 6.4 스프링의 프록시 팩토리 빈 (0) 2021.07.16 6장) 6.3 다이내믹 프록시와 팩토리 빈 (0) 2021.07.16 5장) 5.3 서비스 추상화와 단일 책임 원칙 ~ 5.5 정리 (0) 2021.07.02 5장) 5.2 트랜잭션 서비스 추상화 (0) 2021.06.26 5장) 5.1 사용자 레벨 관리 기능 추가 (0) 2021.06.26