사용자 레벨 관리 기능 추가UserService.upgradeLevels트랜잭션 서비스 추상화트랜잭션 경계설정비지니스 로직 내의 트랜잭션 경계설정트랜잭션 동기화트랜잭션 서비스 추상화서비스 추상화와 단일 책임 원칙수직, 수평 계층구조와 의존관계단일 책임 원칙의 장점메일 서비스 추상화테스트 대역
사용자 레벨 관리 기능 추가
사용자 관리 기능에서 단지 정보를 넣고 검색하는 것 이외에 정기적으로 사용자의 활동내역을 참고해서 레벨을 조정해주는 기능이 필요함
- 사용자의 레벨은 BASIC, SILVER, GOLD 세 가지 중 하나
- 사용자가 처음 가입하면 BASIC 레벨이 되며, 이후 활동에 따라서 한 단계씩 업그레이드 될 수 있음
- 가입 후 50회 이상 로그인을 하면 BASIC → SILVER
- SILVER 레벨이면서 30번 추천을 받으면 GOLD 레벨
- 사용자 레벨의 변경 작업은 일정한 주기를 가지고 일괄적으로 진행됨. 변경 작업 전에는 조건이 충족하더라도 레벨의 변경이 일어나지 않음
Level Enum 클래스
public enum Level { BASIC(1, SILVER), SILVER(2, GOLD), GOLD(3, null); private final int value; private final Level next; Level(int value, Level next) { this.value = value; this.next = next; } public int intValue() { return value; } public Level nextLevel() { return this.next; } public static Level valueOf(int value) { switch(value) { case 1: return BASIC; case 2: return SILVER; case 3: return GOLD; default : throw new AssertionError("Unknown value : " + value); } } }
UserService.upgradeLevels
private void upgradeLevel(User user) { user.upgradeLevel(); userDao.update(user); }
- 그러나 이와 같은 메서드를 유저별로 호출하게 되면 중간에 멈춰버리게 되면 특정 유저는 레벨 변경이 적용되고 특정 유저는 레벨 변경이 적용 안될 수 있음 → 트랜잭션이 필요함
트랜잭션 서비스 추상화
트랜잭션 경계설정
Connection c = dataSource.getConnection(); c.setAutoCommit(false); // start transaction try { PreparedStatement st1 = c.prepareStatement("update users..."); st1.executeUpdate(); ... c.commit(); } catch(Exception e) { c.rollback(); } c.close();
- 트랜잭션의 시작과 종료는 Connection 오브젯트를 통해 이루어짐
- 일반적으로 트랜잭션은 커넥션보다도 존재 범위가 짧다
- 어떤 일련의 작업이 하나의 트랜잭션으로 묶이려면 그 작업이 진행되는 동안 DB 커넥션도 하나만 사용돼야 한다. 하나의 DB 커넥션 안에서 만들어지는 트랜잭션을
로컬 트랜잭션
이라 함
- 글로벌 트랜잭션(JTA). 하나 이상의 DB가 참여하는 트랜잭션을 만들려면 JTA를 사용해야 한다
비지니스 로직 내의 트랜잭션 경계설정
jdbc dao를 이용한 일련의 작업들을 하나의 트랜잭션으로 묶기 위해서는 아래와 같이 서비스 레이어에서 트랜잭션의 경계 설정을 해야 하는데 그러려면 커넥션도 이 메서드 안으로 가져와야 하는 문제가 생김(원래는 DAO에서 커넥션을 관리하고 있었는데)
public void upgradeLevels() throws Exception { (1) DB Connection 생성 (2) 트랜잭션 시작 try { (3) Dao 메서드 호출 (4) 트랜잭션 커밋 } catch(Exception e) { (5) 트랜잭션 롤백 throw e; } finally { (6) DB Connection 종료 } }
- 그리고 위와 같이 코드를 구성하면 DAO 메서드 파라미터에 Connection 객체도 추가되어야 함; 왜냐하면 같은 Connection에서 DB 작업을 진행해야 하기때문(같은 트랜잭션 안에서 동작하기 위해서는)
트랜잭션 동기화
- 위와 같은 문제를 해결하기 위해서 스프링이 제공하는 방법은 독립적인
트랜잭션 동기화
방식임
- 트랜잭션 동기화란 UserService에서 트랜잭션을 시작하기 위해 만든
Connection 오브젝트
를 특별한 저장소에 보관해두고, 이후에 호출되는 DAO의 메서드에서는저장된 Connection
을 가져다가 사용하게 하는 것임 - 트랜잭션 동기화 저장소에서 DB 커넥션을 가져왔을 때 JdbcTemplate 은 Connection을 닫지 않은 채로 작업을 마침
- 트랜잭션 동기화 저장소는 작업 스레드마다 독립적으로 Connection 오브젝트를 저장하고 관리하기 때문에 다중 사용자를 처리하는 서버의 멀티스레드 환경에서도 충돌날 염려가 없음
그리고 트랜잭션이 모두 종료되면, 그때는 동기화를 마치는 것
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(); } }
트랜잭션 서비스 추상화
- 위와 같은 상황에서 대부분의 문제가 해결된 것 같지만 만약 여러개의 DB를 사용하는 상황이 발생한다면 로컬 트랜잭션 만으로는 불가능하고 글로벌 트랜잭션이 필요함
- 자바는 JDBC 외에 글로벌 트랜잭션을 지원하는 트랜잭션 매니저를 지원하기 위한 API인
JTA
를 제공하고 있음
- 글로벌 트랜잭션을 이용하기 위해서는 JTA 를 사용하는 코드로 변경이 필요함. 또한 하이버네이트를 이용한 트랜잭션 관리 코드는 JDBC 와 JTA와는 또 다름 → 사용하는 기술에 따라 트랜잭션 코드가 의존적이게 되어버림 → 추상화가 필요! (
PlatformTransactionManager
)

JDBC의 로컬 트랜잭션을 이용하려면 DataSourceTxManager를 빈으로 주입해주면 됨
public void upgradeLevels() { TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); try { List<User> users = userDao.getAll(); for (User user : users) { if (canUpgradeLevel(user)) { upgradeLevel(user); } } this.transactionManager.commit(status); } catch (RuntimeException e) { this.transactionManager.rollback(status); throw e; } }
서비스 추상화와 단일 책임 원칙
- 추상화란 하위 시스템의 공통점을 뽑아내서 분리시키는 것
- 그렇게 하면 하위 시스템이 어떤것인지 알지 못해도, 하위 시스템이 바뀌더라도 일관된 방법으로 접근 가능
- 스프링의 트랜잭션 추상화(PlatformTransactionManager)
@Transactional
의 사용 케이스를 보면 서비스 계층에 적용을 하고있음- 원래는 커넥션보다 적은 개념이니 dao안에 들어가 있어야 하는게 정상인건데
- 일반적으로 서비스 추상화라고 하면 트랜잭션과 같이 기능은 유사하나 사용 방법이 다른 로우레벨의 다양한 기술에 대해 추상 인터페이스와 일관성 있는 접근 방법을 제공해주는 것을 말함
- 스프링의 트랜잭션 서비스 추상화 기법을 이용해 다양한 트랜잭션 기술을 일관된 방식으로 제어할 수 있음. 설정을 고치는 것만으로 DB 연결 기술, 데이터 액세스 기술, 트랜잭션 기술을 자유롭게 바꿔서 사용할 수 있기 때문에
수직, 수평 계층구조와 의존관계
- UserDao와 UserService가 각각 담당하는 코드의 분리는 내용에 따라 분리한 것으로 같은 계층에서 수평적인 분리라고 볼 수 있음
- 트랜잭션 추상화는 이와 달리, 애플리케이션의 비즈니스 로직과 그 하위에서 동작하는 로우 레벨의 트랜잭션 기술이라는 아예 다른 계층의 특성을 갖는 코드를 분리한 것
- 기술과 서비스에 대한 추상화 기법을 이용하면 특정 기술환경에 종속되지 않는 포터블한 코드를 만들 수 있음

- UserDao와 UserService는 인터페이스와 DI를 통해 연결됨으로써 결합도가 낮아짐
- 즉, 데이터 액세스 로직이 바뀌거나, 심지어 데이터 액세스 기술이 바뀐다고 할지라도 UserService의 코드에는 영향을 주지 않는다는 뜻임
- UserDao는 DB 연결을 생성하는 방법에 대해 독립적임. DataSource 인터페이스와 DI를 통해 추상화된 방식으로 로우레벨의 DB 연결 기술을 사용하기 때문
- 마찬가지로 UserService와 트랜잭션 기술과도 스프링이 제공하는 PlatformTransactionManager 인터페이스를 통한 추상화 계층을 사이에 두고 사용하게 했기 때문에 구체적인 트랜잭션 기술에 독립적인 코드가 되었음
⇒ 애플리케이션 코드를 로우레벨의 기술 서비스와 환경에서 독립시켜줌. 수평적 구분이든 수직적 구분이든 모두 결합도가 낮으며, 서로 영향을 주지 않고 자유롭게 확장될 수 있는 구조를 만들수 있는 데는 스프링의 DI가 중요한 역할을 하고 있음
단일 책임 원칙의 장점
- 어떤 변경이 필요할 때 수정 대상이 명확해짐
- 기술이 바뀌면 기술계층과의 연동을 담당하는 기술 추상화 계층의 설정만 바꿔주면 됨
- 데이터를 가져오는 테이블의 이름이 바뀌었다면 데이터 액세스 로직을 담고 있는 UserDao를 변경하면 됨
메일 서비스 추상화
JavaMail
클래스를 통해서 메일을 전송할 수 있음
- 그러나,
JavaMail
은Interface
가 아니기에(Class
임) 다른 빈으로의 교체가 쉽지 않다
- 테스트를 돌릴 때마다 메일을 보내게 되는 것은 메일서버에도 부담이고, 테스트를 돌리는 시간도 길어지기에 바람직하지 않음 ⇒ 테스트를 위한 서비스 추상화 도입
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()); ... this.mailSender.send(mailMessage); } }
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl"> <property name="host" value="mail.server.com" /> </bean> <!-- 테스트용 mailSender bean--> <bean id="mailSender" class="springbook.user.service.DummyMailSender" />
- 위와 같이 서비스 추상화를 통해 메일 서비스를 도입하면 위에서 계속 이야기했던 것과 마찬가지로
JavaMail
이 아닌 다른 메시징 서버의 API 을 이용해 메일을 전송하는 경우가 생겨도 해당 기술의 API를 이용하는MailSender
구현 클래스를 만들어서 DI 해주면 됨
- 메일 발송에 트랜잭션 개념 도입하기
- 메일을 업그레이드할 사용자를 발견했을 때마다 발송하지 않고 발송 대상을 별도의 목록에 저장 후, 업그레이드 작업 끝났을 때 한번에 메일 전송
- MailSender를 확장해서 메일 전송에 트랜잭션 개념을 적용.
- MailSender를 구현하는 오브젝트를 만들어서 업그레이드 작업 이전에 새로운 메일 전송 작업 시작 알려주고,
- 그 후 send() 메서드 호출해도 실제로 메일 발송하지 않고 저장해둠
- 업그레이드 작업 완료되면 트랜잭션 기능을 가진 MailSender에 지금까지 저장된 메일 모두 발송 or 예외 발생시 모두 취소
테스트 대역
- 테스트 대상이 되는 오브젝트가 또 다른 오브젝트에 의존하는 일은 매우 흔함
- 하나의 오브젝트가 사용하는 오브젝트를 DI에서 의존 오브젝트라고 부름
- 의존한다는 말은 종속되거나 기능을 사용한다는 의미
- 의존 오브젝트를 협력오브젝트(
collaborator
) 라고도 함
- 테스트 시, 간단한 오브젝트 코드를 테스트하는데 너무 거창한 작업이 뒤따르는 경우, 의존 오브젝트를 아무런 일도 하지 않는 빈 오브젝트로 대치해 줄 수 있음