토비의 스프링 - 서비스 추상화
5.1 사용자 레벨 관리 기능 추가
-
정보를 넣고 검색할 수 있다.
-
정기적으로 사용자의 활동내역을 참고해서 레벨을 조정한다.
-
사용자의 레벨은 BASIC, SILVER, GOLD 3단계로 나뉜다.
-
사용자가 처음 가입하면 BASIC 레벨로 시작한다. 이후 활동에 따라 레벨이 올라간다.
-
사용자가 가입 후 50회 이상 로그인하면 SILVER 레벨로 올라간다.
-
사용자가 가입 후 100회 이상 로그인하면 GOLD 레벨로 올라간다.
-
사용자 레벨의 변경 작업은 일정한 주기를 가지고 일괄적으로 진행된다. 변경 작업 전에는 조건을 충족하더라도 레벨의 변경이 일어나지 않는다.
문제
- 필드 이름이나 SQL 키워드를 잘못 넣은 거라면 테스트를 돌려보면 에러가 나니 쉽게 확인할 수 있다. 하지만
UPDATE문에서WHERE절을 빼먹는 실수는 컴파일러나 테스트로 잡아낼 수 없다.
방안
update가 돌려주는 결과값을 보고 영향받은 행의 수가 1인지 확인하는 방법- 테스트를 보강해서
update후에 다시 조회해서 값이 제대로 바뀌었는지 확인하는 방법
5.1.3 UserService.updateLevels()
5.1.4 UserService.add()
처음 가입하는 사용자는 기본적으로 BASIC 레벨을 부여받는다. 이 로직은 어디에 담는게 좋을까?
- User 클래스에서 level 필드를 BASIC 으로 초기화
- UserService.add() 메서드에서 level 필드를 BASIC 으로 초기화
5.1.5 코드 개선
작성된 코드를 살펴보고 다음과 같은 질문을 스스로에게 던져보자.
- 코드의 중복된 부분은 없는가?
- 코드가 무엇을 하는 것인지 이해하기 불편하지 않은가?
- 코드가 자신이 있어야 할 자리에 있는가?
- 앞으로 변경이 일어난다면 어떤 것들이 있을 수 있고, 그 변경에 잘 대응할 수 있는가?
upgradeLevels()
- if-else 문에서 switch 문으로 변경
upgradeLevel()메서드로 분리- GOLD 레벨에서 업그레이드 시도 시 예외 발생
- 예외를 내지 않아 부수효과가 없는 메서드로 변경
- “최고 등급을 더 올릴 수 없음”은 정상 상황
nextLevel()메서드로 분리
5.2 트랜잭션 서비스 추상화
정기 사용자 레벨 업그레이드 작업중에 장애가 발생하면 어떻게 될까?
- 일부 사용자의 레벨만 올라가고, 나머지는 올라가지 않는 상황이 발생할 수 있다.
- 정책은 모두 롤백하기로 결정
강제 예외 발생을 통한 테스트
비즈니스 로직 내의 트랜잭션 경계 설정
여러번 DB에 업데이트를 해야하는 작업을 하나의 트랜잭션으로 만들려면 어떻게 해야 할까?
-
DAO 안에 옮기면 비즈니스 로직과 데이터 액세스 로직이 섞이게 된다.
-
서비스 계층에서 트랜잭션 경계를 설정해야 한다.
-
Connection을 DAO 계층으로 전달해야한다.
interface UserDao {
fun update(conn: Connection, user: User)
fun getAll(conn: Connection): List<User>
fun add(conn: Connection, user: User)
...
}
class UserService {
fun upgradeLevels() {
val conn = dataSource.getConnection()
try {
conn.autoCommit = false
val users = userDao.getAll(conn)
...
conn.commit()
} catch (e: Exception) {
conn.rollback()
throw e
} finally {
conn.close()
}
}
}
UserService 트랜잭션 경계 설정의 문제점
- 깔끔한 리소스 처리를 가능하게 했던 JdbcTemplate 을 사용할 수 없다.
- Dao와 서비스 계층이
Connection객체에 의존하게 된다. - UserDao는 더 이상 데이터 엑세스 기술에 독립적이지 않다. (Connection, EntityManager, Session 등등 특정 기술에 종속)
트랜잭션 동기화
- 트랜잭션 동기화 매니저(TransactionSynchronizationManager) 라는 별도의 유틸리티 클래스를 만들어서 트랜잭션과 관련된 정보를 보관
- 미리 생성돼서 트랜잭션 동기화 저장소에 등록된 DB 커넥션이나 트랜잭션이 없는 경우에는 JdbcTemplate이 직접 새로운 커넥션을 생성
- 트랜잭션이 이미 시작된 상태라면 트랜잭션 동기화 매니저에서 커넥션을 꺼내서 사용
더 생각해보기
- 사용자가 많아지면 한꺼번에 모두 처리할 수 있을까?
- 레벨 업그레이드 시간이 정책적으로 자정으로 정해져 있다면, 자정에 맞춰서 모든 사용자를 처리할 수 있을까?
5.2.4 트랜잭션 서비스 추상화
여러개의 DB를 사용하는 경우에는 트랜잭션을 어떻게 관리해야 할까?
- 로컬 트랜잭션은 하나의 DB Connection에만 적용된다.
- 각 DB와 독립적으로 만들어지는 Connection 이 아닌 별도의 트랜잭션 관리자를 사용해야 한다. (글로벌 트랜잭션)
- JTA (Java Transaction API) 가 대표적인 글로벌 트랜잭션 관리자
멀티 DB 트랜잭션의 문제점
- 2PC(XA) 기피 - 가용성,복잡도,락 경합 비용이 높음
- eventual consistency(최종 일관성) 모델이 보편화
- Saga - 서비스별로 로컬 트랜잭션 처리 -> 서비스 간 이벤트/메시징으로 연계 -> 실패 시 보상(Compensation) 실행
트랜잭션 API의 의존 관계 문제와 해결책
- 하이버네이트의 트랜잭션 관리 코드와 JDBC 트랜잭션 관리 코드는 다르다.
PlatformTransactionManager인터페이스 도입
트랜잭션 기술 설정의 분리
- 어떤 트랜잭션 매니저 구현 클래스를 사용할지
UserService가 아는 것은 바람직하지 않다. - DI 컨테이너가
UserService에 적절한PlatformTransactionManager구현체를 주입하도록 한다.
5.3 서비스 추상화와 단일 책임 원칙
- 트랜잭션의 추상화는 레이어의 분리와 다르다
- 애플리케이션에서의 비즈니스 로직과 하위에서 제공하는 기술적 기능을 분리하는 것
애플리케이션 계층 - UserService, UserDao 서비스 추상화 계층 - TransactionManager, DataSource 기술 서비스 계층 - JDBC, JTA, Connection Pooling
5.4 메일 서비스 추상화
레벨이 업그레이드되는 사용자에게는 안내 메일을 발송해달라.
- 이메일 정보 관리
- 메일 발송 기능
5.4.3 메일 서비스 추상화
레벨 업그레이드 작업 중간에 예외가 발생해서 롤백이 일어나더라도 이미 발송된 메일은 취소할 수 없다.
- 업그레이드할 사용자를 미리 파악한 후, 트랜잭션이 커밋된 후에 메일을 발송하도록 변경
List<User>를 만들어서 업그레이드 대상 사용자를 담아야하는 문제
- MailSender를 확장해서 트랜잭션 커밋 후에 메일을 발송하도록 구현
- 트랜잭션 동기화 매니저를 사용해서 트랜잭션 커밋 후에 실행될 작업을 등록