Study with book/토비의 스프링 3.1

6장. AOP4


6.6 트랜잭션 속성

6.6.1 트랜잭션 정의

트랜잭션 전파

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

1. PROPAGATION_REQUIRED

가장 많이 사용되는 트랜잭션 속성이다. 진행 중인 트랜잭션이 없으면 새로 시작하고, 이미 시작된 트랜잭션이 있으면 이에 참여한다. DefaultTransactionDefinition의 트랜잭션 전파 속성은 바로 이 PROPAGATION_REQUIRED이다.

 

2. PROPAGATION_REQUIRED_NEW

항상 새로운 트랜잭션을 시작한다. 즉 앞에서 시작된 트랜잭션이 있든 없든 상관없이 새로운 트랜잭션을 만들어서 독자적으로 동작하게 한다.

 

3. PROPAGATION_NOT_SUPPORTED

트랜잭션이 없이 동작하도록 만들 수 있다. 진행 중인 트랜잭션이 있어도 무시한다. 트랜잭션 경계설정은 보통 AOP를 이용해 한 번에 많은 메소드에 동시에 적용하는 방법을 사용한다. 그런데 그 중에서 특별한 메소드만 트랜잭션 적용에서 제외하려면 어떻게 해야 할까? 이 속성을 이용해서 제외시킬 수 있다.

 

격리수준

DB 트랜잭션은 격리수준을 갖고 있어야 한다. 서버 환경에서는 여러 개의 트랜잭션이 동시에 진행될 수 있다. 따라서 적절하게 격리수준을 조정해서 가능한 한 많은 트랜잭션을 동시에 진행시키면서 문제가 발생하지 않게 하는 제어가 필요하다. DefaultTransactionDefinition에 설정된 격리수준은 ISOLATION_DEFAULT다. 이는 DataSource에 설정되어 있는 디폴트 격리수준을 그대로 따른 것이다.

 

제한시간

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

 

읽기전용

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

 

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

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/6.6.2

메소드별로 다른 트랜잭션 정의를 적용하려면 어드바이스의 기능을 확장해야 한다. 마치 초기에 TransactionHandler에서 메소드 이름을 이용해 트랜잭션 적용 여부를 판단했던 것과 비슷한 방식을 사용하면 된다.

 

TransactionInterceptor

스프링이 제공하는 TransactionInterceptor에는 기본적으로 두 가지 종류의 예외 처리 방식이 있다. 런타임 예외가 발생하면 트랜잭션은 롤백된다. 반면에 타깃 메소드가 런타임 예외가 아닌 체크 예외를 던지는 경우는 이것을 예외상황으로 해석하지 않고 일종의 비지니스 로직에 따른 의미가 있는 리턴 방식의 한 가지로 인식해서 트랜잭션을 커밋해버린다. 스프링의 기본적인 예외처리 원칙에 따라 비지니스적인 의미가 있는 예외상황에만 체크 예외를 사용하고, 그 외의 모든 모든 복구 불가능한 순수한 예외의 경우는 런타임 예외로 포장돼서 전달하는 방식을 따른다고 가정하기 때문이다. 그런데 TransactionInterceptor의 이러한 예외처리 기본 원칙을 따르지 않는 경우가 있다. 그래서 TransactionAttribute는 rollback() 이라는 속성을 둬서 기본 원칙과 다른 예외처리가 가능하게 해준다. 이를 활용하면 특정 체크 예외의 경우는 트랜잭션을 롤백시키고, 특정 런타임 예외에 대해서는 트랜잭션을 커밋시킬 수도 있다.

 

tx 네임스페이스를 이용한 설정 방법

트랜잭션 어드바이스도 포인트컷이나 어드바이저만큼 자주 사용되고, 애플리케이션의 컴포넌트가 아닌 컨테이너가 사용하는 기반기술 설정의 한 가지이기 때문이다.

 

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

트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 이용한다.

쓰기 작업이 없는 단순한 조회 작업만 하는 메소드에도 모두 트랜잭션을 적용하는게 좋다. 조회의 경우에는 읽기전용으로 트랜잭션 속성을 설정해두면 그만큼 성능의 향상을 가져올 수 있다. 또, 복잡한 조회의 경우 제한시간을 지정해줄 수도 있고, 격리 수준에 따라 조회도 반드시 트랜잭션 안에서 진행해야 할 필요가 발생하기도 한다. 따라서 트랜잭션용 포인트컷 표현식에는 메소드나 파라미터, 예외에 대한 패턴을 정의하지 않는 게 바람직하다. 트랜잭션의 경계로 삼을 클래스들이 선정됐다면, 그 클래스들이 모여 있는 패키지를 통째로 선택하거나 클래스 이름에서 일정한 패턴을 찾아서 표현식으로 만들면 된다.

메소드의 시그니처를 이용한 execution() 방식의 포인트컷 표현식 대신 스프링의 빈 이름을 이용하는 bean() 표현식을 사용하는 방법도 고려해볼 만하다.

 

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

너무 다양하게 트랜잭션 속성을 부여하면 관리만 힘들어질 뿐이다. 따라서 기준이 되는 몇 가지 트랜잭션 속성을 정의하고 그에 따라 적절한 메소드 명명 규칙을 만들어 두면 하나의 어드바이스만으로 애플리케이션의 모든 서비스 빈에 트랜잭션 속성을 지정할 수 있다.

 

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

프록시 방식의 AOP에서는 프록시를 통한 부가기능의 적용은 클라이언트로부터 호출이 일어날 때만 가능하다. 반대로 타깃 오브젝트가 자기 자신의 메소드를 호출할 때는 프록시를 통한 부가기능의 적용이 일어나지 않는다. 

타깃 안에서의 호출에는 적용되지 않는 프록시

위 그림처럼 타깃 오브젝트 안에서 메소드 호출이 일어나는 경우에는 프록시 AOP를 통해 부여해준 부가기능이 적용되지 않는다는 점을 주의해야 한다. 따라서 같은 오브젝트 안에서 호출은 새로운 트랜잭션 속성을 부여하지 못한다는 사실을 의식하고 개발할 필요가 있다.

 

해결방법은 두가지가 있는데 하나는 스프링의 API를 이용해 프록시 오브젝트에 대한 레퍼런스를 가져온 뒤에 같은 오브젝트의 메소드 호출도 프록시를 이용하도록 강제하는 방법이다. 하지만 복잡한 과정을 거쳐서 순수한 비지니스 로직만을 남겨두려고 노력했는데, 거기에 스프링 API와 프록시 호출 코드가 등장하는 건 그다지 바람직하지 않다.

다른 방법은 AspectJ와 같은 타깃의 바이트코드를 직접 조작하는 방식의 AOP기술을 적용하는 것이다. 스프링은 프록시 기반의 AOP를 기본적으로 사용하고 있지만 필요하면 언제든지 AspectJ 방식으로 변경할 수 있다. 

 

6.6.4 트랜잭션 속성 적용

실습 코드 링크 :https://github.com/vvshinevv/toby-spring/tree/feature/6.6.4

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

트랜잭션 경계설정의 부가기능을 여러 계층에서 중구난방으로 적용하는 건 좋지 않다. 일반적으로 특정 계층의 경계를 트랜잭션 경계와 일치시키는 것이 바람직하다. 비지니스 로직을 담고 있는 서비스 계층 오브젝트의 메소드가 트랜잭션 경계를 부여하기에 가장 적절한 대상이다.

 

서비스 빈에 적용되는 포인트컷 표현식 등록

빈 이름을 사용한 표현식을 갖는 포인트컷과 어드바이저

트랜잭션 속성을 가진 트랜잭션 어드바이스 등록

실습 코드로 대체

 

트랜잭션 속성 테스트

실습 코드로 대체

 

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

6.7.1 트랜잭션 애노테이션

@Transactional

@Transactional 애노테이션의 타깃은 메소드와 타입이다. 따라서 메소드, 클래스, 인터페이스에 사용할 수 있다. 스프링은 @Transactional이 부여된 모든 오브젝트를 자동으로 타깃 오브젝트로 인식한다. 이때 사용되는 포인트컷은 TransactionAttributeSourcePointcut이다. TransactionAttributeSourcePointcut은 스스로 표현식과 같은 선정기준을 갖고 있진 않다. 대신  @Transactional이 타입 레벨이든 메소드 레벨이든 상관없이 부여된 빈 오브젝트를 모두 찾아서 포인트컷의 선정 결과로 돌려준다.

 

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

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

이 방식을 이용하면 포인트컷과 트랜잭션 속성을 애노테이션 하나로 지정할 수 있다. 트랜잭션 속성은 타입 레벨에 일괄적으로 부여할 수도 있지만, 메소드 단위로 세분화해서 트랜잭션 속성을 다르게 지정할 수도 있기 때문에 매우 세밀한 트랜잭션 속성 제어가 가능해진다.

 

대체 정책

스프링은 @Transactional을 적용할 때 4단계의 대체(fallback) 정책을 이용하게 해준다. 메소드의 속성을 확인할 때 타깃 메소드, 타깃 클래스, 선언 메소드, 선언 타입(클래스, 인터페이스) 순서에 따라서 @Transactional이 적용됐는지 차례로 확인하고 가장 먼저 발견되는 속성정보를 사용하게 하는 방법이다. 

@Transactional 대체 정책의 예

@Transactional 대체 정책 순서를 확인해보자.

 1. [5], [6] 

 2. [4]

 3. [2], [3]

 4. [1]

 

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

 

6.7.2 트랜잭션 애노테이션 적용

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/6.7.2

 

6.8 트랜잭션 지원 테스트

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

AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있게 하는 방법을 선언적 트랜잭션이라고 한다. 반대로 TransactionTemplate이나 개별 데이터 기술의 트랜잭션 API를 사용해 직접 코드 안에서 사용하는 방법을 프로그램에 의한 트랜잭션이라고 한다.

 

6.8.2 트랜잭션 동기화 테스트

트랜잭션 매니저와 트랜잭션 동기화

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/6.8.2-1

 

트랜잭션 매니저를 이용한 테스트용 트랜잭션 제어

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/6.8.2-2

 

트랜잭션 동기화 검증

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/6.8.2-3

 

롤백 테스트

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/6.8.2-4

 

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

@Transactional

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/6.8.3-1

 

@Rollback

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/6.8.3-2

 

@TransactionConfiguration

최근 버전(5.2.4)에선 없어진 듯

 

NotTransactional과 Propagation.NEVER

최근 버전(5.2.4)에선 없어진 듯

 

효과적인 DB테스트

일반적으로 의존, 협력 오브젝트를 사용하지 않고 고립된 상태에서 테스트를 진행하는 단위 테스트와 DB 같은 외부의 리소스나 여러 계층의 클래스가 참여하는 통합 테스트는 아예 클래스를 구분해서 따로 만드는게 좋다. 테스트는 어떤 경우에도 서로 의존하면 안 된다. 테스트가 진행되는 순서나 앞의 테스트의 성공 여부에 따라서 다음 테스트의 결과가 달라지는 테스트를 만들면 안 된다. 코드가 바뀌지 않는 한 어떤 순서로 진행되더라도 테스트는 일정한 결과를 내야 한다.


[참고자료]

토비 스프링 vol.1 - 이일민

'Study with book > 토비의 스프링 3.1' 카테고리의 다른 글

6장. AOP3  (0) 2020.04.26
6장. AOP2  (1) 2020.04.19
6장. AOP1  (0) 2020.04.12
5장. 서비스 추상화  (0) 2020.04.05
4장. 예외  (0) 2020.03.29