5장 서비스 추상화
비즈니스 로직을 담은 UserService 클래스를 만들고
트랜잭션을 적용하면서 스프링의 서비스 추상화에 대해 살펴보기
5.1 사용자 레벨 관리 기능 추가
- Level 이늄
- User 필드 추가
- UserDaoTest, UserDaoJdbc 수정
- 사용자 수정 기능 추가
- ...
5.1 사용자 레벨 관리 기능 추가
UserServiceTest
UserService
UserDao
UserDaoJdbc
UserDaoJpa
5.1 사용자 레벨 관리 기능 추가
public void upgradeLevels() {
List<User> users = userDao.getAll();
for(User user : users) {
Boolean changed = null;
if (user.getLevel() == Level.BASIC && user.getLogin() >= 50) {
user.setLevel(Level.SILVER);
changed = true;
} else if (user.getLevel() == Level.SILVER && user.getRecommend() >= 30) {
user.setLevel(Level.GOLD);
changed = true;
} else if (user.getLevel() == Level.GOLD) {
changed = false;
} else {
changed = false;
}
if (changed) { userDao.update(user);
}
}
5.1 사용자 레벨 관리 기능 추가
- 코드에 중복된 부분은 없는가?
- 코드가 무엇을 하는 것인지 이해하기 불편하지 않은가?
- 코드가 자신이 있어야 할 자리에 있는가?
- 앞으로 변경이 일어난다면 어떤 것이 있을 수 있고, 그 변화에 쉽게 대응할 수 있게 작성되어 있는가?
337 page 참고
5.1 사용자 레벨 관리 기능 추가
public void upgradeLevels() {
List<User> users = userDao.getAll();
for(User user : users) {
if (canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
}
private boolean canUpgradeLevel(User user) {
Level currentLevel = user.getLevel();
switch(currentLevel) {
case BASIC: return (user.getLogin() >= 50);
case SILVER: return (user.getRecomment() >= 30);
case GOLD: return false;
default: throw new IllegalArgumentException("Unknown Level : " + currentLevel);
}
}
5.1 사용자 레벨 관리 기능 추가
private void upgradeLevel(User user) {
// 객체지향적인 코드는 다른 오브젝트의 데이터를 가져와서 작업하는 대신
// 데이터를 갖고 있는 다른 오브젝트에게 작업을 해달라고 요청한다.
user.upgradeLevel();
userDao.update(user);
}
// User의 레벨 업그레이드 작업용 메소드
public void upgradeLevel() {
Level nextLevel = this.level.nextLevel();
if (nextLevel == null) {
throw new IllegalStateException(this.level + "은 업그레이드가 불가능합니다.");
} else {
this.level = nextLevel;
}
}
정리
- 비즈니스 로직을 담은 코드는 데이터 액세스 로직을 담은 코드와 깔끔하게 분리되는 것이 바람직하다. 비즈니스 로직 또한 내부적으로 책임과 역할에 따라서 깔끔하게 메소드로 정리돼야 한다.
- 이를 위해서는 DAO의 기술 변화에 서비스 계층의 코드가 영향을 받지 않도록 인터페이스와 DI를 잘 활용해서 결합도를 낮춰줘야 한다.
5.2 트랜잭션 서비스 추상화
- "사용자 레벨 조정 작업은 중간에 문제가 발생해서 작업이 중단되면 그때까지 진행된 변경 작업도 모두 취소시키자."
- 레벨 업그레이드 작업은 하나의 트랜잭션 안에서 동작해야 한다.
- 트랜잭션 : 더 이상 나눌 수 없는 단위 작업
5.2 트랜잭션 서비스 추상화
- 현재 상태 문제점
- UserDao 각 메소드마다 하나씩의 독립적인 트랜잭션으로 실행된다.
- UserService에서 DB 커넥션을 다룰 수 있는 방법이 없다.
- DAO 메소드 안으로 upgradeLevels() 내용을 옮길까? (X)
- 트랜잭션의 경계설정 작업을 UserService 쪽으로 가져와야 한다.
public void upgradeLevels() throws Exception {
// (1) DB Connection 생성
// (2) 트랜잭션 시작
try {
// (3) DAO 메소드 호출
// (4) 트랜잭션 커밋
}
catch (Exception e) {
// (5) 트랜잭션 롤백
throw e;
}
finally {
// (6) DB Connection 종료
}
}
5.2 트랜잭션 서비스 추상화
Connection 오브젝트를 파라미터로 전달해줘야 한다.
359 페이지 참고
5.2 트랜잭션 서비스 추상화
- 문제점
- DB 커넥션을 비롯한 리소스의 깔끔한 처리를 가능하게 했던 JdbcTemplate을 더 이상 활용할 수 없다.
- UserService의 메소드에 Connection 파라미터가 추가돼야 한다. (Connection 오브젝트가 계속해서 전달돼야 한다.)
- UserDao는 더 이상 데이터 액세스 기술에 독립적일 수가 없다.
- 테스트 코드에도 영향을 미친다.
5.2 트랜잭션 서비스 추상화
- 스프링이 제안하는 방법은 트랜잭션 동기화
- UserService에서 트랜잭션을 시작하기 위해 만든 Connection 오브젝트를 특별한 저장소에 보관해두고, 이후에 호출되는 DAO의 메소드에서는 저장된 Connection을 가져다가 사용하게 하는 방법
- 361페이지 참고
public void upgradeLevels() throws Exception {
TransactionSynchronizationManager.initSynchronization();
Connection c = DataSourceUtils.getConnection(dataSource);
c.setAutoCommit(false);
try {
List<User> users = userDao.getAll();
for (User user : users) {
if (canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
c.commit();
} catch (Exception e) {
c.rollback();
throw e;
} finally {
DataSourceUtils.releaseConnection(c, dataSource);
TransactionSynchronizationManager.unbindResource(this.dataSource);
TransactionSynchronizationManager.clearSynchronization();
}
}
5.2 트랜잭션 서비스 추상화
정리
- DAO를 사용하는 비즈니스 로직에는 단위 작업을 보장해주는 트랜잭션이 필요하다.
- 트랜잭션의 시작과 종료를 지정하는 일을 트랜잭션 경계설정이라고 한다. 트랜잭션 경계설정은 주로 비즈니스 로직 안에서 일어나는 경우가 많다.
- 시작된 트랜잭션 정보를 담은 오브젝트를 파라미터로 DAO에 전달하는 방법은 매우 비효율적이기 때문에 스프링이 제공하는 트랜잭션 동기화 기법을 활용하는 것이 편리하다.
5.2 트랜잭션 서비스 추상화
- 새로운 문제 발생
- 여러개의 DB를 사용하고 있을 경우, JDBC의 Connection을 이용한 트랜잭션 방식인 로컬 트랜잭션으로는 불가능하다.
- 자바는 JDBC외에 이런 글로벌 트랜잭션을 지원하는 트랜잭션 매니저를 지원하기 위한 API인 JTA를 제공하고 있다.
- 해결 방안
- JDBC API가 아닌 JTA를 사용해 트랜잭션을 관리한다.
5.2 트랜잭션 서비스 추상화
- 또 새로운 문제 발생
- JDBC를 이용한 트랜잭션 관리 코드, JTA를 이용한 트랜잭션 관리 코드를 필요에 따라 다르게 적용해야 한다.
- => UserService는 자신의 로직이 바뀌지 않았음에도 기술환경에 따라서 코드가 바뀌는 코드가 돼버림
- 하이버네이트를 이용한 트랜잭션 관리 코드는 JDBC나 JTA의 코드와는 또 다르다.
5.2 트랜잭션 서비스 추상화
- 다행히도 트랜잭션의 경계설정을 담당하는 코드는 일정한 패턴을 갖는 유사한 구조다.
- => 추상화를 생각해볼 수 있다.
- 추상화 : 하위 시스템의 공통점을 뽑아내서 분리시키는 것
// 스프링의 트랜잭션 추상화 API를 적용한 upgradeLevels()
public void upgradeLevels() {
PlatformTansactionManager transactionManager =
new DataSourceTransactionManager(datasource);
TransactionStatus status =
transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
List<User> users = userDao.getAll();
for (User user : users) {
if (canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
transactionManager.commit(status);
} catch (RuntimeException e) [
transactionManager.rollback(status);
throw e;
}
}
5.2 트랜잭션 서비스 추상화
정리
- 자바에서 사용되는 트랜잭션 API의 종류와 방법은 다양하다. 환경과 서버에 따라서 트랜잭션 방법이 변경되면 경계설정 코드도 함께 변경돼야 한다.
- 트랜잭션 방법에 따라 비즈니스 로직을 담은 코드가 함께 변경되면 단일 책임 원칙에 위배되며, DAO가 사용하는 특정 기술에 대해 강한 결합을 만들어낸다.
- 트랜잭션 경계설정 코드가 비즈니스 로직 코드에 영향을 주지 않게 하려면 스프링이 제공하는 트랜잭션 서비스 추상화를 이용하면 된다.
- 서비스 추상화는 로우레벨의 트랜잭션 기술과 API의 변화에 상관없이 일관된 API를 가진 추상화 계층을 도입한다.
5.4 메일 서비스 추상화
- 새로운 요청사항 : 레벨이 업그레이드되는 사용자에게는 안내 메일을 발송하자.
- JavaMail은 자바의 표준 기술이고 이미 수많은 시스템에 사용돼서 검증된 안정적인 모듈이다. 따라서 JavaMail API를 통해 요청이 들어간다는 보장만 있다면 굳이 테스트 할 때마다 JavaMail을 직접 구동시킬 필요가 없다.
- 스프링이 제공하는 메일 서비스 추상화 인터페이스 활용
정리
- 서비스 추상화는 테스트하기 어려운 JavaMail 같은 기술에도 적용할 수 있다. 테스트를 편리하게 작성하도록 도와주는 것만으로도 서비스 추상화는 가치가 있다.
5.4 메일 서비스 추상화
- 테스트 대상 오브젝트의 메소드가 돌려주는 결과뿐 아니라 테스트 오브젝트가 간접적으로 의존 오브젝트가 넘기는 값과 그 행위 자체에 대해서도 검증하고 싶다면?
- 목 오브젝트를 사용
- 목 오브젝트는 스텁처럼 테스트 오브젝트가 정상적으로 실행되도록 도와주면서, 테스트 오브젝트와 자신의 사이에서 일어나는 커뮤니케이션 내용을 저장해뒀다가 테스트 결과를 검증하는 데 활용할 수 있게 해준다.
5.4 메일 서비스 추상화
public class DummyMailSender implements MailSender {
public void send(SimpleMailMessage mailMessage) throws MailException {
}
public void send(SimpleMailMessage[] mailMessage) throws MailException {
}
}
static class MockMailSender implements MailSender {
private List<String> requests = new ArrayList<String>();
public List<String> getRequests() }
return requests;
}
public void send(SimpleMailMessage mailMessage) throws MailException {
requests.add(mailMessage.getTo()[0]);
}
public void send(SimpleMailMessage[] mailMessage) throws MailException {
}
}
정리
- 테스트 대상이 사용하는 의존 오브젝트를 대체할 수 있도록 만든 오브젝트를 테스트 대역이라고 한다.
- 테스트 대역은 테스트 대상 오브젝트가 원활하게 동작할 수 있도록 도우면서 테스트를 위해 간접적인 정보를 제공해주기도 한다.
- 테스트 대역 중에서 테스트 대상으로부터 전달받은 정보를 검증할 수 있도록 설계된 것을 목 오브젝트라고 한다.
spring-chapter-5
By Ming Kim
spring-chapter-5
- 482