트랜잭션 경계설정 코드 분리

DI 사용

  • 비즈니스 로직과 트랜잭션 같은 기술적인 코드의 분리
  • 비즈니스 로직에 대한 테스트를 쉽게 만들어낼 수 있음

고립된 단위 테스트

  • 테스트는 작은 단위로 하면 좋다
    • 그럴수없는 경우가 많다.
    • 테스트 대상이 다른 오브젝트와 환경에 의존하고 있다면 작은 단위의 테스트가 주는 장접을 얻기 힘들다.

테스트 대상 오브젝트 고립시키기

테스트의 대상이 환경이나, 외부 서버, 다른 클래스의 코드에 종속되고 영향을 받지 않도록 고립시킬 필요

트랜잭션

  • TransactionDefinition 타입 오브텍트를 사용하면 네 가지 속성을 이용해 트랜잭션의 독작방싱을 제어할 수 있다.

정의

  • 트랜잭션이라고 모두 같은 방식으로 동작하는 것이 아니다
  • 트랜잭션의 기본 개념인 더 이상 쪼갤 수 없는 최소 단위의 작업이라는 개념은 항상 유효
  • 트랜잭션 경계 안에서 진행된 작업은 모두 성공하든지 아니면 모두 취소돼야 함

트랜잭션 전파(transaction propagation)

트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가를 결정하는 방식을 말한다.

위 그림의 B와 같이 독자적인 트랜잭션 경계를 가진 코드에 대해 이미 진행 중인 트랜잭션이 어떻게 영향을 미칠 수 있는가를 정의하는 것이 트랜잭션 전파 속성이다.

PROPAGATION_REQUIRED

  • 진행 중인 트랜잭션이 없으면 새로 시작, 이미 시작된 트랜잭션이 있으면 이에 참여
  • DefaultTransactionDefinition의 트랜잭션 전파 속성임

PROPAGATION_REQUIRES_NEW

  • 항상 새로운 트랜잭션을 시작. 즉 앞에서 시작된 트랜잭션이 있든 없든 상관없이 새로운 트랜잭션을 만들어서 독자적으로 동작하게 함
  • 독립적인 트랜잭션이 보장돼야 하는 코드에 적용

PROPAGATION_NOT_SUPPORTED

  • 트랜잭션 없이 동작하도록 만들 수도 있음
  • 진행 중인 트랜잭션이 있어도 무시
  • 모든 메소드에 트랜잭션 AOP 적용, 특정 메소드의 트랜잭션 전파 속성만 NOT_SUPPORTED로 설정해서 트랜잭션 없이 동작하게 만들수 있음

트랜잭션 매니저의 getTransaction()

항상 트랜잭션을 새로 시작하는 것이 아니라 트랜잭션의 전파 속성과 현재 진행중인 트랜잭션이 존재하는지 여부에 따라서 동작

  • 진행 중인 트랜잭션에 참여하는 경우
    • 최초로 트랜잭션을 시작한 경계까지 정상적으로 진행돼야 커밋

격리수준(isolation level)

모든 DB 트랜잭션은 격리수준을 갖고 있어야 한다.

  • 가능하다면 모든 트랜잭션이 순차적으로 진행돼서 다른 트랜잭션의 작업에 독립적인 것이 좋음
  • → 성능이 크게 떨어짐
  • → 적절하게 격리수준을 조정해서 가능한 한 많은 트랜잭션을 동시에 진행시키면서도 문제가 발생하지 않게 하는 제어가 필요
  • 기본적으로 DB에 설정되어 있음
  • JDBC드라이버 또는 DataSource 등에서 재설정 가능
  • 필요하다면 트랜잭션 단위로 격리수준을 조정할 수 있음
  • DefaultTransactionDefinition에 설정된 격리수준은 ISOLATION_DEFAULT임 → DataSource에 설정되어 있는 디폴트 격리수준을 그대로 따른다는 뜻
  • 특별한 작업을 수행하는 메소드의 경우는 독자적인 격리수준을 지정할 필요가 있음

제한시간

트랜잭션을 수행하는 제한시간(timeout)을 설정할 수 있다.

  • DefaultTransactionDefinition의 기본 설정은 제한시간이 없는것임
  • 제한시간은 트랜잭션을 직접 시작할 수 있는 PROPAGATION_REQUIRED, PROPAGATION_REQUIRES_NEW와 함께 사용해야만 의미가 있다

읽기전용

읽기전용(read only)으로 설정해두면 트랜잭션 내에서 데이터를 조작하는 시도를 막아줄 수 있다. 또한 데이터 액세스 기술에 따라서 성능이 향상될 수도 있다.

트랜잭션 인터셉터와 트랜잭션 속성

  • 메소드별로 다른 트랜잭션 정의를 적용하려면 어드바이스의 기능을 확장해야함
  • 메소드 이름 패턴에 따라 다른 트랜잭션 정의가 적용되도록 만드는 것

TransactionInterceptor

편리하게 트랜잭션 경계설정 어드바이스로 사용할 수 있도록 해주는 스프링의 기능?

  • 트랜잭션 정의를 메소드 이름 패턴을 이용해서 다르게 지정할 수 있는 방법을 추가로 제공
  • PlatformTransactionManager와 Properties 타입의 두 가지 프로퍼티를 가짐
  • Properties 타입인 두 번째 프로퍼티 이름은 transactionAttributes로, 트랜잭션 속성을 정의한 프로퍼티

예외처리 방식

런타임 예외 → 롤백 체크 예외 → 커밋 스프링의 기본적인 예외처리 원칙에 따라 비즈니스적인 의미가 있는 예외상황에만 체크 예외를 사용하고, 그 외의 모든 복구 불가능한 순수한 예외의 경우는 런타임 예외로 포장돼서 전달하는 방식을 따른다고 가정하기 때문

TransactionAttributes는 rollbackOn()이라는 속성을 둬서 기본 원칙과 다른 예외처리가 가능하게 해준다. → 특정 체크 예외의 경우는 트랜잭션을 롤백시키고, 특정 런타임 예외에 대해서는 트랜잭션을 커밋시킬 수 있음

메소드 이름 패턴을 이용한 트랜잭션 속성 지정

  • Properties 타입의 transactionAttributes 프로퍼티는 메소드 패턴과 트랜잭션 속성을 키와 값으로 갖는 컬렉션이다. 트랜잭션 속성은 다음과 같은 문자열로 정의할 수 있다.
  • 트랜잭션 전파 항목만 필수, 나머지는 다 생략 가능. 생략시 DefaultTransactionDefinition에 설정된 디폴트 속성이 부여
    • 또는 -로 시작하는 건 기본 원칙을 따르지 않는 예외를 정의해주는 것
      • +XXXRuntimeException → 커밋 가능
  • 트랜잭션 속성 중 readOnly나 timeout 등은 트랜잭션이 처음 시작될 때가 아니면 적용 X
  • 메소드 이름이 하나 이상의 패턴과 일치하는 경우, 메소드 이름 패턴중에서 가장 정확히 일치하는 것이 적용?

포인트컷과 트랜잭션 속성의 적용 전략

트랜잭션 부가기능을 적용할 후보 메소드를 선정하는 작업은 포인트컷에 의해 진행

어드바이스의 트랜잭션 전파 속성에 따라서 메소드별로 트랜잭션의 적용 방식이 결정

  • 트랜잭션용 포인트컷 표현식에는 메소드나 파라미터, 예외데 대한 패턴을 정의하지 않는 게 바람직함
  • 가능하면 클래스보다는 인터페이스 타입을 기준으로 타입 패턴을 적용하는 것이 좋음
    • 인터페이스는 클래스에 비해 변경 빈도가 적고 일정한 패턴을 유지하기 쉭기 때문

공통된 메소드 이름 규칙을 통해 최소한의 트랜잭션 어드바이스와 속성을 정의한다.

  • 가장 간단한 트랜잭션 속성 부여 방법은 다음과 같이 모든 메소드에 대해 디폴트 속성을 지정하는 것
  • 메소드의 접두어를 정해두는 것이 좋음
  • 트랜잭션 적용 대상 클래스의 메소드는 일정한 명명 규칙을 따르게 해야함
  • 일반화하기에는 적당하지 않은 특별한 트랜잭션 속성이 필요한 타깃 오브젝트에 대해서는 별도의 어드바이스와 포인트컥 표현식을 사용하는 편이 좋음

프록시 방식 AOP는 같은 타깃 오브젝트 내의 메소드를 호출할 때는 적용되지 않는다.

  • 같은 타깃 오브젝트 안에서 메소드 호출이 일어나는 경우에는 프록시 AOP를 통해 부여해준 부가기능이 적용되지 않는다는 점을 주의해야 함

해결방법

  • 스프링 API를 이용해 프록시 오브젝트에 대한 레퍼런스를 가져온 뒤에 같은 오브젝트의 메소드 호출도 프록시를 이용하도록 강제하는 방법
    • 순수한 비즈니스 로직만을 남겨두려고 노력했는데, 거기에 이런 짓을 하는건 바람직하지 않다.
  • AspectJ와 같은 타깃의 바이트코드를 직접 조작하는 방식의 AOP 기술을 적용

트랜잭션 속성 적용

트랜잭션 경계설정의 일원화

  • 여러 계층에서 중구난방으로 적용하는 건 좋지 않다.
  • 일반적으로 특정 계층의 경계를 트랜잭션 경계와 일치시키는 것이 바람직하다.
  • 서비스 계층 오브젝트의 메소드가 트랜잭션 경계를 부여하기에 가장 적절한 대상
  • 서비스 계층을 경계로 정했다면,
    • 테스트와 같은 특별한 이유가 아니고는 다른 계층이나 모듈에서 DAO에 직접 접근하는 것은 차단해야 함

애노테이션 트랜잭션 속성과 포인트컷

트랜잭션 애노테이션

@Transactional

  • 메소드, 클래스, 인터페이스에 사용할 수 있음
  • 트랜잭션 속성정보로 사용하도록 지정하면 스프링은 @Transaction이 부여된 모든 오브젝트를 자동으로 타깃 오브젝트로 인식
    • 이때 사용되는 포인트컷은 TransactionAttributeSourcePointcut
      • 스스로 표현식과 같은 선정기준을 갖고 있진 않음
      • @Transactional이 타입 레벨이든 메소드 레벨이든 상관없이 부여된 빈 오브젝트를 모두 찾아서 포인트컷의 선정 결과로 돌려준다
  • 기본적으로 트랜잭션 속성을 정의하는 것이지만, 동시에 포인트컷의 자동등록에도 사용

트랜잭션 속성을 이용하는 포인트컷

  • @Transactional은 메소드마다 다르게 설정할 수도 있으므로 매우 유연한 트랜잭션 속성 설정이 가능
  • 동시에 포인트컷도 @Transactional을 통한 트랜잭션 속성정보를 참조하도록 만든다
    • @Transactional로 트랜잭션 속성이 부여된 오브젝트라면 포인트컷의 선정 대상이기도 하기 때문
  • 이 방식을 이용하면 포인트컷과 트랜잭션 속성을 애노테이션 하나로 지정할 수 있다
  • 트랜잭션 부가기능 적용 단윈는 메소드
    • 메소드 마다 @Transactional을 부여하고 속성 지정 할수 있음 → 유연한 속성 제어 가능
    • But 코드 지저분해짐, 동일한 속성 정보를 가진 애노테이션을 반복적으로 메소드마다 부여해주는 바람직하지 못한 결과를 가져올 수 있음

대체 정책

스프링은 @Transactional을 적용할 때 4단계의 대체(fallback) 정책을 이용하게 해준다.

메소드의 속성을 확인할 때 타깃 메소드 → 타깃 클래스 → 선언 메소드 → 선언 타입(클래스, 인터페이스)의 순서에 따라서 @Transactional이 적용됐는지 차례로 확인하고, 가장 먼저 발견되는 속성정보를 사용하게 하는 방법

  • @Transactional을 사용하면 대체 정책을 잘 활용해서 애노테이션 자체는 최소한으로 사용하면서도 세밀한 제어가 가능
  • 타깃 클래스보다는 인터페이스에 두는 게 바람직
  • But! 인터페이스를 사용하는 프록시 방식의 AOP가 아닌 방식으로 트랜잭션을 적용하면 인터페이스에 정의한 @Transactional은 무시되기 때문에 안전하게 타깃 클래스에 @Transactional을 두는 방법을 권장

트랜잭션 지원 테스트

선언적 트랜잭션과 트랜잭션 전파 속성

  • REQUIRED 전파 속성을 가진 메소드를 결합해서 다양한 크기의 트랜잭션 작업을 만들 수 있고
  • 트랜잭션 적용 때문에 불필요하게 코드를 중복하는 것도 피할 수 있으며,
  • 애플리케이션을 작은 기능 단위로 쪼개서 개발할 수가 있다.
  • AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있게 하는 방법을 선언적 트랜잭션(declarative transaction)
  • TransactionTemplate, 개별 데이터 기술의 트랜잭션 API를 사용해 직접 코드 안에서 사용하는 방법은 프로그램에 의한 트랜잭션(programmatic transaction)
  • 스프링은 이 두가지 모두 지원
  • 특별한 경우가 아니라면 선언적 방식의 트랜잭션을 사용하는 것이 바람직하다

트랜잭션 동기화와 테스트

트랜잭션 추상화 기술의 핵심은 트랜잭션 매니저와 트랜잭션 동기화

롤백 테스트

테스트 내의 모든 DB 작업을 하나의 트랜잭션 안에서 동작하게 하고 테스트가 끝나면 무조건 롤백 해버리는 테스트를 말한다.

  • 어떤 변경을 가하든 상관없음(데이터 추가, 제거, 등등)
  • 특별한 검색조건을 만족시키기 위해 기본적인 테스트 데이터에 부가정보를 넣을 수도 있음
  • 여러 개발자가 하나의 공용 테스트용 DB를 사용할 수 있게도 해줌
    • 적절한 격리수준만 보장해주면 동시에 여러 개의 테스트가 진행돼도 상관업음

테스트를 위한 트랜잭션 애노테이션

@Transactional

테스트 클래스(메소드)에 부여하면 마치 타깃 클래스나 인터페이스에 적용된 것처럼 테스트 메소드에 트랜잭션 경계가 자동으로 설정

테스트 내에서 진행하는 모든 트랜잭션 관련 작업을 하나로 묶어줄 수 있다

  • 테스트에 사용하는 애노테이션은 AOP를 위한 것은 아님
  • 컨텍스트 테스트 프레임워크에 의해 트랜잭션을 부여해주는 용도로 쓰일 뿐

@Rollback

@Transactional은 애플리케이션의 클래스에 적용 할 때와 디폴트 속성은 동일, 테스트용 트랜잭션은 테스트가 끝나면 자동으로 롤백됨

이때 롤백 기능을 제어하기 위해 사용

  • 기본값은 true
  • 트랜잭션은 적용되지만 롤백을 원치 않는다면 @Rollback(false)라고 해줘야 함

@TransactionConfiguration

클래스 레벨에 부여할 수 있는 롤백 여부에 대한 기본 설정과 트랜잭션 매니저 빈을 지정하는데 사용

  • @TransactionConfiguration(defaultRollback=false)로 사용

NotTransactional과 Propagation.NEVER

  • @NotTransactional을 테스트 메소드에 부여하면 클래스 레벨의 @Transactional설정을 무시하고 트랜잭션을 시작하지 않은 채로 테스트를 진행
    • 스프링 3.0에서 제거 대상
    • 스프링의 개발자들은 트랜잭션 테스트와 비 트랜잭션 테스트를 아예 클래스를 구분해서 만들도록 권장
  • @Transactional의 전파 속성을 NEVER로 지정하여 진행

효과적인 DB 테스트

의존, 협력 오브젝트를 사용하지 않고 고립된 상태에서 테스트를 진행하는 단위 테스트와 , DB 같은 외부의 리소스나 여러 계층의 클래스가 참여하는 통합 테스트는 아예 클래스를 구분해서 따로 만드는 게 좋다.

테스트는 어떤 경우에도 서로 의존하면 안 된다.

Ref.

이일민, “토비의 스프링 3.1”, 에이콘 출판사(2012)