5. 서비스 추상화
스프링 어플리케이션 프레임워크의 기반이 되늰 3가지 기술에는 DI, 서비스추상화, AOP가 있다. 이번에는 서비스 추상화에 대해 배워보자.
자바에는 구체적 구현과 사용 방식은 조금 다르지만, 기능과 목적이 유사한 기술들이 더러 있다. 심지어는 같은 자바 표준 기술 중에서도 플랫폼/컨텍스트에 차이가 있거나 발전 역사가 다라 목적이 유사한 여러 기술들이 공존하기도 한다. 이런 비슷한 목적의 기술들을 환경에 따라 다르게 구현해야 한다면 무척 힘들고, 유지보수도 어려울 것이다.
스프링에서는 이런 비슷한 기능의 기술들을 ‘서비스 추상화’라는 방법으로 보다 일관된 방법으로 사용할 수 있게 도와준다.
트랜젝션을 예로 들어보자. 트랜젝션이라는 기능은 데이터베이스 관련 기능을 구현할때 필수적인 기능이다. 하지만 실제 디비 종류에 따라 조금씩 구현체가 다르다. 이런 트랜젝션 구현체를 직접 의존하는 방식으로 구현하면 디비가 달라질때마다 소스 코드를 수정해야 하니, OCP원칙을 위반하는 코드가 나온다.
이 문제를 해결하기 위해 스프링은 트랜젝션 동기화 저장소라는 기능을 제공한다. 스레드 별로 독립적인 트랜젝션 저장소에 트랜젝션 객체를 저장해놓는 방식으로, 디비 독립적인 트랜젝션 기능을 구현할 수 있다. 또한 PlatformTransactionManager라는 플랫폼 독립적인 트랜젝션 서비스 추상화 계층(인터페이스)을 제공한다. 이를 통해 아래와 같은 코드를 작성할 수 있다.
public class UserService {
UserDao userDao;
UserLevelUpgradePolicy userLevelUpgradePolicy;
PlatformTransactionManager transactionManager;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void setUserLevelUpgradePolicy(UserLevelUpgradePolicy userLevelUpgradePolicy) {
this.userLevelUpgradePolicy = userLevelUpgradePolicy;
}
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public void upgradeLevels() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
List<User> users = userDao.getAll();
for (User user : users) {
if (userLevelUpgradePolicy.canUpgradeLevel(user)) {
userLevelUpgradePolicy.upgradeLevel(user);
}
}
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
}
실제 각 디비 벤더들은 PlatformTransactionManager 인터페이스를 구현한 트랜젝션 매니저 구현 클래스를 제공하므로, 개발자는 시스템 디비 종류에 따라 다른 클래스를 사용해 스프링 DI 설정을 변경하면, 어플리케이션 코드의 변경 없이 여러 디비 트랜젝션을 적용할 수 있다.
단일 책임 원칙 Single Responsibility Principle
- 수평적 / 수직적 분리
UserSevice 클래스와 UserDAO 클래스를 분리한것은 어플리케이션 계층에서 기능에 따라 두 클래스로 분리한것이니 수평적 분리라고 할 수 있다. 반면 트랜젝션을 TransactionManager로 분리한것은 비즈니스 로직과 기술적 로직을 분리한 것이니 수직적 분리이다.
위와 같이 코드를 수직/수평적으로 분리를 하다보면, 하나의 클래스가 한가지 책임만 가지는 단일책임원칙을 자연스럽게 따르게 된다. 단일책입 원칙은 다시 말해 어떤 클래스를 수정하는 데는 한가지 이유만 존재해야 한다는 원칙이다.
그리고 단일책임 원칙을 지키기 위해 주로 사용되는 도구가 바로 DI이다. 이렇듯 스프링 DI는 단순히 IoC컨테이너를 사용한다는 것이 아닌, 스프링을 활용한 엔터프라이즈 어플리케이션 개발에 폭넓게 적용되는 개념이다.
테스트를 위한 서비스 추상화
서비스 추상화의 또 다른 장점은 테스트가 수월해진다는 점이다. 서비스를 추상화 시켜, 해당 추상화 계층을 구현하는 목업 대역 컴포넌트를 만들고, 테스트 상황에서 목 오브젝트를 주입하여 테스트를 하면 직접 입출력 뿐 아니라 간접 입출력도 테스트가 가능해진다. 아래가 목 오브젝트를 활용한 테스트 예제 코드이다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext-test.xml")
public class UserServiceTest {
@Autowired
ApplicationContext context;
@Autowired
UserService userService;
@Autowired
UserDao userDao;
@Autowired
UserLevelUpgradePolicy userLevelUpgradePolicy;
@Autowired
MailSender mailSender;
static class MockMailSender implements MailSender {
private List<String> requestTos = new ArrayList<>();
public List<String> getRequestTos() {
return requestTos;
}
@Override
public void send(SimpleMailMessage simpleMailMessage) throws MailException {
requestTos.add(simpleMailMessage.getTo()[0]);
}
@Override
public void send(SimpleMailMessage[] simpleMailMessages) throws MailException {
}
}
...
@Test
public void upgradeLevels() throws SQLException {
for (User user : users) {
userDao.add(user);
}
MockMailSender mockMailSender = new MockMailSender();
((NormalUserLevelUpgradePolicy) userLevelUpgradePolicy).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);
assertThat(mockMailSender.getRequestTos().get(0), is(users.get(1).getEmail()));
assertThat(mockMailSender.getRequestTos().get(1), is(users.get(3).getEmail()));
}
...
}
결론
-
서비스 추상화는 구체적 구현체에 상위 계층이 영향받지 않도록, 같은 기능을 하는 서비스 구현체들의 공통 인터페이스를 추상화하여 제공한다.
-
서비스 추상화는 실제 구현체가 바뀌는 경우 뿐 아니라, 목 오브젝트를 통한 테스트에도 도움이 된다.