[토비의 스프링] Week7(5.3~5.5)

9 분 소요

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

기술과 서비스에 대한 추상화 기법을 사용하면 특정 기술환경에 종속되지 않는 코드를 만들 수 있다.

수직, 수평 계층구조와 의존관계

수평관계

UserDaoUserService는 각각 담당하는 코드의 기능적인 관심에 따라 분리되고, 서로 불필요한 영향을 주지 않으면서 독자적으로 확장이 가능하도록 하는, 같은 애플리케이션 로직을 담은 코드지만 각자의 관심사(내용)에 따라 나뉘어진 수평적인 분리이다.

수직관계

트랜잭션 추상화의 경우는 비지니스 로직과 그 하위에서 동작하는 트랜잭션 기술이라는 다른 계층의 특성을 갖는 수직적 분리 관계라고 할 수 있다.

수평적 분리이든 수직적 분리이든 모두 스프링의 DI가 그 역할을 담당

단일 책임 원칙

하나의 모듈은 한 가지 책임을 가져야한다.

즉, 하나의 모듈이 바뀌는 이유는 한가지여야 한다.
단일 책임 원칙을 잘 지키게 되면 변경이 필요할 때 수정 대상이 명확해진다.

Spring에서 DI를 사용할 때의 장점

유지보수가 아주 간편해진다.

DI가 아닌 종속적이고 의존적인 코드들로 이루어져있다면, 수평적이든 수직적 의존관계이든 수정해야 할 부분이 의존을 하고 있는 서비스와 그 메소드 갯수 만큼 증가하게 된다.

객체지향 설계와 프로그래밍의 원칙은 서로 긴밀하게 관련이 있다. 단일 책임 원칙을 잘 지키는 코드를 만들려면 인터페이스를 도입하고 이를 DI로 연결해야 하며, 그 결과로 단일 책임 원칙뿐 아니라 개방 폐쇄 원칙도 잘 지키고, 모듈 간에 결합도가 낮아서 서로의 변경이 영향을 주지 않고, 같은 이유로 변경이 단일 책임에 집중되는 응집도 높은 코드가 나오게 된다. 이런 과정에서 자연스럽게 디자인 패턴이 적용되기도 한다

메일 서비스 추상화

“레벨이 올라가면 사용자에게 이메일로 알려주세요!”

User 테이블에 email 칼럼을 추가하고, 해당 프로퍼티와 메서드에 기능 추가한다.
이후에는 자바에서 제공해주는 JavaMail을 이용해서 메일 발송 기능을 추가.

JavaMail이 포함된 코드의 테스트, 테스트를 위한 서비스 추상화

매번 메일이 발송되는 것은 테스트로써 바람직하지 못하다. 메일 발송은 부하가 매우 크기 때문

JavaMail은 자바의 표준 기술이고 검증된 안정적인 모듈이므로, JavaMail의 동작하면 성공적으로 메일이 발송 되었다고 생각하자.

JavaMail을 이용한 테스트의 문제점

JavaMail의 API는 구현 클래스이므로 추상화를 하지 못한다.

-> 스프링이 제공하는 메일 서비스 추상화 인터페이스를 사용

public interface MailSender{
  void send(SimpleMailMessage simpleMessage) throws MailException;
  void send(SimpleMailMessage[] simpleMessage) throws MailException;
}
private void sendUpgradeEmail(User user) {
  JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
  mailSender.setHost("mail.server.com");

  SimpleMailMessage mailMessage = new SimpleMailMessage();
  mailMessage.setTo(user.getEmail());
  mailMessage.setFrom("useradmin@ksug.org");
  mailMessage.setSubject("Upgrade 안내");
  mailMessage.setText("사용자님의 등급이 " + user.getLevel().name());

  mailSender.send(mailMessage);
}

위 코드는 스프링이 제공하는 MailSender 인터페이스를 이용하여 메일 전송 서비스를 추상화 하였다.

JavaMailSenderImpl는 실제 메일을 전송하는 오브젝트 이므로, 이를 테스트용 오브젝트로 변경하자.

// 실제 simpleMessage로 부터 메일을 받아 전송하는 것이 아닌 dummy 전송 객체
public class DummyMailSender implements MailSender {
	@Override
	public void send(SimpleMailMessage simpleMessage) throws MailException {
	}

	@Override
	public void send(SimpleMailMessage[] simpleMessages) throws MailException {
	}
}

위 더미 객체를 DI 해주자.


	<bean id="userService" class ="com.taxol.service.UserService">
		<property name="userDao" ref="userDao"/>
		<property name="transactionManager" ref="transactionManager"></property>
		<property name="mailSender" ref="mailSender"/>
	</bean>

	<bean id="mailSender" class = "com.taxol.service.DummyMailSender"/>

이후 userServiceTest에 수동 DI 해주면 정상적으로 동작한다.


@Autowired
MailSender mailSender;

@Test
	public void upgradeAllOrNothing() throws Exception {
		UserService testUserService = new TestUserService(users.get(3).getId());
		testUserService.setUserDao(userDao);
		testUserService.setDataSource(dataSource);
		testUserService.setTransactionManager(transactionManager);
		testUserService.setMailSender(mailSender);	// UserService를 위한 메일 전송 오브젝트 수동 DI

		userDao.deleteAll();

		for (User user : users) {
			userDao.add(user);
		}

		try {
			testUserService.upgradeLevels();
			fail("TestUserServiceException expected");
		} catch (TestUserServiceException e) {
		}

	}

테스트와 서비스 추상화

서비스 추상화

일반적으로 기능은 유사하나 사용 방법이 다른 로우레벨의 다양한 기술에 대해 일관성 있는 접근 방법을 제공하는 것을 의미
JavaMail과 같이 테스트를 어렵게 만드는 API를 사용할 때도 유용하게 사용됨
JavaMail이 아닌 다른 메세징 서버의 API를 이용하는 경우에도 MailSender 구현 클래스를 만들어서 DI 해주면 됨
비즈니스 로직이 바뀌지 않는 한 UserService는 수정할 필요가 없음

🤔문제점 : 트랜잭션 개념이 빠져있음

레벨 업그레이드에 트랜잭션이 적용되어 있으므로 메일 발송 기능에도 트랜잭션을 적용해야 함

-> 해결 방안

  1. 발송 대상을 별도의 목록에 저장 : 사용자 관리 비즈니스 로직과 메일 발송 트랜잭션 기술 부분이 섞임
  2. MailSender를 확장해서 메일 전송에 트랜잭션 개념을 적용 : 서로 다른 종류의 작업을 분리해 처리가 가능

테스트 대역

  • 테스트용 DB / DummyMail
  • 테스트 환경에서 유용하게 사용 가능

테스트 할 대상이 의존하고 있는 오브젝트를 DI를 통해 바꿔치기 함

UserDao

UserDao는 운영 시스템에서 사용하는 DB와 연결돼서 동작함
-> 테스트에서는 운영 DB의 연결이나 WAS의 DB 풀링 서비스는 번거롭다. 그럼에도 불구하고 DB는 반드시 있어야 하므로 가벼운 버전을 이용

UserService

UserService는 메일 전송 기능을 이용

메일 전송 기능을 아예 뗄 수는 없음 테스트에 지장을 주지 않기 위해 DummyMailSender를 도입

의존이란 종속되거나 기능을 사용한다는 의미 간단한 오브젝트의 테스트를 위해 너무 많은 작업이 뒤 따름

🥰해결책

UserDao : 환경 자체를 간단한게 만듬 UserService : 아무런 일도 하지 않는 빈 오브젝트로 대체

테스트 대역의 종류와 특징

⚾테스트 대역 : 테스트용으로 사용되는 특별한 오브젝트

  • 대부분 테스트 대상인 오브젝트의 의존 오브젝트
  • UserDao의 DataSource, UserService의 DummyMailSender

⚾테스트 스텁 : 대표적인 테스트 대역

  • 테스트 대상 오브젝트의 의존객체
  • 테스트 동안 코드가 정상적으로 수행할 수 있도록 돕는 것
  • DI를 통해 의존 오브젝트를 테스트 스텁으로 변경해야 함

목 오브젝트를 이용한 테스트

🤷‍♂️목 오브젝트란?
테스트 대상 오브젝트의 메소드가 돌려주는 결과뿐 아니라 테스트 오브젝트가 간접적으로 의존 오브젝트에 넘기는 값과 그 행위 자체에 대해서도 검증하고 싶은 경우에 사용

쉽게 얘기해서 테스트 코드에서는 우리가 테스트를 하기로 한 오브젝트의 결과값만을 받아오는데, 테스트 오브젝트에서 사용하는 의존 오브젝트에 값을 잘 전달하고 반환이 잘 되는지에 대한 것을 검사할 때 사용한다.

public class UserServiceTest {
  static class MockMailSender implements MailSender {
    // UserService로부터 전송 요청을 받은 메일 주소를 저장해두고 이를 읽을 수 있게 한다.
    private List<String> requests = new ArrayList<String>();

    public List<String> getRequests() {
      return requests;
    }

    @Override
    public void send(SimpleMailMessage mailMessage) throws MailException {
      // 전송 요청을 받은 이메일 주소를 저장해둔다.
      // 간단하게 첫 번째 수신자 메일 주소만 저장했다.
      requests.add(mailMessage.getTo()[0]);
    }

    @Override
    public void send(SimpleMailMessage... mailMessage) throws MailException {
    }
  }
}
@Test
@DirtiesContext
public void upgradeLevels() {
  userDao.deleteAll();
  for(User user : users) userDao.add(user);

  // 메일 발송 결과를 테스트할 수 있도록 목 오브젝트를 만들어 userService의 의존 오브젝트로 주입한다.
  MockMailSender mockMailSender = new MockMailSender();
  userService.setMailSender(mockMailSender);

  userService.upgradeLevels();

  // 각 사용자별로 업그레이드 후의 예상 레벨을 검증한다.
  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);

  // 목 오브젝트에 저장된 메일 수신자 목록을 가져와 업그레이드 대상과 일치하는지 확인 한다.
  List<String> request = mockMailSender.getRequests();
  assertThat(request.size(), is(2));
  assertThat(request.get(0), is(users.get(1).getEmail()));
  assertThat(request.get(1), is(users.get(3).getEmail()));
}

👻테스트 프로세스

  1. DummyMailSender를 대신해서 사용할 메일 전송 검증용 목 오브젝트를 준비
  2. MockMailSender 오브젝트 생성 후 수동 DI
  3. 목 오브젝트로부터 getRequests()를 호출해서 메일 주소가 저장된 리스트를 가져옴
  4. 두 번째와 네 번째 유저가 업그레이드가 되었고, 메일이 발송되었음을 확인

댓글남기기