2020-5-24 토비의 스프링 5장 서비스추상화
서비스 추상화
- 요구사항
- 사용자의 등급 GOLD, SIVER, BASIC
- 처음 가입하면 베이직. 50회 이상 로그인하면 베이식 => 실버
- 실버에서 30번 추천 => 골드
- 레벨의 변경작업은 일정한 주기가 있다.
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("Unknown value : " + value);
}
}
}
- Enum으로 정의하면 내부적으로는 Level타입의 오브젝트이고, DB에 저장할때는 int타입으로 가지고 있다.
- intValue를 통해 DB에 저장될때 Level타입을 int값으로 변환하고, DB에서 값을 조회할때 valueOf를 통해 다시 int값을 Level타입으로 변경해야한다.
- 점점 복잡해지므로 비즈니스 로직을 관리하는 UserService를 만들어서 관리한다. UserService는 UserDao인터페이스를 주입받는데, 구현클래스가 바뀌어도 영향을 받지 않게 하기 위해 이렇게했다.
public class UserService {
private UserDao userDao;
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);
}
}
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
-
upgradeLevels의 경우 여러가지 문제가 있다. 여러가지의 로직들이 섞여있다. ex) 현재레벨 확인 및 업그레이드 조건확인.
-
Level이 많아질수록 if문이 늘어나는구조다.
-
리팩토링
-
private static final int MIN_LOGOUT_FOR_SILVER = 50; private static final int MIN_RECOMMEND_FOR_GOLD = 30; public void upgradeLevels() { List<User> users = userDao.getAll(); for (User user : users) { if (canUpgradeLevel(user)) upgradeLevel(user); } } 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); } private boolean canUpgradeLevel(User user) { Level currentLevel = user.getLevel(); switch (currentLevel) { case BASIC: return (user.getLogin() >= MIN_LOGOUT_FOR_SILVER); case SILVER: return (user.getRecommend() >= MIN_RECOMMEND_FOR_GOLD); case GOLD: return false; default: throw new IllegalArgumentException("Unknown Level : " + currentLevel); } }
-
위의 메서드에서 레벨정보를 다음 단계로 변경하고, 변경된 오브젝트를 DB에 업데이트하는 2가지 작업을 한다.
-
updgradeLevel에서 사용자의 level필드를 변경하고 예외처리가 없다. Enum Level에서 레벨의 순서와 다음 레벨이 무엇인지 결정하는 일을 맡기자.
- 값이 정해져있는것들은 상수를 쓰자.
-
-
레벨 리팩토링
-
public enum Level { GOLD(3, null), SILVER(2, GOLD), BASIC(1, SILVER); 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 getNext() { return next; } } //User.class public void upgradeLevel() { Level nextLevel = this.level.nextLevel(); if (nextLevel == null) throw new IllegalStateException(this.level + " can't upgrade"); else this.level = nextLevel; } //UserService private void upgradeLevel(User user) { user.upgradeLevel(); userDao.update(user); }
-
Level이 다음 Level의 정보를 담을 수 있게 필드추가한다.
- User가 직접레벨의 상태를 관리할 수 있게해준다. UserService에서는 canUpgradeLevel()로 업그레이드 가능 여부만 체크해주면 된다.
-
트랜잭션
-
여러개의 SQL들이 처리되고 중간에 취소가 되면 트랜잭션 롤백이된다.
-
모든 SQL이 다 수행되면 DB에 알려주어 작업을 확정짓는다. 이를 트랜잭션 커밋이라 한다.
-
트랜잭션은 시작할때는 하나이지만 끝내는건 2가지다. 롤백과 커밋
-
JDBC에서는 Connectino에서 트랜잭션 시작과 종료가 이뤄진다.
- JDBC에서는 기본적으로 setAutoCommit이 true로 되어있다. false로 설정하면 하나의 트랜잭션으로 걸려서 Rollback, Commit()이 일어날때까지 트랜잭션이 하나로 유지된다.
- setAutoCommit(false)로 트랜잭션 시작하고, commit(), rollback()으로 종료하는걸 트랜잭션 경계설정이라한다.
- 트랜잭션은 Connectino에서 만들어지는데, JdbcTemplate을 사용하는 UserDao는 각 메서드마다 하나의 트랜잭션이 독립적으로 실행되는거다.
-
DAO를 호출할때마다 Connection을 파라미터로 전달받는것은 좋은 방식이 아니다. 독립적인 트랜잭션 동기화 방식을 사용해라
-
UserService에서 트랜잭션을 시작하기 위해 만든 Connection오브젝트를 특별한 저장소에 보관해두고, 호출되는 DAO 메서드에서는 저장된 Connection을 가져다가 사용하는것.
-
UserService에서 Connection생성 => 동기화 저장소에 보관, setAutoCommit(false)로 트랜잭션 시작 => dao메서드를 호출하면 저장소에 Connection존재하는지 확인 => Connection을 이용해 PreparedStatement를 만들고 수정 SQL실행 => Connection을 닫지않고 반환. => 다른 update들이 발생할때, Connection 계속 재사용하다가 모든 작업이 끝나면 commit()을 호출해서 트랜잭션을 완료한다.
-
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 { TransactionSynchronizationManager.unbindResource(this.dataSource); TransactionSynchronizationManager.clear(); } } private DataSource dataSource; public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; }
- Datasource에 대한 DI를 직접해줘야한다. DatasourceUtils를 쓰는 이유는 getConnection()를 쓰면 생성뿐만 아니라 트랜잭션 동기화에 사용되도록 저장소에 바인딩도 해준다.
-
-
JdbcTemplate의 경우. 트랜재겻ㄴ 동기화 저장소에 등록된 DB커넥션이나 트랜잭션이 없으면 직접 DB커넥션을 만들고 트랜잭션을 시작한다. 만약 트랜잭션동기화를 시작했으면 JdbcTemplate은 직접 DB커넥션을 만드는 대신 트랜잭션 동기화 저장소에 있는 DB커넥션들을 가져온다.
트랜잭션 서비스 추상화
- DB가 여러개이거나 하이버네이트와 같은 서비스를 쓰게 되면 변화가 많다. JDBC를 이용해서 의존관계를 많이 줄여야한다.
public void upgradeLevels() throws Exception {
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource); // JDBC 트랜잭션 추상 오브젝트
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;
}
}
-
스프링에서는 PlatfromTransactionManager라는 추상화 인터페이스를 제공한다. 여기서는 getConnection()만 요청하면 트랜잭션을 시작하면서 DB Connection을 가져온다.
-
가져온 트랜잭션은 TransactionStatus에 저장된다.
-
글로벌 트랜잭션으로 바꾸기위해선 PlatformTransactionManager의 구현클래스를 JTATransactionManager로 바꿔주면 되고 하이버네이트로 구현했으면 HibernateTransactionManager로 바꾸면 된다. 하지만 UserService코드가 어떤 트랜잭션 구현체를 사용할지 아는것은 DI원칙에 위배되기때문에 외부주입을 받자.
-
DataSourceTransactionManager를 빈으로 등록하고 UserService에서 주입받게 바꾸자.
-
빈으로 등록할때, 싱글톤으로 만들어져 멀티스레드 환경에서 안전한지 검토해야된다.
-
private PlatformTransactionManager transactionManager; public void setTransactionManager(PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; } public void upgradeLevels() throws Exception { 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 (Exception e) { this.transactionManager.rollback(status); throw e; } } // change ApplicationContext.xml <bean id="userService" class="service.UserService"> <property name="userDao" ref="userDao" /> <property name="transactionManager" ref="transactionManager" /> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean>
DI의 가치는 책임, 관심, 성격이 다른 코드를 분리하는데 있다. 스프링에서는 인터페이스와 DI를 통해 단일 책임 원칙이 잘 지켜지고 있다.
-