-
Notifications
You must be signed in to change notification settings - Fork 0
Description
사용자 레벨 관리 기능 추가
비스니스 로직
- 사용자 레벨은 BASIC, SILVER, GOLD 세 가지 중 하나
- 사용자가 처음 가입하면 BASIC 레벨이 되며, 이후 활동에 따라서 한 단계씩 업그레이드
- 가입 후 50회 이상 로그인을 하면 BASIC → SILVER
- SILVER 레벨이면서 30번 이상 추천을 받으면 GOLD 레벨
- 사용자 레벨의 변경 작업은 일정한 주기를 가지고 일괄적으로 진행
변경 작업 전에는 조건을 충족하더라도 레벨의 변경이 일어나지 않음
필드 추가
Level Enum
public enum Level {
BASIC(1), SILVER(2), GOLD(3);
private final int value;
Level(int value) {
this.value = value;
}
public int intValue(){
return value;
}
public static Level valueOf(int value){
switch(value){
case 1: return BASIC;
case 2: return SILVER;
case 3: return GOLD;
default: throw new AssertionError("Unkown value:" + value);
}
}
}UserDaoJdbc 수정
public class UserDaoJdbc implements UserDao {
...
private RowMapper<User> userMapper =
new RowMapper<User>(){
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
...
user.setLevel(Level.valueOf(rs.getInt("level")));
return user;
}
};
public void add(User user){
this.jdbcTemplate.update(
"insert into users(id, name, password, level, login, recommend) "
+ "values(?,?,?,?,?,?)",
user.getId(), user.getName(), user.getPassword(),
user.getLevel().intValue(), user.getLogin(), user.getRecommend());
}
}- JDBC가 사용하는 SQL은 컴파일 과정에서는 자동으로 검증이 되지 않는 단순 문자열에 불과
- SQL 문장이 완성돼서 DB에 전달되기 전까지는 문법 오류나 오타조차 발견하기 힘들다.
- JPA를 사용한다면 @query를 사용해서 컴파일시점에 문법 오류를 잡을 수도 있다.
UserService.upgradeLevels()
- 사용자 관리 로직을 담기 위한 UserService 클래스를 추가하자.
- DAO는 데이터를 어떻게 가져오고 조작할지를 다루는 곳이라서 비즈니스 로직을 두는 곳이 아니다.
upgradeLevels() 메서드
public void upgradeLevels(){
List<User> users = userDao.getAll();
for (User user : users) {
Boolean changed = false
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;
}
if(changed){
userDao.update(user);
}
}
}- 모든 사용자 정보를 DAO에서 가져온 후에 한 명씩 레벨 변경 작업을 수행
코드 개선
작성된 코드를 살펴볼 때는 다음과 같은 질문을 해보자.
- 코드에 중복된 부분은 없는가?
- 코드가 무엇을 하는지 이해하기 불편하지 않은가?
- 코드가 자신이 있어야 할 자리에 있는가?
- 앞으로 변경이 일어난다면 어떤 것이 있을 수 있고, 그 변화에 쉽게 대응할 수 있게 작성되어 있는가?
upgradeLevels() 메서드 리팩토링
public void upgradeLevels(){
List<User> users = userDao.getAll();
for(User user : users){
if(canUpgradeLevel(user)){
upgradeLevel(user);
}
}
}
private boolean canUpgradeLevel(User user){
Level level = user.getLevel();
switch(level){
case BASIC: return user.getLogin >= 50;
case SILVER: return user.getRecommend() >= 30;
case GOLD: return false;
default: throw new IllegalArgumentException(...);
}
}
private void upgradeLevel(User user){
if(user.getLevel() == Level.BASIC) user.setLevel(Level.SILVER);
else if(user.getLevel() == Level.SILVER) user.setLevel(Level.GOLD);
userDao.update(user);
}-
canUpgradeLevel() 로직에서 처리할 수 없는 레벨인 경우에 예외를 던져준다.
-
새로운 레벨이 추가됐지만 업그레이드 로직을 추가하지 않았다면 예외가 발생할 테니 쉽게 확인 가능
-
아니면 이런 테스트 로직이 있어도 괜찮을 것이다.
@Test public void numOfLevel(){ assertEquals(Level.values().size(), 3); }
- Level Enum에 항목이 추가되면 이 테스트 케이스에서 통과하지 못하고 추가된 항목에 따라 변경할 부분을 수정하면 된다.
-
upgradeLevel()는 레벨이 늘어나면 if 문이 점점 길어질 것이고, 레벨 변경 시 사용자 오브젝트에서 level 필드 외의 값도 같이 변경해야 한다면 if 조건 뒤에 붙는 내용도 점점 길어질 것이다.
-
레벨의 순서와 다음 단계 레벨이 무엇인지를 결정하는 일은 Level에게 맡기자.
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("Unkown value:" + value); } } }
-
User의 내부 정보가 변경되는 것은 UserService 보다는 User가 스스로 다루는 게 적절
public class User { ... public void upgradeLevel(){ Level nextLevel = this.level.nextLevel(); if(nextLevel == null){ throw new IllegralStateException(...); } this.level = nextLevel; } }
- User에 업그레이드 작업을 담당하는 독립적인 메서드를 두고 사용하면 업그레이드 시 기타 정보도 변경이 필요해지면 그 장점이 무엇인지 알 수 있을 것이다.
참고
- 객체지향적인 코드는 다른 오브젝트의 데이터를 가져와서 작업하는 대신 데이터를 갖고 있는 다른 오브젝트에게 작업을 해달라고 요청한다.
- 오브젝트에게 데이터를 요구하지 말고 작업을 요청하라는 것이 객체지향 프로그래밍의 가장 기본이 되는 원리
- UserService는 User에게 레벨 업그레이드 작업을 해달라고 요청하고, User는 Level에게 다음 레벨이 무엇인지 알려달라고 요청하는 방식으로 동작하게 하는 것이 바람직하다.
상수의 도입
public static final int MIN_LOGCOUNT_FOR_SILVER = 50;
public static int MIN_RECOMMEND_FOR_GOLD = 30;
private boolean canUpgradeLevel(User user){
Level level = user.getLevel();
switch(level){
case BASIC: return user.getLogin >= MIN_LOGCOUNT_FOR_SILVER;
case SILVER: return user.getRecommend() >= MIN_RECOMMEND_FOR_GOLD;
case GOLD: return false;
default: throw new IllegalArgumentException(...);
}
}트랜잭션 서비스 추상화
- 정기 사용자 레벨 관리 작업을 수행하는 도중에 네트워크가 끊기거나 서버에 장애가 생겨서 작업을 완료할 수 없다면, 그때까지 변경된 사용자의 레벨은 어떻게 할 것인가?
- 책에서는 작업 중간에 문제가 발생하면 그때까지 변경 작업도 모두 취소시키도록 결정함.
- 즉 하나의 트랜잭션에 묶어서 간다는 말이다.
모 아니면 도
테스트용 UserService 대역
- 간단히 UserService를 상속해서 테스트에 필요한 기능을 추가하도록 일부 메서드를 오버라이딩하는 방법을 사용하자.
- 테스트용 UserService의 서브클래스는 UserService 기능의 일부를 오버라이딩해서 특정 시점에서 강제로 예외가 발생(롤백되도록)만들 것이다.
- 테스트용으로 작업 중간에 예외가 발생하도록 하면 이전 변경 작업은 취소되지 않고 그대로 남는 것을 확인할 수 있을 것이다.
문제는 트랜잭션
- 모든 사용자의 레벨을 업그레이드하는 작업인 upgradeLevels()가 하나의 트랜잭션 안에서 동작하지 않았기 때문에 발생한 문제이다.
- 트랜잭션이란 더 이상 나눌 수 없는 단위 작업을 말한다.
- 작업을 쪼개서 작은 단위로 만들 수 없다는 것은 트랜잭션의 원자성을 기억하자.
- 중간에 예외가 발생해서 작업을 완료할 수 없다면 아예 작업이 시작되지 않은 것처럼 초기 상태로 돌려놔야 한다
트랜잭션 경계설정
- 하나의 SQL 명령을 처리하는 경우는 DB가 트랜잭션을 보장해준다.
- 여러 개의 SQL이 사용되는 작업을 하나의 트랜잭션을 취급해야 하는 경우도 있다.
- 이체를 할 때는 출금계좌의 잔고는 이체금액만큼 줄어들고, 입금계좌에는 이체금액만큼 증가
- DB에 두 번 요청을 보냄(빼고, 더하고)
두 가지 작업을 하나의 트랜잭션으로 묶기
-
두 번째 SQL이 성공적으로 DB에서 수행되기 전에 문제가 발생할 경우에는 앞에서 처리한 SQL도 취소해야 함
→ 트랜잭션 롤백
-
여러 개의 SQL을 하나의 트랜잭션으로 처리하는 경우에 모든 SQL 수행작업이 다 성공적으로 마무리됐다고 DB에 알려줘서 작업을 확정시켜야 한다.
→ 트랜잭션 커밋
JDBC 트랜잭션의 트랜잭션 경계설정
- 모든 트랜잭션은 시작하는 시점과(begin)과 끝나는 시점(rollback, commit)이 있다.
- 애플리케이션 내에서 트랜잭션이 시작되고 끝나는 위치를 트랜잭션의 경계라고 부른다.
- 복잡한 로직의 흐름 사이에서 정확하게 트랜잭션 경계를 설정하는 일은 매우 중요한 작업
- JDBC의 트랜잭션은 하나의 Connection을 가져와 사용하다가 닫는 사이에서 일어난다.
- 트랜잭션의 시작과 종료는 Connection을 통해 이뤄지기 때문이다.
- JDBC에서 트랜잭션을 시작하려면 자동커밋 옵션을 false로 만들어주자
Connection.setAutoCommit(false) - JDBC의 기본 설정은 DB 작업을 수행한 직후에 자동으로 커밋이 되도록 되어 있다.
- 작업마다 커밋해서 트랜잭션을 끝내버리므로 여러 개의 DB 작업을 모아서 트랜잭션을 만드는 기능이 꺼져있는 것이다.
- 트랜잭션이 한 번 시작되면 commit() 또는 rollback()이 호출될 때까지의 작업이 하나의 트랜잭션으로 묶인다.
- setAutoCommit(false)로 트랜잭션 시작을 선언하고, commit() 또는 rollback()으로 트랜잭션을 종료하는 작업을 트랜잭션의 경계설정이라고 한다.
- 트랜잭션의 경계는 하나의 Connection이 만들어지고 닫히는 범위 안에 존재한다는 점을 꼭 기억하자.
- 이렇게 하나의 DB 커넥션 안에서 만들어지는 트랜잭션을 로컬 트랜잭션이라고도 한다.
UserService와 UserDao의 트랜잭션 문제
- UserService.upgradeLevels()에 트랜잭션이 적용되지 않았던 이유는 지금까지 만든 코드 어디에도 트랜잭션을 시작하고, 커밋하고, 롤백하는 트랜잭션 경계설정 코드가 존재하지 않기 때문이다.
- 여러 번 DB에 업데이트를 해야 하는 작업을 하나의 트랜잭션으로 만들기 위해서는 그 작업이 진행되는 동안 DB 커넥션도 하나만 사용해야 한다.
비즈니스 로직 내의 트랜잭션 경계설정
- UserService와 UserDao를 그대로 둔 채로 트랜잭션을 적용하려면 결국 트랜잭션의 경계설정 작업을 UserService 쪽으로 가져와야 한다.
- 흐름을 볼 때 upgradeLevels()의 시작과 함께 트랜잭션이 시작하고 메서드를 빠져나올 때 트랜잭션이 종료돼야 하기 때문이다.
upgradeLevels의 트랜잭션 경계설정 구조
public void upgradeLevels() throws Exception {
(1) DB Connection 생성
(2) 트랜잭션 시작
try {
(3) DAO 메서드 호출
(4) 트랜잭션 커밋
} catch (Exception e) {
(5) 트랜잭션 롤백
throw e;
} finally {
(6) DB Connection 종료
}
}- 트랜잭션을 사용하는 전형적인 JDBC 코드의 구조
- 생성된 Connection을 가지고 데이터 액세스 작업을 진행하는 UserDao의 update() 안에 있어야 한다.
- 트랜잭션 때문에 DB 커넥션과 트랜잭션 관련 코드는 어쩔 수 없이 UserService로 가져옴.
- 순수한 데이터 액세스 로직은 UserDao에 둔다.
- 기존의 JdbcTemplate처럼 매번 새로운 Connection을 만들어버리면, upgradeLevels() 안에서 시작한 트랜잭션과 무관한 별개의 트랜잭션이 만들어지므로 주의해야 한다.
- UserService에서 만든 Connection을 UserDao에서 사용하려면 DAO 메서드를 호출할 때마다 Connection 오브젝트를 파라미터로 전달해줘야 한다.
- upgradeLevels() 안에서 트랜잭션의 경계설정 작업이 일어나야 하고, 그 트랜잭션을 갖고 있는 DB 커넥션을 이용하도록 해야만 별도의 클래스에 만들어둔 DAO 내의 코드도 트랜잭션이 적용될 테니 결국 이 방법을 사용할 수밖에 없다.
UserService 트랜잭션 경계설정의 문제점
- DB 커넥션을 비롯한 리소스의 깔끔한 처리를 가능하게 했던 JdbcTemplate을 더이상 사용 불가
- try/catch/finally 블록은 이제 UserService에 존재
- UserService의 코드는 JDBC 작업 코드의 전형적인 문제점을 갖게 된다.
- DAO의 메서드와 비즈니스 로직을 담고 있는 UserService의 메서드에 Connection 파라미터가 추가되어야 한다는 점
- 트랜잭션이 필요한 작업에 참여하는 UserService의 메서드는 Connection 파라미터로 지저분해질 것임
- Connection 파라미터가 UserDao 인터페이스 메서드에 추가되면 UserDao는 더이상 데이터 액세스 기술에 독립적일 수 없다.
- JPA나 하이버네이트로 UserDao의 구현 방식을 변경하려고 하면 Connection 대신 EntityManager나 Session 오브젝트를 UserDao 메서드가 전달받도록 해야 한다.
- DAO 메서드에 Connection 파라미터를 받게 하면 테스트 코드에도 영향을 미친다.
- 테스트 코드에서 직접 Connection 오브젝트를 일일이 만들어서 DAO 메서드를 호출하도록 모두 변경해야 한다.
트랜잭션 동기화
Connection 파라미터 제거
- 스프링이 제안하는 방법은 독립적인 트랜잭션 동기화(Transaction Synchronization) 방식이다.
- 트랜잭션 동기화란 UserService에서 트랜잭션을 시작하기 위해 만든 Connection을 특별한 저장소에 보관하고, 이후에 호출되는 DAO의 메서드에서는 저장된 Connection을 가져다가 사용하게 하는 것이다.
- 정확히는 DAO가 사용하는 JdbcTemplate이 트랜잭션 동기화 방식을 이용하도록 하는 것이다.
- 트랜잭션이 모두 종료되면, 그때는 동기화를 마치면 된다.
트랜잭션 동기화를 사용한 경우의 작업 흐름
- UserService에서 Connection 생성
- 생성한 Connection을 트랜잭션 동기화 저장소에 저장하고 Connection.setAutoCommit(false) 호출해서 트랜잭션을 시작시킨 후에 DAO 메서드 호출
- update() 호출되고, update() 내부의 JdbcTemplate는 트랜잭션 동기화 저장소에 현재 시작된 트랜잭션을 가진 Connection이 존재하는지 확인
- upgradeLevels() 시작 부분에서 저장해둔 Connection을 발견하고 이를 가져와서 Connection을 이용해서 SQL을 실행
- 트랜잭션 동기화 저장소에서 DB 커넥션을 가져왔을 때는 JdbcTemplate은 Connection을 닫지 않은 채로 작업을 마친다.
- 여전히 Connection은 열려 있고 트랜잭션은 진행 중인 채로 트랜잭션 동기화 저장소에 저장되어 있음.
- 두 번째 update()가 호출되면 마찬가지로 트랜잭션 동기화 저장소에서 Connection을 가져와서 사용
- 마지막 update()도 같은 트랜잭션을 가진 Connection을 가져와서 사용
- 작업을 정상적으로 끝났다면 UserService는 이제 Connection.commit() 호출
- 예외상황이 발생하면 UserService는 Connection.rollback() 호출하고 트랜잭션을 종료
- 이때도 9번과 같이 트랜잭션 저장소에 저장된 동기화된 Connection을 제거해줘야 함.
- 마지막으로 트랜잭션 저장소가 더 이상 Connection을 저장하지 않도록 제거
스레드마다 독립적인 Connection
- 트랜잭션 동기화 저장소는 작업 스레드마다 독립적으로 Connection 오브젝트를 저장하고 관리
- 다중 사용자를 처리하는 서버의 멀티스레드 환경에서도 충돌이 날 염려는 없다.
- 트랜잭션 도기화 기법을 사용하면 파라미터에 일일이 Connection을 전달할 필요가 없어진다.
트랜잭션 동기화 적용
- 스프링은 JdbcTemplate과 더불이 트랜잭션 동기화 기능을 지원하는 간단한 유틸리테 메서드 제공
- 트랜잭션 동기화 방식을 적용한 UserService
private DateSource dataSource;
public void setDateSource(DataSource dataSource){
this.dataSource = dataSource;
}
public void upgradeLevels() throws Exception {
//(1) DB Connection 생성
TransactionSynchronizationManager.initSynchronization();
//(2) 트랜잭션 시작
Connection c = DataSourceUtils.getConnnection(dataSource);
c.setAutoCommit(false);
try {
// (3) DAO 메서드 호출
List<User> users = userDao.getAll();
for (User user : users) {
if(canUpgradeLevel(user)){
upgradeLevel(user);
}
}
// (4) 트랜잭션 커밋
c.commit();
} catch (Exception e){
// (5) 트랜잭션 롤백
c.rollback();
throw e;
} finally {
// (6) DB Connection 종료
DataSourceUtils.releaseConnection(c, dataSource);
TranscationSynchronizationManager.unbindResource(this.dataSource);
TranscationSynchronizationManager.clearSynchronization();
}
}- 스프링이 제공하는 트랜잭션 동기화 관리 클래스는 TransactionSychronizationManager
- 이 클래스를 이용해 먼저 트랜잭션 동기화 작업을 초기화하도록 요청
- DataSourceUtils에서 제공하는 getConnection()을 통해 DB 커넥션 생성
- DataSource에서 직접 가져오지 않고, 스프링이 제공하는 유틸리티를 쓰는 이뉴는 DataSourceUtils의 getConnection()은 Connection을 생성해줄 뿐만 아니라 트랜잭션 동기화에 사용하도록 저장소에 바인딩해주기 때문이다.
- 트랜잭션 동기화가 되어 있는 채로 JdbcTemplate을 사용하면 JdbcTemplate의 작업에서 동기화시킨 DB 커넥션을 사용
- UserDao에서는 upgradeLevels()에서 만든 Connection을 사용하고 같은 트랜잭션에 참여
- JDBC의 트랜잭션 경계설정 메서드를 사용해 트랜잭션을 이용하는 전형적인 코드에 간단한 트랜잭션 동기화 작업만 붙여줌으로써, 지저분한 Connection 파라미터의 문제를 깔끔히 해결
JdbcTemplate과 트랜잭션 동기화
- 트랜잭션 동기화를 시작해놓았다면 그때부터 실행되는 JdbcTemplate의 메서드에서는 직접 DB 커넥션을 만드는 대신 트랜잭션 동기화 저장소에 들어 있는 DB 커넥션을 가져와서 사용한다.
- 이를 통해 이미 시작된 트랜잭션에 참여하는 것이다.
- DAO를 사용할 때 트랜잭션이 굳이 필요없다면 바로 호출해서 사용해도 되고, DAO 외부에서 트랜잭션을 만들고 이를 관리할 필요가 있다면 미리 DB 커넥션을 생성한 다음 트랜잭션 동기화를 해주고 사용하면 된다.
- 트랜잭션 동기화를 하면 DAO에서 사용하는 JdbcTemplate은 자동으로 트랜잭션 안에서 동작할 것이다.
- 트랜잭션 적용 여부에 맞춰 UserDao를 수정할 필요는 없어진다.
트랜잭션 서비스 추상화
기술과 환경에 종속되는 트랜잭션 경계설정 코드
- 한 개 이상의 DB로의 작업을 하나의 트랜잭션으로 만드는 건 JDBC의 Connection을 이용한 트랜잭션 방식인 로컬 트랜잭션으로는 불가능
- 로컬 트랜잭션은 하나의 DB Connection에 종속되기 때문이다.
- 각 DB와 독립적으로 만들어지는 Connection을 통해서가 아니라 별도의 트랜잭션 관리자를 통해 트랜잭션을 관리하는 글로벌 트랜잭션 방식을 사용해야 한다.
- 글로벌 트랜잭션을 이용하면 트랜잭션 매니저를 통해 여러 개의 DB가 참여하는 작업을 하나의 트랜잭션으로 만들 수 있다.
분산 트랜잭션 관리(글로벌 트랜잭션)
- 자바는 글로벌 트랜잭션을 지원하는 트랜잭션 매니저를 지원하기 위한 API인 JTA(Java Transaction API)를 제공하고 있다.
- 트랜잭션은 JDBC나 JMS API를 사용해서 직접 제어하지 않고 JTA를 통해 트랜잭션 매니저가 관리
- 트랜잭션 매니저는 DB와 메시징 서버를 제어하고 관리하는 각각의 리소스 매니저와 XA 프로토콜을 통해 연결
- 트랜잭션 매니저가 실제 DB와 메시징 서버의 트랜잭션을 종합적으로 제어 가능
- JTA를 이용해 트랜잭션 매니저를 활용하면 여러 개의 DB나 메시징 서버에 대한 작업을 하나의 트랜잭션으로 통합하는 분산 트랜잭션 또는 글러벌 트랜잭션이 가능해진다.
- 11장에서 자세히 다룸.
트랜잭션 API의 의존관계 문제와 해결책
- 현재 UserService에서 트랜잭션 경계 설정을 해야할 필요가 생기면 특정 데이터 액세스 기술에 종속되는 구조
- JDBC 트랜잭션 API와 JdbcTemplate과 동기화하는 API로 인해 JDBC DAO에 의존하게 됨.
- DAO 클래스의 구현 기술이 JDBC에서 하이버네이트와 같은 다른 기술로 바뀌면 UserService는 영향을 받지 않는다.
- 하지만, JDBC에 종속적인 Connection을 이용한 트랜잭션 코드가 UserService에 등장하면서부터 UserService는 UserDaoJdbc에 간접적으로 의존하는 코드가 돼버렸다.
- UserService가 특정 트랜잭션 방법에 의존적이지 않고 독립적일 수 있게 만들기 위해서는 여러 기술의 사용 방법에 공통점을 찾고 이를 추상화해볼 수 있다.
- 트랜잭션의 경계설정을 담당하는 코드는 일정한 패턴을 갖는 유사한 구조이기 때문이다.
- DB에서 제공하는 DB 클라이언트 라이브러리와 API는 서로 전혀 호환되지 않지만 모두 SQL을 사용하는 방식이라는 공통점이 있다.
- 이 공통점을 뽑아내 추상화한 것이 JDBC
- 애플리케이션 코드에서는 트랜잭션 추상화 계층이 제공하는 API를 이용해 트랜잭션을 이용하게 만들어준다면 특정 기술에 종속되지 않는 트랜잭션 경계설정 코드를 만들 수 있을 것이다.
스프링의 트랜잭션 서비스 추상화
- 스프링은 트랜잭션 기술의 공통점을 담은 트랜잭션 추상화 기술을 제공하고 있다.
- 이를 이용하면 애플리케이션에서 직접 각 기술의 트랜잭션 API를 이용하지 않고도, 일관된 방식으로 트랜잭션을 제어하는 트랜잭션 경계설정 작업이 가능해진다.
스프링의 트랜잭션 추상화 API를 적용한 upgradeLevels()
public void upgradeLevels(){
PlatformTransactionManager transactionManager =
new DataSourceTransactionManger(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 (Exception e){
transactionManager.rollback(status);
throw e;
}
}- 스프링이 제공하는 트랜잭션 경계설정을 위한 추상 인터페이스는 PlatformTransactionManager다.
- JDBC의 로컬 트랜잭션을 이용한다면 PlatformTransactionManager를 구현한 DataSourceTransactionManager를 사용하면 된다.
- 사용할 DB의 DataSource를 생성자 파라미터로 넣으면서 DataSourceTransactionManager의 오브젝트를 만든다.
- PlatformTransactionManager에서는 트랜잭션을 가져오는 요청인 getConnection() 메서드만 호출하면 트랜잭션이 시작된다.
- 필요에 따라 트랜잭션 매니저가 DB 커넥션을 가져오는 작업도 같이 수행해주기 때문이다.
- 트랜잭션을 가져온다는 것은 일단 트랜잭션을 시작한다는 의미라고 생각하자.
- 파라미터로 넘기는 DefaultTransactionDefinition 오브젝트는 트랜잭션에 대한 속성을 담고 있다.
- 시작된 트랜잭션은 TransactionStatus 타입의 변수에 저장된다.
- 트랜잭션에 대한 조작이 필요할 때 PlatformTransactionManager 메서드의 파라미터로 전달해주면 된다.
- PlatformTransactionManager로 시작한 트랜잭션은 트랜잭션 동기화 저장소에 저장된다.
- PlatformTransactionManager를 구현한 DataSourceTransactionManager는 JdbcTemplate에서 사용될 수 있는 방식으로 트랜잭션을 관리해준다.
- PlatformTransactionManager를 통해 시작한 트랜잭션은 UserDao의 JdbcTemplate 안에서 사용됨.
- 트랜잭션 작업을 모두 수행한 후에는 트랜잭션을 만들 때 돌려받은 TransactionStatus를 파라미터로 해서 PlatformTransactionManager의 commit()을 호출하면 된다.
- 예외가 발생하면 rollback()을 호출하자.
트랜잭션 기술 설정의 분리
- 트랜잭션 추상화 API를 적용한 UserService에 JTA를 이용한 글로벌 트랜잭션으로 적용하거나 Dao를 하이버네이트나 JPA를 적용했다면
- 각 데이터 액세스 기술의 PlatformTransactionManager 인터페이스를 구현한 구현 클래스를 사용하면 됨
- 하지만 어떤 트랜잭션 매니저 구현 클래스를 사용할지 UserService 코드가 알고 있는 것은 DI 위배
- 사용할 구체적인 클래스를 스스로 결정하고 생성하지 말고, 컨테이너를 통해 외부에서 제공받는 스프링 DI를 사용하자.
- 어떤 클래스든 스프링의 빈으로 등록할 때 먼저 검토해야 하는 것이 있다.
- 바로 싱글톤으로 만들어져 여러 스레드에서 동시에 사용해도 괜찮은가 하는 점이다.
- 상태를 갖고 있고, 멀티 스레드 환경에서 안전하지 않은 클래스를 빈으로 등록하면 심각한 문제 발생
- 스프링이 제공하는 모든 PlatformTransactionManager의 구현 클래스는 싱글톤으로 사용 가능
서비스 추상화와 단일 책임 원칙
수직, 수평 계층구조와 의존관계
- 기술과 서비스에 대한 추상화 기법을 이용하면 특정 기술환경에 종속되지 않는 포터블한 코드를 만들 수 있다.
- UserService와 트랜잭션 기술과도 스프링이 제공하는 PlatformTransactionManager 인터페이스를 통한 추상화 계층을 사이에 두고 사용하게 했기 때문에, 구체적인 트랜잭션 기술에 독립적인 코드가 됐다.
- 애플리케이션 로직의 종류에 따른 수평적인 구분이든, 로직과 기술이라는 수직적인 구부이든 모든 결합도가 낮으며, 서로 영향을 주지 않고 자유롭게 확장될 수 있는 구조를 만들 수 있는 데는 스프링의 DI가 중요한 역할
- DI의 가치는 관심, 책임, 성격이 다른 코드를 깔끔하게 분리하는 데 있다.
단일 책임 원칙(Single Responsibility Principle)
- 하나의 모듈은 한 가지 책임을 가져야 한다.
- 하나의 모듈이 바뀌는 이유는 한 가지여야 한다.
- 트랜잭션 서비스의 추상화 방식을 도입하고, DI를 통해 외부에서 제어하도록 만들고 나서는 UserService가 바뀔 이유는 한 가지 뿐이다.
- 사용자 관리 로직이 바뀌거나 추가되지 않은 한 UserService는 변경되지 않는다.
단일 책임 원칙의 장점
- 단일 책임 원칙을 잘 지키고 있다면, 어떤 변경이 필요할 때 수정 대상이 명확해진다.
- 기술이 바뀌면 기술 계층과의 연동을 담당하는 기술 추상화 계층의 설정만 바꿔주면 된다.
- 데이터를 가져오는 테이블의 이름이 바뀌었다면 데이터 액세스 로직을 담고 있는 UserDao를 변경하면 된다.
스프링 DI
-
적절하게 책임과 관심이 다른 코드를 분리하고, 서로 영향을 주지 않도록 다양한 추상화 기법을 도입하고
애플리케이션 로직과 기술/환경을 분리하는 등의 작업은 갈수록 복잡해지는 엔터프라이즈 애플리케이션에는
반드시 필요→ 이를 위한 핵심적인 도구가 스프링이 제공하는 DI
-
스프링의 DI가 없다면 인터페이스를 도입해서 나름 추상화를 했더라도 적지 않은 코드 사이의 결합이 남아 있게 된다.
-
스프링을 DI 프레임워크라고 부르는 이유는 외부 설정정보를 통한 런타임 오브젝트 DI라는 단순한 기능을 제공하기 때문이 아니다.
- 오히려 스프링이 DI에 담긴 원칙과 이를 응용하는 프로그래밍 모델을 자바 엔터프라이즈 기술의 많은 문제를 해결하는 데 적극적으로 활용하고 있기 때문이다.
- DI를 활용해서 깔끔하고 유연한 코드와 설계를 만들어낼 수 있도록 지우너하고 지지해준다.
정리
- 비즈니스 로직을 담은 코드는 데이터 액세스 로직을 담은 코드와 깔끔하게 분리되는 것이 바람직
- DAO를 사용하는 비즈니스 로직에는 단위 작업을 보장해주는 트랜잭션이 필요
- 트랜잭션의 시작과 종료를 지정하는 일을 트랜잭션 경계설정이라고 한다.
- 주로 비스니스 로직 안에서 일어나는 경우가 많다.
- 자바에서 사용되는 트랜잭션 API의 종류와 방법은 다양하다.
- 트랜잭션 경계설정 코드가 비즈니스 로직 코드에 영향을 주지 않게 하려면 스프링이 제공하는 트랜잭션 서비스 추상화를 이용하면 된다.
- 서비스 추상화는 로우레벨의 트랜잭션 기술과 API의 변화에 상관없이 일관된 API를 가진 추상화 계층을 도입
- 서비스 추상화는 테스트하기 어려운 JavaMail 같은 기술에도 적용할 수 있다.
- 테스트 대역 중에서 테스트 대상으로부터 전달받은 정보를 검증할 수 있도록 설계된 것을 Mock 오브젝트라고 한다.

