트랜잭션 추상화와 동기화
트랜잭션 추상화
트랜잭션 서비스의 종류는 다양하다.
스프링은 데이터 액세스 기술과 트랜잭션 서비스 사이의 종속성을 제거하고 스프링이 제공하는 트랜잭션 추상화 계층을 이용해서 트랜잭션 기능을 활용하도록 만들어준다.
이를 통해 트랜잭션 서비스의 종류나 환경이 바뀌더라도 트랜잭션을 사용하는 코드는 그대로 유지할 수 있는 유연성을 얻을 수 있다.
트랜잭션 동기화
트랜잭션 동기화는 트랜잭션을 일정 범위 안에서 유지해주고, 어디서든 자유롭게 접근할 수 있게 만들어준다.
PlatformTransactionManager
스프링 트랜잭션 추상화의 핵심 인터페이스는 PlatformTransactionManager로, 모든 스프링의 트랜잭션 기능과 코드는 이 인터페이스를 통해서 로우 레벨의 트랜잭션 서비스를 이용할 수 있다.
PlatformTransactionManager는 트랜잭션 경계를 지정하는 데 사용한다. 트랜잭션이 어디서 시작하고 종료하는지, 종료할 때 커밋인지, 롤백인지를 결정하는 것이다. 또한 시작과 종료를 트랜잭션 전파 기법을 이용해 자유롭게 조합하고 확장할 수 있다.
cf) Spring 5부터는 리액티브 애플리케이션을 위한 ReactiveTransactionManager가 추가되었다고 합니다.
트랜잭션 경계설정 전략
트랜잭션의 시작과 종료가 되는 경계는 보통 서비스 계층 오브젝트의 메소드이다.
트랜잭션 경계를 설정하는 방법은 코드에 의한 프로그래밍 방법과 AOP를 이용한 선언적인 방법으로 구분할 수 있다. 보통 AOP를 이용한 @Transactional을 통한 선언적 트랜잭션 경계 설정 방법을 사용한다.
선언적 트랜잭션 경계설정, @Transactional
선언적 트랜잭션을 이용하면 메인 비즈니스 코드에는 전혀 영향을 주지 않으면서 특정 메소드 실행 전후에 트랜잭션이 시작되고 종료되거나 기존 트랜잭션에 참여하도록 만들 수 있다. 이를 위해서는 데코레이터 패턴을 적용한 트랜잭션 프록시 빈을 사용해야 한다.
선언적 트랜잭션 경계설정은 트랜잭션 프록시 빈 덕분에 가능한 것이다. 트랜잭션은 대부분 성격이 비슷하기 때문에 적용 대상마다 일일이 선언해주기보다는 일괄적으로 선언하는 것이 편리하다. 그래서 간단한 설정으로 특정 부가기능을 임의의 타깃 오브젝트에 부여해줄 수 있는 프록시 AOP를 주로 활용한다.
프록시 모드: 인터페이스와 클래스
Dynamic Proxy
스프링의 AOP는 기본적으로 Dynamic Proxy 기법을 이용해 동작한다. Dynamic Proxy를 적용하려면 인터페이스가 있어야 한다.
CGLib
하지만 인터페이스가 없는 경우에는 스프링이 지원하는 클래스 프록시 모드를 사용하면 된다. 스프링에서는 CGLib 라이브러리가 제공하는 클래스 레벨의 프록시도 사용할 수 있다.
다만 클래스 프록시를 사용하는 경우에 상속을 허용하지 않으면 사용할 수 없다. (private 생성자 혹은 final class)
왜냐하면 클래스 프록시는 타깃 클래스를 상속해서 프록시를 만드는 방법을 사용하기 때문이다.
또한 클래스 프록시를 적용하면 클래스의 생성자가 두 번 호출된다. 상속을 통해 프록시를 만들기 때문에 발생하는 현상인데, 이 때문에 생성자에서 리소스를 할당하는 것 같은 중요한 작업은 피하도록 해야한다. (참고로 CGLib 3.X 부터 해결되었다고 한다..)
Spring Boot에서는...
Spring Boot 2.X에서는 인터페이스가 있더라도 기본적으로 CGLib을 사용해서 프록시 객체를 생성한다.
왜인지 봤더니 CGLib이 2.X에서 3.X로 올라오면서 기존의 클래스의 생성자를 두 번 호출하는 문제나 디폴트 생성자가 있어야만 프록시를 생성할 수 있는 등의 문제가 많이 개선되었다고 한다.
이러한 이유로 Spring Boot 2.X부터는 성능이 더 좋은 CGLib을 사용해서 기본적으로 프록시를 생성한다고 한다.
자세한 내용은 아래 내용을 참고하면 좋다.
https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html
AOP 방식: 프록시와 AspectJ
스프링의 AOP는 기본적으로 프록시 방식이다. 인터페이스를 이용하는 JDK Dynamic Proxy든 클래스에 바로 프록시를 만드는 CGLib이든, 모두 프록시 오브젝트를 타깃 오브젝트 앞에 두고 호출 과정을 가로채서 트랜잭션과 같은 부가적인 작업을 진행해준다.
스프링의 프록시 AOP 대신 AOP 전용 프레임워크인 AspectJ의 AOP를 사용할 수 있다. AspectJ AOP는 스프링과 달리 프록시를 타깃 오브젝트 앞에 두지 않는다. 대신 타깃 오브젝트 자체를 조작해서 부가기능을 직접 넣는 방식이다.
AspectJ를 사용하면, 마치 처음부터 타깃 오브젝트의 클래스에 부가기능을 가진 소스코드가 있었던 것처럼 만들어준다. 때문에 매우 강력하다. 메소드 실행 지점만 조인 포인트로 사용할 수 있는 프록시 방식의 스프링 AOP에서는 불가능한 다양한 조인 포인트와 고급 기능을 이용할 수 있다. 대신 별도의 빌드 과정이나 바이트코드 조작을 위한 로드 타임 위버 설정과 같은 부가적인 작업이 필요하다.
트랜잭션 AOP를 적용하기 위해 굳이 번거롭게 AspectJ를 사용할 필요는 없지만, 다음과 같은 경우라면 AspectJ를 통해 트랜잭션 AOP를 검토해볼 수 있다.
프록시 AOP의 한계
프록시가 적용되면 클라이언트는 프록시를 타깃 오브젝트라고 생각하고 프록시의 메소드를 호출한다. 프록시는 클라이언트로부터 요청을 받으면 타깃 오브젝트의 메소드로 위임해준다. 이때 부가작업을 추가할 수 있다. 트랜잭션 AOP에 의해 추가된 프록시라면 타깃 오브젝트 메소드 호출 전에 트랜잭션을 시작하고 호출 후에 트랜잭션을 커밋하거나 롤백해줄 것이다.
여기서 프록시는 클라이언트가 타깃 오브젝트를 호출하는 과정에서만 동작한다는 점을 주목하자.
타깃 오브젝트의 메소드가 자기 자신의 다른 메소드를 호출할 때는 프록시가 동작하지 않는다. 즉 자기 자신의 다른 메소드를 호출하는 경우에는 프록시를 통하지 않고 직접 타깃 오브젝트의 메소드로 호출이 일어나는 문제점이 있다.
이렇게 타깃 오브젝의 자기 호출에는 AOP가 적용되지 않는다는 점이 프록시 AOP의 한계이다.
이 문제를 해결하기 위해서는 AspectJ AOP를 사용하는 방법을 고려해볼 수 있다.
AspectJ에서는..?
AspectJ는 프록시 대신 클래스 바이트코드를 직접 변경해서 부가기능을 추가하기 때문에 타깃 오브젝트의 자기 호출 중에도 트랜잭션 부가기능이 잘 적용된다.
트랜잭션 속성
트랜잭션 전파 - propagation
트랜잭션을 시작하거나 기존 트랜잭션에 참여하는 방법을 결정하는 속성.
REQUIRED
- Default 속성
- 미리 시작된 트랜잭션이 있으면 참여하고 없으면 새로 시작하는 전파 속성.
SUPPORTED
- 이미 시작된 트랜잭션이 있다면 참여하고, 그렇지 않으면 트랜잭션 없이 진행하게 하는 전파 속성.
MANDATORY
- REQUIRED가 비슷하게 이미 시작된 트랜잭션이 있으면 참여한다.
- 하지만 트랜잭션이 시작된 것이 없으면 새로 시작하는 대신 예외를 발생시킨다.
REQUIRED_NEW
- 항상 새로운 트랜잭션을 시작한다.
- 이미 진행 중인 트랜잭션이 있으면 트랜잭션을 잠시 보류시킨다.
NOT_SUPPORTED
- 트랜잭션을 사용하지 않게 한다. 이미 진행중인 트랜잭션이 있으면 보류시킨다.
NEVER
- 트랜잭션을 사용하지 않도록 강제한다.
- 이미 진행 중인 트랜잭션이 있다면 예외를 발생시킨다.
NESTED
- 이미 진행중인 트랜잭션이 있으면 중첩 트랜잭션을 시작한다. (트랜잭션 안에 다시 트랜잭션을 만드는 것)
- REQUIRED_NEW와 다른 점은 중첩된 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만, 자신의 커밋과 롤백은 부모 트랜잭션에게 영향을 주지 않는다.
트랜잭션 격리 수준 - isolation
트랜잭션 격리 수준은 동시에 여러 트랜잭션이 진행될 때 트랜잭션 작업 결과를 여타 트랜잭션에게 어떻게 노출할 것인지를 결정하는 기준이다.
DEFAULT
- 사용하는 데이터 액세스 기술 or DB 드라이버의 디폴트 설정을 따른다.
- 보통 드라이버의 격리 수준은 DB의 격리 수준을 따르는 게 일반적이다.
그 외에 DB의 격리 수준인 READ_UNCOMMITED, READ_COMMITED, REPEATABLE_READ, SERIALIZALE 격리 수준을 제공한다.
트랜잭션 격리 수준에 관한 자세한 내용은 다음 링크를 참고.
https://willseungh0.tistory.com/34?category=874332
트랜잭션 제한시간: timeout
트랜잭션에 제한시간을 지정할 수 있다.
읽기 전용 트랜잭션: readOnly
트랜잭션을 읽기 전용으로 설정할 수 있다.
- 성능을 최적화하기 위해 사용할 수도 있고 특정 트랜잭션 작업 안에서 쓰기 작업이 일어나는 것을 의도적으로 방지하기 위해 사용할 수 도 있다.
- 일반적으로 읽기 전용 트랜잭션이 시작된 이후, INSERT, UPDATE, DELETE 같은 쓰기 작업이 진행되면 예외가 발생한다.
트랜잭션 롤백 예외: rollback-for
Default로, 선언적 트랜잭션에서는 에러나, 런타임 예외(UncheckedException)가 발생하면 롤백한다. 반면에 예외가 전혀 발생하지 않거나 체크 예외(Checked Exception)이 발생하면 롤백하지 않고, 커밋한다.
- 만약 원한다면 기본 동작 방식을 바꿀 수 있다.
참고
- 토비의 스프링 3.1 Vol.2 스프링의 기술과 선택
- Spring Docs https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#spring-data-tier
'Application > Spring Framework' 카테고리의 다른 글
Spring MVC, DispatcherServlet (0) | 2021.08.29 |
---|---|
로컬에서 임베디드 S3 사용하기 (4) | 2021.07.27 |
스프링 인터셉터와 어노테이션으로 인증 및 권한 관리하기 (0) | 2021.03.08 |
[Spring] IoC 컨테이너와 DI와 빈의 스코프 (0) | 2021.02.16 |
[스프링 인 액션 정리] 11-12 리액티브 API, 데이터 퍼시스턴스 (0) | 2021.01.12 |