Back to writing

토비의 스프링 - 서비스 추상화

토비의 스프링 - 서비스 추상화

commit

5.1 사용자 레벨 관리 기능 추가

  • 정보를 넣고 검색할 수 있다.

  • 정기적으로 사용자의 활동내역을 참고해서 레벨을 조정한다.

  • 사용자의 레벨은 BASIC, SILVER, GOLD 3단계로 나뉜다.

  • 사용자가 처음 가입하면 BASIC 레벨로 시작한다. 이후 활동에 따라 레벨이 올라간다.

  • 사용자가 가입 후 50회 이상 로그인하면 SILVER 레벨로 올라간다.

  • 사용자가 가입 후 100회 이상 로그인하면 GOLD 레벨로 올라간다.

  • 사용자 레벨의 변경 작업은 일정한 주기를 가지고 일괄적으로 진행된다. 변경 작업 전에는 조건을 충족하더라도 레벨의 변경이 일어나지 않는다.

문제

  • 필드 이름이나 SQL 키워드를 잘못 넣은 거라면 테스트를 돌려보면 에러가 나니 쉽게 확인할 수 있다. 하지만 UPDATE 문에서 WHERE 절을 빼먹는 실수는 컴파일러나 테스트로 잡아낼 수 없다.

방안

  1. update 가 돌려주는 결과값을 보고 영향받은 행의 수가 1인지 확인하는 방법
  2. 테스트를 보강해서 update 후에 다시 조회해서 값이 제대로 바뀌었는지 확인하는 방법

5.1.3 UserService.updateLevels()

5.1.4 UserService.add()

처음 가입하는 사용자는 기본적으로 BASIC 레벨을 부여받는다. 이 로직은 어디에 담는게 좋을까?

  1. User 클래스에서 level 필드를 BASIC 으로 초기화
  2. 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 트랜잭션 경계 설정의 문제점

  1. 깔끔한 리소스 처리를 가능하게 했던 JdbcTemplate 을 사용할 수 없다.
  2. Dao와 서비스 계층이 Connection 객체에 의존하게 된다.
  3. 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를 확장해서 트랜잭션 커밋 후에 메일을 발송하도록 구현
    • 트랜잭션 동기화 매니저를 사용해서 트랜잭션 커밋 후에 실행될 작업을 등록