ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 5장) 5.3 서비스 추상화와 단일 책임 원칙 ~ 5.5 정리
    Java & Spring/토비의 스프링 3.1 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)
      • 테스트 대상이 사용하는 의존 오브젝트를 대체하도록 만든 오브젝트
      • 테스트 스텁
        • 테스트 대상 오브젝트가 원활하게 동작할 수 있도록 도우면서 간접적인 정보를 제공
      • 목 오브젝트
        • 테스트 대상으로부터 전달받은 정보를 검증할 수 있도록 설계된 것
    반응형

    댓글

Designed by Tistory.