5.2 트랜잭션 서비스 추상화

트랜잭션 경계설정

  • 트랜잭션 : 더 이상 나눌 수 없는 단위 작업임. 작업을 쪼개서 작은 단위로 만들 수 없다는 것은 트랜잭션의 핵심 속성인 원자성임.
    • 중간에 예외가 발생해서 작업을 완료할 수 없다면 작업이 시작되지 않은 초기 상태로 되돌려놔야함.
    • 하나의 SQL을 처리하는 경우 DB가 트랜잭션 보장함.
    • 여러 개의 SQL이 사용되는 작업을 하나의 트랜잭션으로 취급해야하는 경우도 있음.
  • 트랜잭션은 시작하는 지점과 끝나는 지점이 있음. 끝나는 방법은 두 가지임.
    • 트랜잭션 롤백(transaction rollback): 두개의 SQL이 있을 때 두번째 SQL이 성공적으로 DB에서 수행되기 전에 문제가 발생할 경우 앞에서 처리한 작업도 취소시켜야 함.
    • 트랜잭션 커밋(tansaction commit): 반대로 여러개의 SQL을 하나의 트랜잭션으로 처리하는 경우에 모든 SQL 수행 작업이 다 성공적으로 마무리됐다고 DB에 알려줘서 작업 확정시켜야함.
  • 트랜잭션의 경계: 애플리케이션 내에서 트랜잭션이 시작되고 끝나는 위치임.
  • 트랜잭션의 경계설정(transaction demarcation):setAutocommit(false)로 트랜잭션의 시작을 선언하고 commit() 또는 rollback()으로 트랜잭션을 종료하는 작업
  • 로컬 트랜잭션(local transaction): 하나의 DB 커넥션 안에서 만들어지는 트랜잭션

비즈니스 로직 내의 트랜잭션 경계설정

  • 트랜잭션의 경계설정 작업을 UserService쪽으로 가져온다.
    • 문제점
      • (1) JdbcTemplate을 더 이상 활용할 수 없음. (JDBC API 직접 사용)
      • (2) DAO의 메소드와 비즈니스 로직을 담고 있는 UserService의 메소드에 Connection 파라미터가 추가돼야 함.
      • (3) Connection 파라미터가 UserDao 인터페이스 메소드에 추가되면 UserDao는 더 이상 데이터 엑세스 기술에 독립적일 수 없음.
      • (4)테스트 코드에도 영향을 미침.

트랜잭션 동기화

  • connection을 파라미터로 직접 전달하는 문제 → 스프링에서는 트랜잭션 동기화 방법으로 해결
  • 트랜잭션 동기화(transaction synchronization): UserService에서 트랜잭션을 시작하기 위해 만든 Connection 오브젝트를 특별한 저장소에 보관해두고, 이후에 호출되는 DAO의 메소드에서는 저장된 Connection을 가져다가 사용하게 하는 것임.
    • DAO가 사용하는 JdbcTemplate이 동기화 방식을 이용하도록 하는 것임.
  • 트랜잭션 동기화 저장소는 작업 스레드마다 독립적으로 Connection 오브젝트 저장 & 관리 → 멀티스레드 환경에서도 충돌X

트랜잭션 동기화 적용

  • 스프링의 트랜잭션 동기화 관리 클래스 = TransactionSynchronizationManager
    • 이 클래스를 이용해 먼저 트랜잭션 동기화 작업을 초기화하도록 요청
    • DataSourceUtils에서 제공하는 getConnection()메소드를 통해 DB 커넥션 생성
      • getConnection()메소드는 Connection 오브젝트 생성 & 트랜잭션 동기화에 사용하도록 저장소에 바인딩해줌.

JdbcTemplate과 트랜잭션 동기화

  • 미리 생성돼서 트랜잭션 동기화 저장소에 등록된 DB 커넥션이나 트랜잭션이 없는 경우 → JdbcTemplate이 직접 DB 커넥션을 만들고 트랜잭션을 시작해서 JDBC 작업을 진행한다.
  • upgradeLevels() 메소드에서처럼 트랜잭션 동기화를 시작해놨다면 → JdbcTemplate의 메소드에서는 직접 커넥션을 만드는 대신 트랜잭션 동기화 저장소에 들어있는 DB 커넥션 꺼내와 사용

트랜잭션 서비스 추상화

  • 하나의 트랜잭션 안에서 여러 개의 DB에 데이터를 넣는 작업을 해야한다면?
    • 로컬 트랜잭션은 하나의 DB Connection에 종속되기 때문에 불가능함.
  • 글로벌 트랜잭션(global transaction): 별도의 트랜잭션 관리자를 통해 트랜잭션을 관리함.
  • 자바는 JTA(Java Transaction API) 제공 →JDBC 외에 글로벌 트랜잭션을 지원하는 트랜잭션 매니저를 지원하기 위한 API → JTA를 이용해 트랜잭션 매니져를 활용하면 여러 개의 DB나 메시징 서버에 대한 작업을 하나의 트랜잭션으로 통합하는 분산 트랜잭션 또는 글로벌 트랜잭션 가능해짐.

트랜잭션 API의 의존관계 문제와 해결책

  • 트랜잭션의 경계설정을 담당하는 코드는 일정한 패턴을 갖는 유사한 구조임. → 추상화 도입
  • 추상화: 하위 시스템의 공통점을 뽑아내서 분리시키는 것임.
  • JDBC, JTA, 하이버네이트, JPA, JDO, JMS의 경우 트랜잭션 개념을 갖고 있으니 트랜잭션 경계설정 방법에서 공통점이 있을 것임. → 이 공통적인 특징을 모아서 추상화된 트랜잭션 관리 계층을 만들 수 있음. → 애플리케이션 코드에서는 트랜잭션 추상 계층이 제공하는 API를 이용해 트랜잭션을 이용하게 만들어준다면 특정 기술에 종속되지 않는 트랜잭션 경계설정 코드를 만들 수 있음.

스프링의 트랜잭션 서비스 추상화

  • code(스프링의 트랜잭션 추상화 API를 적용한 upgradeLevels())
public void upgradeLevels() {
	//JDBC 트랜잭션 추상 오브젝트 생성
	PlatformTransactionManager transactionManager = new DataSourceTransactionManager(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 (RuntimeException e) {
			transactionManager.rollback(status);
			throw e;
	}
}
  • 스프링의 트랜잭션 경계설정을 위한 추상 인터페이스 → PlatformTransactionManager
    • JDBC의 로컬 트랜잭션 이용할 경우 → PlatformTransactionManager를 구현한 DataSourceTransactionManager 사용.
  • [1] PlatformTransactionManager에서는 트랜잭션을 가져오는 요청인 getTransaction() 메소드 호출하면 됨.
  • [2] 트랜잭션은 TransactionStatus 타입의 변수에 저장됨.
    • 이는 트랜잭션에 대한 조작이 필요할 때 PlatformTransactionManager 메소드의 파라미터로 전달해주면 됨.
  • [3] PlatformTransactionManager에서는로 시작한 트랜잭션은 동기화 저장소에 저장됨.
  • [4] 트랜잭션 작업을 모두 수행한 후에는 트랜잭션을 만들 때 돌려받은 transactionStatus 오브젝트를 파라미터로 해서 PlatformTransactionManager의 commit()메소드 호출하면 됨. 예외가 발생하면 rollback() 메소드 호출.

트랜잭션 기술 설정의 분리

  • 트랜잭션 추상화 API를 적용한 UserService 코드를 JTA를 이용하는 글로벌 트랜잭션으로 변경하려면 어떻게 해야할까?
    • PlatformTransactionManager 구현 클래스를 DataSourceTransactionManager에서 JTATransactionManager로 바꿔 주면 됨. → 어떤 트랜잭션 매니저 구현 클래스를 사용할지 UserService 코드가 알고 있는 것은 DI 원칙에 위배된다. → DataSourceTransactionManager는 스프링 빈으로 등록하고 UserService가 DI 방식으로 사용하게 함.
  • 스프링의 빈으로 등록할 때 검토해야할 부분
    • 싱글톤으로 만들어져 여러 스레드에서 동시에 사용해도 괜찮은지
  • UserService에 DI될 transactionManager 빈을 설정파일에 등록
    • DataSourceTransactionManager 클래스 사용
  • code (트랜잭션 매니저를 빈으로 분리시킨 UserService)
public class UserService {
	...
	private PlatformTransactionManager transactionManger;

	public void setTransactionManager(PlatformTransactionManager transactionManager) {
		this.transactionManager = transactionManger;
	}
	public void upgradeLevels() {
		//JDBC 트랜잭션 추상 오브젝트 생성
		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;
		}
	}
}

5.3 서비스 추상화와 단일 책임 원칙

  • 기술과 서비스에 대한 추상화 기법을 이용하면 특정 기술환경에 종속되지 않는 포터블한 코드를 만들 수 있음.
  • UserDao와 UserService → 수평적인 분리(같은 애플리케이션 로직을 담은 코드지만 내용에 따라 분리됨.)
    • 인터페이스와 DI를 통해 연결됨으로써 결합도가 낮아짐.
  • UserDao와 DB 연결 기술도 결합도가 낮음.
  • 트랜잭션의 추상화
    • 애플리케이션의 비즈니스 로직과 그 하위에서 동작하는 로우레벨의 트랜잭션 기술이라는 아예 다른 계층의 특성을 갖는 코드를 분리함.
    • UserService와 트랜잭션 기술과도 스프링이 제공하는 PlatformTransactionManager 인터페이스를 통한 추상화 계층을 사이에 두고 사용하게해, 구체적인 트랜잭션 기술에 독립적인 코드가 됨.
  • 애플리케이션 로직의 종류에 따른 수평적인 구분이든, 로직과 기술이라는 수직적인 구분이든 모두 결합도가 낮으며, 서로 영향을 주지 않고 자유롭게 확장할 수 있는 구조를 만들 수 있는데는 스프링의 DI가 중요한 역할을 함.

단일 책임 원칙

  • 단일 책임 원칙: 하나의 모듈은 한 가지 책임을 가져야 함.
    • 장점 ) 어떤 변경이 필요할 때 수정 대상이 명확해짐.
  • 적절하게 책임과 관심이 다른 코드를 분리하고, 서로 영향을 주지 않도록 다양한 추상화 기법을 도입하고, 애프리케이션 로직과 기술/환경을 분리하는 작업은 복잡한 엔터프라이즈 애플리케이션에서는 반드시 필요함. → 이를 위한 핵심 도구는 DI
  • 객체지향설계 원칙을 잘 지키면 테스트하기도 편함.
  • 기능이 동작한다고 코드에 만족하지 말고 계속 개선하려는 자세 필요함.
  • 스프링에서 DI가 어떻게 적용되고 있는지 살펴보자.
  • 변경 사유가 생겼을 때 코드의 어디를 어떻게 수정해야하는지 살펴보자.

5.4 메일 서비스 추상화

  • 레벨이 업그레이드되는 사용자에게 안내 메일 발송해야하는 상황.
  • JavaMail 사용하면 됨.

JavaMail이 포함된 코드의 테스트

  • 테스트를 하면서 매번 매일이 발송되는 것이 바람직한가? 대개는 아니다.
    • 방법1 ) 테스트용 서버 이용하는 법도 있음.
    • 방법2) 테스트용 JavaMail 이용 → 문제점 ) JavaMail의 핵심 API에는 DataSource처럼 인터페이스로 만들어져서 구현을 바꿀 수 있는게 없음. → 해결 ) 서비스 추상화 적용

메일 발송 기능 추상화

  • JavaMail의 경우처럼 테스트를 어렵게 만드는 건전하지 않은 방식으로 설계된 API를 사용할 때도 유용하게 쓰일 수 있음.
  • 스프링의 MailSender 구현한 추상화 클래스 → JavaMailServiceImpl
  • 메일 발송 작업에도 트랜잭션 개념 도입해야함.
    • 방법1) 메일을 업그레이드할 사용자를 발견했을 때마다 발송하지 않고 발송 대상을 별도의 목록에 저장
    • 방법2) MailSender를 확장해 메일 전송에 트랜잭션 개념 적용 (MailSender를 구현한 트랜잭션 기능이 있는 메일 전송용 클래스 만들기)

테스트 대역

  • 테스트할 대상이 의존하고 있는 오브젝트를 DI 통해 바꿔치기 할 수 있음.
  • 테스트 대역(test double): 테스트 대상이 되는 오브젝트의 기능에만 충실하게 수행하면서 빠르게, 자주 테스트를 실행할 수 있도록 사용하는 오브젝트를 통틀어서 테스트 대역이라함.
  • 테스트 스텁(test stub): 테스트 대상 오브젝트의 의존객체로서 존재하면서 테스트 동안에 코드가 정상적으로 수행할 수 있도록 돕는 것을 말함.
  • 테스트 대상 오브젝트의 메소드가 돌려주는 결과뿐 아니라 테스트 오브젝트가 간접적으로 의존 오브젝트에 넘기는 값과 그 행위자체도 검증하고 싶다면? → 목 오브젝트(mock object) 사용해야함.
  • 목 오브젝트: 스텁처럼 테스트 오브젝트가 정상적으로 실행되도록 도와주면서, 테스트 오브젝트와 자신의 사이에서 일어나는 커뮤니케이션 내용을 저장했다가 테스트 결과 검증하는데 활용할 수 있게 해줌.
  • 테스트 대역의 대표적인 방법
    • 스텁 오브젝트 - 테스트가 수행될 수 있도록 의존 오브젝트에 간접적으로 입력 값 제공
    • 목 오브젝트 - 간접적인 출력값까지 확인 가능

참고 자료