-
5장) 5.3 서비스 추상화와 단일 책임 원칙 ~ 5.5 정리Java & Spring/토비의 스프링 3.1 2021. 7. 2. 14:11반응형
5장 서비스 추상화
5.3 서비스 추상화와 단일 책임 원칙
- 수직, 수평 계층구조와 의존관계
- 기술과 서비스에 대해 추상화 기법 적용
- UserDao와 UserService가 각각 담당하는 코드의 기능적인 관심에 따라 분리, 독자적으로 확장이 가능하도록 작업
- 같은 계층에서 수평적인 분리
- UserDao와 UserService가 각각 담당하는 코드의 기능적인 관심에 따라 분리, 독자적으로 확장이 가능하도록 작업
- 트랜잭션 추상화
- 비즈니스 로직과 그 하위에서 동작하는 로우레벨의 트랜잭션 기술이라는 아예 다른 계층의 특성을 갖는 코드를 분리
- 기술과 서비스에 대해 추상화 기법 적용
- 단일 책임 원칙
- 하나의 모듈은 한가지 책임을 가져야함
- == 하나의 모듈이 바뀌는 이유는 한 가지여야함
- UserService 예시
- JDBC Connection 메소드를 직접 사용하는 트랜잭션 코드가 있던 경우
- 두 가지의 책임을 가짐
- 사용자 레벨을 어떻게 관리할 것인가
- 트랜잭션을 어떻게 관리할 것인가
- 단일 책임 원칙을 지키지 못함
- 두 가지의 책임을 가짐
- 트랜잭션 서비스 추상화 방식 도입 후
- 사용자 관리 로직에 대해서만 관심
- 트랜잭션에 대해서는 책임이 없음
- 단일 책임 원칙을 지킴
- JDBC Connection 메소드를 직접 사용하는 트랜잭션 코드가 있던 경우
- 장점
- 변경이 필요할 때 수정 대상이 명확
- 인터페이스를 도입하고 DI로 연결해야 하며, 그 결과로 단일 책임 원칙과 개방 폐쇄 원칙도 잘 지키고, 모듈 간 결합도가 낮아서 서로의 변경이 영향 X, 같은 이유로 변경이 단일 책임에 집중되는 응집도 높은 코드를 작성하게 됨
- 스프링 DI의 장점
- 애플리케이션 로직의 종류에 따른 수평적인 구분이든, 로직과 기술의 수직적 구분이든 모두 결합도가 낮으며, 서로 영향을 주지 않고 자유롭게 확장될 수 있는 구조를 만들어 줌
- 적절하게 책임과 관심이 다른 코드 분리
- 서로 영향이 없도록 다양한 추상화 기법 도입
- 애플리케이션 로직과 기술/환경 분리
- 디자인 패턴 적용
- 테스트하기 용이
- 애플리케이션 로직의 종류에 따른 수평적인 구분이든, 로직과 기술의 수직적 구분이든 모두 결합도가 낮으며, 서로 영향을 주지 않고 자유롭게 확장될 수 있는 구조를 만들어 줌
- 스프링 DI의 장점
- 하나의 모듈은 한가지 책임을 가져야함
5.4 메일 서비스 추상화
레벨이 업그레이드되는 사용자에게 안내 메일을 발송해달라는 요구 사항이 추가
JavaMail을 이용한 메일 발송 기능
DB의 User 테이블에 email 필드 추가, User 클래스에 email 프로퍼티 추가
- UserDao의 userMapper와 insert(), update()에 email 필드 처리 코드 추가 및 테스트 코드 수정
JavaMail 발송
protected void upgradeLevel(User user) { user.upgradeLevel(); userDao.update(user); sendUpgradeEamil(); // JavaMail을 이용한 이메일 전송 메소드 }
SMTP 프로토콜을 지원하는 메일 전송 서버가 준비되었다면, 위의 코드는 정상적으로 작동
JavaMail이 포함된 코드의 테스트
- 개발 중인 경우, 메일 서버가 준비되지 않으면
MessagingException
을 마주함- 메일 발송의 경우 부하가 크고 서버에 부담이 됨
- 테스트 메일이 실제로 전송되는 경우가 생김
- SMTP로 메일 전송 요청을 받으면 정상이라고 테스트 가능
- 테스트용 메일 서버를 만들어서, 메일 전송 요청은 받지만 아무런 동작을 안하도록 설정
- 개발 중인 경우, 메일 서버가 준비되지 않으면
테스트를 위한 서비스 추상화
JavaMail을 이용한 테스트의 문제점
- JavaMail의 API는 위에서 제시한 테스트용 메일 서버를 사용하는 것이 불가능
- 핵심 API가 인터페이스로 만들어져 있기 때문
- 스프링이 제공하는 JavaMail에 대한 추상화 기능 이용
- 기본적으로 JavaMail을 사용해 메일 발송 기능을 제공하는 JavaMailSenderImpl을 이용하면되지만, 테스트 시 메일 발송을 하지 않도록 하기 위해 인터페이스를 구현
- JavaMail의 API는 위에서 제시한 테스트용 메일 서버를 사용하는 것이 불가능
메일 발송 기능 추상화
public interface MailSender { void send(SimpleMailMessage simpleMessage) throws MailException; void send(SimpleMailMessage[] simpleMessages) throws MailException; }
public class UserService { ... private MailSender mailSender; public void setMailSender(MailSender mailSender) { this.mailSender = mailSender; } private void sendUpgradeEmail(User user) { SimpleMailMessage mailMessage = new SimpleMailMessage(); mailMessage.setTo(user.getEmail()); mailMessage.setFrom("zin0@test.com"); mailMessage.setSubject("Upgrade 안내"); mailMessage.setText("사용자님의 등급이 " + user.getLevel().name() + "로 업그레이드 됐습니다."); this.mailSender.send(mailMessage); } }
자바 메일에서 처리하는 각종 예외 (AddressException, MessagingException, UnsupportedEncodingException) 를 MailException이라는 런타임 예외로 포장해서 던져줌
테스트용 메일 발송 오브젝트
테스트용으로 MailSender 인터페이스를 구현
public class DummyMailSender implements MailSender { public void send(SimpleMailMessage mailMessage) throws MailException {} public void send(SimpleMailMessage[] mailMessages) throws MailException {}
메일 발송 기능 자체에 대한 테스트는 MailSender에 대한 별도의 학습 테스트 or 메일 서버 설정 점검용 테스트를 통해 확인
pubilc class UserServiceTest { @Autowired MailSender mailSender; @Test public void upgradeAllOrNothing() throws Exception { ... testUserService.setMailSender(mailSender); } }
테스트와 서비스 추상화
- 서비스 추상화
- 기능은 유사하나 사용 방법이 다른 로우레벨의 다양한 기술에 대해 추상 인터페이스와 일관성 있는 접근 방법을 제공해주는 것
- 테스트를 어렵게 만드는 건전하지 않은 방식으로 설계뙨 API를 사용할 때도 유용
- UserService와 같은 애플리케이션 계층의 코드는 아래 계층에 대한 관심이 없이 메일 발송을 요청한다는 기본 기능에 충실하게 작성
- 비즈니스 로직이 바뀌지 않는한 수정할 필요가 없음
- 메일 발송에 대한 트랜잭션 작업
- 메일을 업그레이드할 사용자를 발견할 때마다 발송하지 않고 발송 대상을 별도의 목록에 저장
- 메일 저장 리스트 등을 파라미터로 계속 가지고 다녀야함
- MailSender를 확장해서 메일 전송에 트랜잭션 개념을 적용
- MailSender를 확장한 클래스에 업그레이드 작업 이전에 새로운 메일 전송 작업 시작을 알려주고, 이 때 부터 send() 메소드를 호출해도 발송하지 않고 저장해둠
- 업그레이드 작업이 끝나면 트랜잭션 기능을 가진 MailSender에 지금까지 저장된 메일을 모두 발송하고 예외가 발생하면 모두 취소
- 서로 다른 종류의 작업을 분리해서 처리하기 때문에 더 적합
- 메일을 업그레이드할 사용자를 발견할 때마다 발송하지 않고 발송 대상을 별도의 목록에 저장
- 외부의 리소스와 연동하는 대부분 작업은 추상화의 대상이 될 수 있음
- 서비스 추상화
테스트 대역
테스트할 대상이 의존하고 있는 오브젝트를 DI를 통해 바꿔치기
의존 오브젝트 변경을 통한 테스트 방법
- 하나의 오브젝트가 사용하는 오브젝트를 DI에서는 의존 오브젝트라고 부름
- 테스트 대상인 오브젝트가 의존 오브젝트를 가지고 있기 때문에 발생하는 여러 문제점이 존재
- 간단한 오브젝트의 코드를 테스트하는데 거창한 작업이 뒤따르는 경우
- 스프링 DI를 통해 해결
테스트 대역의 종류와 특징
- 테스트 대역(Test Double)
- 테스트 환경을 만들어주기 위해, 테스트 대상이 되는 오브젝트의 기능에만 충실하게 수행하면서 빠르게, 자주 테스트를 실행할 수 있도록 사용하는 오브젝트
- MailSender 인터페이스를 구현한 것들, DataSource 등이 예시
- 테스트 스텁
- 대표적인 Test Double
- 테스트 대상 오브젝트의 의존객체로서 존재하고, 코드가 정상적으로 수행할 수 있도록 도움
- 테스트 시에 DI를 통해 테스트 스텁으로 변경
- DummyMailSender가 예시
- 목 오브젝트(Mock Object)
- 테스트 스텁이 결과를 돌려줘야하는 경우(리턴 값이 있는 메소드 이용)
- 테스트 대상의 간접적인 출력 결과를 검증
- 테스트 대상 오브젝트와 의존 오브젝트 사이에서 일어나는 일을 검증하도록 설계
- 테스트 환경을 만들어주기 위해, 테스트 대상이 되는 오브젝트의 기능에만 충실하게 수행하면서 빠르게, 자주 테스트를 실행할 수 있도록 사용하는 오브젝트
- 테스트 대역(Test Double)
목 오브젝트를 이용한 테스트
위 예시의 upgradeAllOrNothing() 테스트의 경우 메일 전송 여부에 관심 X
- DummyMailSender가 적합
사용자 레벨 업그레이드 결과를 확인하는 upgradeLevels() 테스트의 경우, 메일 전송 자체에 대한 검증 필요
static class MockMailSender implements MailSender { private List<String> requests = new ArrayList<>(); public List<String getRequests() { return this.requests; } public void send(SimpoleMailMessage mailMessage) throws MailException { requests.add(mailMessage.getTo()[0]); } public void send(SimpoleMailMessage[] mailMessages) throws MailException {} }
@Test @DirtiesContext public void upgradeLevels() throws Exception { userDao.deleteAll(); users.forEach(user -> userDao.add(user)); MockMailSender mockMailSender = new MockMailSender(); userService.setMailSender(mockMailSender); 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); List<String> request = mockMailSender.getRequests(); assertTaht(request.size(), is(2)); assertTaht(request.get(0), is(users.get(1).getEmail())); assertTaht(request.get(1), is(users.get(3).getEmail())); }
5.5 정리
- 비즈니스 로직과 데이터 액세스 로직은 깔끔하게 분리
- 비즈니스 로직 코드는 내부적 책임과 역할에 따라 깔끔하게 메소드로 정리
- 이를 위해 DAO의 기술 변화에 서비스 계층 코드가 영향이 없도록 인터페이스와 DI를 활용하여 결합도를 낮춰야함
- DAO를 사용하는 비즈니스 로직에는 단위 작업을 보장해주는 트랜잭션이 필요
- 트랜잭션 경계설정
- 트랜잭션의 시작과 끝을 지정하는 일
- 주로 비즈니스 로직 안에서 일어남
- 스프링이 제공하는 트랜잭션 동기화 기법을 활용
- 트랜잭션 방법에 따라 비즈니스 로직 코드가 함께 변경되면 단일 책임 원칙에 위배
- DAO가 사용하는 특정 기술에 대한 강한 결합을 만들어냄
- 서비스 추상화
- 로우레벨의 트랜잭션 기술과 API의 변화에 상관없이 일관된 API를 가진 추상화 계층 도입
- 테스트가 어려운 기술에 적용 가능
- 트랜잭션 경계설정
- 테스트 대역(Test Double)
- 테스트 대상이 사용하는 의존 오브젝트를 대체하도록 만든 오브젝트
- 테스트 스텁
- 테스트 대상 오브젝트가 원활하게 동작할 수 있도록 도우면서 간접적인 정보를 제공
- 목 오브젝트
- 테스트 대상으로부터 전달받은 정보를 검증할 수 있도록 설계된 것
반응형'Java & Spring > 토비의 스프링 3.1' 카테고리의 다른 글
6장) 6.3 다이내믹 프록시와 팩토리 빈 (0) 2021.07.16 6장) 6.1 트랜잭션 코드의 분리 ~ 6.2 고립된 단위 테스트 (0) 2021.07.11 5장) 5.2 트랜잭션 서비스 추상화 (0) 2021.06.26 5장) 5.1 사용자 레벨 관리 기능 추가 (0) 2021.06.26 4장) 4.2 예외 전환 ~ 4.3 정리 (0) 2021.06.21 - 수직, 수평 계층구조와 의존관계