6.6 트랜잭션 속성
6.6.1 트랜잭션 정의
- 트랜잭션이라고 모두 같은 방식으로 동작하는 것은 아님
commit
, rollback
외에도 트랜잭션의 동작 방식을 제어할 수 있는 몇 가지 조건이 있음
트랜잭션 전파
트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가를 결정
A의 트랜잭션이 시작되고 아직 끝나지 않은 시점에서 B를 호출했다면, B의 코드는 어떤 트랜잭션 안에서 동작해야 할까?
-
B의 코드는 A에서 이미 시작한 트랜잭션에 참여한다.
→ (2)의 코드를 진행하는 중에 예외가 발생하면 A, B코드에서 진행됐던 DB작업이 모두 취소됨
-
A와 무관하게 B를 독립적인 트랜잭션으로 만든다.
→ (2)의 코드를 진행하는 중에 예외가 발생하더라도 B의 코드는 영향을 받지 않음
PROPAGATION_REQUIRED
- 가장 많이 사용되는 트랜잭션 전파 속성
- 진행중인 트랜잭션이 없으면 새로 시작하고, 이미 시작된 트랜잭션이 있다면 이에 참여한다.
- 해당 속성을 갖는 코드는 다양하게 결합해서 하나의 트랜잭션으로 구성하기 쉽다. ex) A, B, A→B, B→A
PROPAGATION_REQUIRES_NEW
PROPAGATION_NOT_SUPORTED
- 트랜잭션 없이 동작하도록 만들고, 진행중인 트랜잭션이 있어도 무시
- 이럴거면 트랜잭션 경계설정을 아예 안하면 되는거 아닌가?
- 트랜잭션 경계설정은 보통 AOP를 이용해 한 번에 많은 메소드에 동시에 적용하는 방법을 사용하기 때문에 이런 경우에 특별한 메소드만 트랜잭션에서 제외하기 위해 설정
- 물론 포인트컷을 잘 만들어서 사용하면 되지만 상당히 복잡해질 수 있기 때문에 차라리 모든 메소드에 트랜잭션 AOP를 설정하고 특정 메소드에만 해당 속성을 사용하는게 낫다고 함
격리수준
- 모든 DB 트랜잭션은 격리수준을 갖고 있어야 함
- 서버환경에서는 여러 개의 트랜잭션이 동시에 진행될 수 있음
- 모든 트랜잭션이 순차적으로 진행되면 성능이 크게 떨어지므로, 격리수준을 조정해 가능한 많은 트랜잭션을 동시에 진행시키면서도 문제가 발생하지 않게끔 제어한다.
- 기본적으로는 DB, DataSource에 설정된 디폴트 격리수준을 따르지만 필요에 따라 메소드별로 독자적인 격리수준을 지정하기도 한다.
제한시간
- 트랜잭션을 수행하는 제한시간을 설정할 수 있음
- 트랜잭션을 직접 시작할 수 있는
PROPAGATION_REQUIRED
, PROPAGATION_REQUIRES_NEW
속성과 같이 사용해야 의미가 있음
읽기전용
- 트랜잭션 내에서 데이터를 조작하는 시도를 막아줌
6.6.2 트랜잭션 인터셉터와 트랜잭션 속성
💡 원하는 메소드만 선택해서 독자적인 트랜잭션 정의를 적용할 수 있는 방법이 없을까?
-
TransactionDefinition
네 가지 속성을 이용해 트랜잭션의 동작방식을 제어
-
TransactionAdvice
TransactionDefinition 오브젝트를 생성하고 사용, 트랜잭션 경계설정 기능을 가짐
TransactionInterceptor
- 트랜잭션 경계설정 어드바이스로 사용 가능
- 트랜잭션 정의를 메소드 이름 패턴을 사용해 지정할 수 있는 방법을 제공해줌
- TransactionInterceptor의 두 가지 프로퍼티
- PlatformTransactionManager
- Properties - transactionAttributes
- TransactionDefinition의 네 가지 기본 트랜잭션 속성 + rollbackOn() 메소드를 갖는 TransactionAttribute 인터페이스
- 트랜잭션 부가기능의 동작방식을 모두 제어할 수 있음
- TransactionInterceptor의 예외 처리 방식
-
런타임 예외
가 발생하면 트랜잭션을 롤백시킨다.
-
타깃 메소드가 런타임 예외가 아닌 체크 예외
를 던지는 경우에는 트랜잭션을 커밋한다.
→ 체크 예외를 일종의 비즈니스 로직에 따른 의미 있는 리턴 방식의 한 가지로 인식
- TransactionAttribute는
rollbackOn()
속성을 둬 이런 기본적인 예외 처리 방식을 따르지 않아도 되게끔 해준다.
트랜잭션 속성 정의 방식
- TransactionInterceptor 타입의 빈 정의
- tx 스키마의 전용 태그를 이용해 정의
6.6.3 포인트컷과 트랜잭션 속성의 적용 전략
트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 이용한다
- 일반적으로 트랜잭션을 적용할 타깃 클래스의 메소드는 모두 트랜잭션 적용 후보가 되는 것이 바람직하다.
- 비즈니스 로직을 담고 있는 클래스라면 메소드 단위까지 세밀하게 포인트컷을 정의해줄 필요는 없음
- 쓰기 작업이 없는 단순한 조회 작업 메소드에도 모두 트랜잭션을 적용하는게 좋다.
- 트랜잭션 경계로 삼을 클래스들이 모여 있는 패키지 혹은 비즈니스 로직 서비스를 담당하는 클래스명의 패턴을 찾아서 표현식으로 만든다.
- 메소드 시그니처를 이용한 execution() 방식의 포인트컷 표현식
- Service, ServiceImpl로 끝나는 경우 execution(*..ServiceImpl.*(..))
- 스프링의 빈 이름을 이용하는 bean() 표현식
공통된 메소드 이름 규칙을 통해 최소한의 트랜잭션 어드바이스와 속성을 정의한다
- 기준이 되는 몇 가지 트랜잭션 속성을 정의하고, 그에 따라 적절한 메소드 명명 규칙을 만들어두면 하나의 어드바이스만으로 애플리케이션 모든 서비스 빈에 트랜잭션 속성을 지정할 수 있다.
프록시 방식 AOP는 같은 타킷 오브젝트 내의 메소드를 호출할 때는 적용되지 않는다
트랜잭션 속성 테스트
→ TransientDataAccessResourceException
예외가 발생한다. 스프링의 DataAccessException의 한 종류로 일시적인 예외상황을 만났을 때 발생하는 예외다.
6.7 애노테이션 트랜잭션 속성과 포인트컷
트랜잭션 속성을 비즈니스 로직을 담당하는 패키지, 클래스 등에 일괄적으로 적용하는 방식은 대부분의 상황에 잘 들어맞는다. 하지만 세밀한 제어가 필요한 경우도 종종 있는데, 이때는 직접 타깃에 트랜잭션 속성정보를 가진 애노테이션을 지정한다.
6.7.1 트랜잭션 애노테이션
@Transactional
@Inherited
@InterceptorBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Transactional {
TxType value() default Transactional.TxType.REQUIRED;
@Nonbinding
Class[] rollbackOn() default {};
@Nonbinding
Class[] dontRollbackOn() default {};
public static enum TxType {
REQUIRED,
REQUIRES_NEW,
MANDATORY,
SUPPORTS,
NOT_SUPPORTED,
NEVER;
private TxType() {
}
}
}
- @Target
- 메소드와 타입이 타깃이므로 메소드, 클래스, 인터페이스에 @Transactional 애노테이션을 사용할 수 있음
- 스프링은 @Transactional이 부여된 모든 오브젝트를 자동으로 타깃 오브젝트로 인식한다.
- 이때
TransactionAttributeSorcePointcut
포인트컷을 사용하는데 표현식과 같은 선정기준을 갖는 것이 아니라, 해당 애노테이션이 붙은 빈 오브젝트를 모두 찾아서 포인트컷의 선정 결과로 돌려준다.
트랜잭션 속성을 이용하는 포인트컷
@Transactional 애노테이션을 사용했을 때 어드바이저의 동작방식
- 포인트컷과 트랜잭션 속성을 애노테이션 하나로 지정 가능
- 메소드 단위로 지정할 수 있으므로 세밀한 제어가 가능
- 애노테이션이 메소드마다 반복적으로 부여되어 코드가 지저분해질 수 있음
대체 정책
스프링은 @Transactional을 적용할 때 4단계의 대체정책을 이용한다.
- 메소드의 속성을 확인할 때 다음의 순서로 트랜잭션이 적용됐는지 차례로 확인하고, 가장 먼저 발견되는 속성을 사용하게 하는 방법
- 타깃 메소드 → 타깃 클래스 → 선언 메소드 → 선언 타입
- 예시) @Transactional을 부여할 수 있는 위치는 총 6개
6.7.2 트랜잭션 애노테이션 적용
애노테이션을 이용할 때는 단순하게 트랜잭션이 필요한 타입 또는 메소드에 직접 애노테이션을 부여한다.
6.8 트랜잭션 지원 테스트
6.8.1 선언적 트랜잭션과 트랜잭션 전파 속성
-
사용자 등록 로직을 담당하는 add() 메소드가 트랜잭션 전파 방식을 사용할 수 없어서 매번 새로운 트랜잭션을 시작하도록 만들어졌다면, 다른 메소드에서 호출하기가 꺼려질 것이다.
- 결국 add() 메소드를 복사해서 하나의 메소드 안에 넣게 되는 중복코드가 계속 발생할 것이다.
-
스프링의 선언적 트랜잭션
을 사용하면 AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있다
- 또한 스프링은 개별 데이터 기술의 트랜잭션 API를 사용해 직접 코드 안에서 사용하는
프로그램에 의한 트랜잭션
도 지원한다.
6.8.2 트랜잭션 동기화와 테스트
트랜잭션 매니저와 트랜잭션 동기화
- 트랜잭션 추상화 기술의 핵심은 트랜잭션 매니저와 트랜잭션 동기화
트랜잭션 매니저
구체적인 트랜잭션 기술의 종류에 상관없이 일관적인 제어 가능
트랜잭션 동기화
시작된 트랜잭션 정보를 저장소에 보관해뒀다가 DAO에서 공유
- 이 기술 덕분에 트랜잭션 전파 속성에 따라 진행 중인 트랜잭션이 있는지 확인하고 이에 참여할 수 있는 것
트랜잭션 매니저를 이용한 테스트용 트랜잭션 제어
- 메소드를 추가하지 않고도, UserService의 메소드를 호출하기 전에 트랜잭션을 미리 시작시켜서 UserService의 세 개의 메소드가 동일한 트랜잭션 내에 참여하게끔 유도할 수 있음
트랜잭션 동기화 검증
- 테스트에서 미리 트랜잭션을 시작함으로써 테스트코드를 하나의 트랜잭션 단위로 묶는 것이 가능 (@Transactional 애노테이션 적용과 같은 용도로..)
- DB 작업이 포함되는 테스트를 원하는 대로 제어하는 것이 가능해졌다.
- 하이버네이트 등 ORM에서 세션에서 분리된 엔티티 동작을 확인할 때도 유용하다.
롤백 테스트
- DB에 엑세스하는 테스트를 할 때마다 테스트 데이터를 초기화하는 작업이 반복되는데, 이럴 때 롤백 테스트가 유용하다.
- 테스트에서 트랜잭션을 제어할 수 있기 때문에 얻을 수 있는 가장 큰 유익이 있다면 롤백 테스트다.
- MySQL에서는 동일한 작업을 수행한 뒤에 롤백하는게 커밋보다 더 빠르기 때문에 성능이 향상되기도 함
6.8.3 테스트를 위한 트랜잭션 애노테이션
@Transactional
- 테스트에서 사용하는 이 애노테이션은 AOP를 위한 것은 아님
@Rollback
- 테스트용 @Transactional 애노테이션은 테스트가 끝나면 자동으로 롤백된다.
- 강제 롤백을 원하지 않는 경우에는 @Rollback(false) 애노테이션을 사용하자
@TransactionConfiguration
NotTransactional과 Propagation.NEVER
효과적인 DB 테스트
- 일반적으로 의존, 협력 오브젝트를 사용하지 않는 단위 테스트와, DB 같은 외부의 리소스나 여러 계층 클래스가 참여하는 통합 테스트는 아예 구분해서 만드는 게 좋다.
- 테스트는 어떤 경우에도 서로 의존하면 안 된다.
6.9 정리
- 목 오브젝트를 활용하면 의존관계에 있는 오브젝트로 쉽게 고립된 테스트로 만들 수 있다.
- DI를 이용한 트랜잭션의 분리는 데코레이터 패턴과 프록시 패턴으로 이해될 수 있다.
- AOP는 OOP만으로 모듈화하기 힘든 부가기능을 효과적으로 모듈화하도록 도와주는 기술이다.
- AOP를 이용해 트랜잭션 속성을 지정할 때, 포인트컷 표현식과 메소드 이름 패턴을 이용하는 방법과 타깃에 직접 부여하는 @Transactioanl 애노테이션을 사용하는 방법이 있다.