Java & Spring/토비의 스프링 3.1

5장) 5.3 서비스 추상화와 단일 책임 원칙 ~ 5.5 정리

Zin0_0 2021. 7. 2. 14:11
반응형

5장 서비스 추상화

5.3 서비스 추상화와 단일 책임 원칙

  • 수직, 수평 계층구조와 의존관계
    • 기술과 서비스에 대해 추상화 기법 적용
      • UserDao와 UserService가 각각 담당하는 코드의 기능적인 관심에 따라 분리, 독자적으로 확장이 가능하도록 작업
        • 같은 계층에서 수평적인 분리
    • 트랜잭션 추상화
      • 비즈니스 로직과 그 하위에서 동작하는 로우레벨의 트랜잭션 기술이라는 아예 다른 계층의 특성을 갖는 코드를 분리
  • 단일 책임 원칙
    • 하나의 모듈은 한가지 책임을 가져야함
      • == 하나의 모듈이 바뀌는 이유는 한 가지여야함
    • UserService 예시
      • JDBC Connection 메소드를 직접 사용하는 트랜잭션 코드가 있던 경우
        • 두 가지의 책임을 가짐
          • 사용자 레벨을 어떻게 관리할 것인가
          • 트랜잭션을 어떻게 관리할 것인가
        • 단일 책임 원칙을 지키지 못함
      • 트랜잭션 서비스 추상화 방식 도입 후
        • 사용자 관리 로직에 대해서만 관심
        • 트랜잭션에 대해서는 책임이 없음
        • 단일 책임 원칙을 지킴
    • 장점
      • 변경이 필요할 때 수정 대상이 명확
    • 인터페이스를 도입하고 DI로 연결해야 하며, 그 결과로 단일 책임 원칙과 개방 폐쇄 원칙도 잘 지키고, 모듈 간 결합도가 낮아서 서로의 변경이 영향 X, 같은 이유로 변경이 단일 책임에 집중되는 응집도 높은 코드를 작성하게 됨
      • 스프링 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을 이용하면되지만, 테스트 시 메일 발송을 하지 않도록 하기 위해 인터페이스를 구현
    • 메일 발송 기능 추상화

      • 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)
          • 테스트 스텁이 결과를 돌려줘야하는 경우(리턴 값이 있는 메소드 이용)
          • 테스트 대상의 간접적인 출력 결과를 검증
          • 테스트 대상 오브젝트와 의존 오브젝트 사이에서 일어나는 일을 검증하도록 설계
    • 목 오브젝트를 이용한 테스트

      • 위 예시의 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)
    • 테스트 대상이 사용하는 의존 오브젝트를 대체하도록 만든 오브젝트
    • 테스트 스텁
      • 테스트 대상 오브젝트가 원활하게 동작할 수 있도록 도우면서 간접적인 정보를 제공
    • 목 오브젝트
      • 테스트 대상으로부터 전달받은 정보를 검증할 수 있도록 설계된 것
반응형