-
스프링 DB 데이터접근 기술 - 스프링 트랜잭션 이해Spring-Boot/스프링 DB 2편 - 데이터 접근 기술 2022. 9. 23. 17:47
* 선언적 트랜잭션 관리 vs 프로그래밍 방식 트랜잭션 관리 *
선언적 트랜잭션 관리 ( Declarative Transaction Management )
- @Transactional 애노테이션 하나만 선언해서 매우 편리하게 트랜잭션을 적용하는 것을 선언적 트랜잭션 관리라 한다.
- 선언적 트랜잭션 관리는 과거 XML에 설정하기도 했다
- 이름 그대로 해당 로직에 트랜잭션을 적용하겠다 라고 어딘가에 선언하기만 하면 트랜잭션이 적용되는 방식이다
프로그래밍 방식의 트랜잭션 관리( programmatic transaction management )
- 트랜잭션 매니저 또는 트랜잭션 템플릿 등을 사용해서 트랜잭션 관련 코드를 직접 작성하는 것을 프로그래밍 방식의 트랜잭션 관리라 한다.
- 프로그래밍 방식의 트랜잭션 관리를 사용하게 되면, 애플리케이션 코드가 트랜잭션이라는 기술 코드와 강하게 결합
- 선언적 트랜잭션 관리가 프로그래밍 방식에 비해서 훨씬 간편하고 실용적이기 때문에 실무에서는 대부분 선언적 트랜잭션 관리를 사용한다.
선언적 트랜잭션과 AOP
- @Transactional 을 통한 선언적 트랜잭션 관리 방식을 사용하게 되면 기본적으로 프록시 방식의 AOP가 적용
- 트랜잭션을 처리하기 위한 프록시를 적용하면 트랜잭션을 처리하는 객체와 비즈니스 로직을 처리하는 서비스 객체를 명확하게 분리할 수 있다.
* 스프링 컨테이너에 트랜잭션 프록시 등록 *
- `@Transactional` 애노테이션이 특정 클래스나 메서드에 하나라도 있으면 트랜잭션 AOP는 프록시를 만들어서 스프링 컨테이너에 등록한다.
* 로그 추가*
logging.level.org.springframework.transaction.interceptor=TRACE
- 이 로그를 추가하면 트랜잭션 프록시가 호출하는 트랜잭션의 시작과 종료를 명확하게 로그로 확인 가능
트랜잭션 적용 위치
- 스프링에서 우선순위는 항상 *더 구체적이고 자세한 것이 높은 우선 순위를 가진다*
- 예를 들어 메서드와 클래스에 애노테이션을 붙일 수 있다면 더 구체적인 메서드가 더 높은 우선순위를 가진다
- 인터페이스와 해당 인터페이스를 구현한 클래스에 애노테이션을 붙일 수 있다면 더 구체적인 클래스가 높은 우선 순위를 가진다.
- 스프링의 `@Transactional`은 두 가지 규칙이 있다
1. 우선순위 규칙
2. 클래스에 적용하면 메서드는 자동 적용
- 참고로 `readOnly=false`는 기본 옵션이기 때문에 보통 생략한다.
`@Transactional == @Transactional(readOnly=false)` 와 같다.
* 인터페이스에 @Transactional 적용 *
- 인터페이스에도 `@Transactional`을 적용할 수 있다.
- 다음 순서로 적용
ㆍ1. 클래스의 메서드( 우선순위가 가장 높다 )
ㆍ2. 클래스의 타입
ㆍ3. 인터페이스의 메서드
ㆍ4. 인터페이스의 타입 ( 우선순위가 가장 낮다 )
- 그런데 인터페이스에 `@Transactional` 사용하는 것은 스프링 공식 메뉴얼에서 권장하지 않는 방법이다.
- AOP를 적용하는 방식에 따라서 인터페이스에 애노테이션을 두면 AOP가 적용되지 않는 경우도 있기 때문이다
- 가급적 구체 클래스에 `@Transactional`을 사용하자
트랜잭션 AOP 주의 사항 - 프록시 내부 호출
- 트랜잭션AOP는 기본적으로 프록시 방식의 AOP를 사용한다.
- `@Transactional`을 적용하면 프록시 객체가 요청을 먼저 받아서 트랜잭션을 처리하고 실제 객체를 호출
- AOP를 적용하면 스프링은 대상 객체 대신에 프록시를 스프링 빈으로 등록한다.
- 따라서 스프링은 의존관계 주입 시에 항상 실제 객체 대신에 프록시 객체를 주입힌다.
- 하지만 *대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생*
- 이렇게 되면 @Transactional 이 있어도 트랜잭션이 적용되지 않는다.
* 프록시와 내부 호출 *
* 문제 원인 *
- 자바 언어에서 메서드 앞에 별도의 참조가 없으면 `this`라는 뜻으로 자기 자신의 인스턴스를 가리킨다
- 결과적으로 자기 자신의 내부 메서드 호출하면 실제 대상 객체의 인스턴스를 뜻한다.
- 결과적으로 이러한 내부 호출은 프록시를 거치지 않는다. 따라서 트랜잭션을 적용할 수 없다.
* 프록시 방식의 AOP 한계 *
- `@Transacitonal` 를 사용하는 트랜잭션 AOP는 프록시를 사용한다. 프록시를 사용하면 메서드 내부 호출에 프록시를 적용할 수 없다.
- 해결 방안
ㆍ여러가지 해결방안이 있지만 실무에서는 별도의 클래스로 분리하는 방법을 주로 사용
public 메서드만 트랜잭션 적용
- 스프링 트랜잭션 AOP 기능은 `public`메서드에만 트랜잭션을 적용하도록 기본 설정되어 있다.
- 그래서 `protected`, `private`, `package-visible` 에는 트랜잭션이 적용되지 않는다.
- 생각해보면 `protected`, `package-visible`도 외부에서 호출이 가능하다. 따라서 앞서 설명한 프록시의 내부 호출과는 무관하고, 스프링이 막아둔 것이다.
- 클래스 레벨에 트랜잭션을 적용하면 모든 메서드에 트랜잭션이 걸릴 수 있다.
- 그러면 트랜잭션이 의도하지 않는 곳까지 트랜잭션이 과도하게 적용된다.
- 트랜잭션은 주로 비즈니스 로직의 시작점에 걸기 때문에 대부분 외부에 열어준 곳을 시작점으로 사용한다.
- 이런 이유로 `public` 메서드에만 트랜잭션을 적용하도록 설정되어 있다.
- 참고로 `public`이 아닌곳에 `@Transactional`이 붙어 있으면 예외가 발생하지는 않고, 트랜잭션 적용만 무시된다.
트랜잭션 AOP 주의 사항 - 초기화 시점
- 스프링 초기화 시점에는 트랜잭션 AOP가 적용되지 않을 수 있다.
- 초기화 코드 ( 예: `@PostConstruct` ) 와 `@Transactional`을 함께 사용하면 트랜잭션이 적용되지 않는다.
- 왜냐하면 초기화 코드가 먼저 호출되고, 그 다음에 트랜잭션 AOP가 적용되기 때문이다.
- 따라서 초기화 시점에는 해당 메서드에서 트랜잭션을 획득할 수 없다.
- 가장 확실한 대안은 `ApplicationReadyEvent` 이벤트를 사용하는 것이다.
@EventListener(ApplicationReadyEvent.class) @Transactional public void initV2() { boolean isActive = TransactionSynchronizationManager.isActualTransactionActive(); log.info("Hello init ApplicationReadyEvent tx active={}",isActive); }
- 이 이벤트는 트랜잭션 AOP를 포함한 스프링이 컨테이너가 완전히 생성되고 난 다음에 이벤트가 붙은 메서드를 호출해준다.
트랜잭션 옵션
* value, transactionManager *
- 트랜잭션을 사용하려면 먼저 스프링 빈에 등록된 어떤 트랜잭션 매니저를 사용할 지 알아야 한다.
- 코드로 직접 트랜잭션을 사용할 때 트랜잭션 매니저를 주입 받아서 사용했다.
@Transactional 에서도 트랜잭션 프록시가 사용할 트랜잭션 매니저를 지정해주어야 한다.
- 사용할 트랜잭션 매니저를 지정할 때는 `value`, `transactionManager` 둘 중 하나에 트랜잭션 매니저의 스프링 빈의 이름을 적어주면 된다.
- 이 값을 생략하면 기본으로 등록된 트랜잭션 매니저를 사용하기 때문에 대부분 생략한다.
그런데 사용하는 트랜잭션 매니저가 둘 이상이라면 트랜잭션 매니저의 이름을 지정해서 구분
- 참고로 애노테이션에서 속성이 하나인 경우 `value`는 생략 가능
* rollbackFor *
- 예외 발생시 스프링 트랜잭션의 기본 정책은 다음과 같다.
ㆍ언체크 예외인 `RuntimeException, Error` 와 그 하위 예외가 발생하면 롤백한다.
ㆍ체크 예외인 `Exception`과 그 하위 예외들은 커밋한다.
- 이 옵션을 사용하면 기본 정책에 추가로 어떤 예외가 발생할 때 롤백할 지 지정할 수 있다.
@Transactional(rollbackFor = Exception.class)
- 예를 들어 이렇게 지정하면 체크 예외인 `Exception`이 발생해도 롤백하게 된다. ( 하위 예외들도 대상에 포함 )
- `rollbackForClassName` 도 있는데, `rollbackFor`는 예외 클래스를 직접 지정하고, `rollbackForClassName`는 예외 이름을 문자로 넣으면 된다.
* noRollbackFor *
- 앞서 설명한 `rollbackFor`와 반대이다.
- 기본정책에 추가로 어떤 예외가 발생했을 때 롤백하면 안되는지 지정할 수 있다.
- 예외 이름을 넣을 수 있는 `noRollbackForClassName`도 있다
* isolation *
- 트랜잭션 격리 수준을 지정할 수 있다.
- 기본 값은 데이터베이스에서 설정한 트랜잭션 격리 수준을 사용하는 `DEFAULT`이다.
- 대부분 데이터베이스에서 설정한 기준을 따른다.
애플리케이션 개발자가 트랜잭션 격리 수준을 직접 지정하는 경우는 드물다.
ㆍ`DEFAULT` : 데이터베이스에서 설정한 격리 수준을 따른다
ㆍ`READ_UNCOMMITED` : 커밋되지 않은 읽기
ㆍ`READ_COMMITTED` : 커밋된 읽기
ㆍ`REPEATABLE_READ` : 반복 가능한 읽기
ㆍ`SERIALIZABLE` : 직렬화 가능
* timeout *
- 트랜잭션 수행 시간에 대한 타임아웃을 초 단위로 지정한다.
- 기본 값은 트랜잭션 시스템의 타임아웃을 사용한다.
- 운영 환경에 따라 동작하는 경우도 있고 그렇지 않은 경우도 있기 때문에 꼭 확인하고 사용해야 한다.
- `timeoutString`도 있는데, 숫자 대신 문자 값으로 지정할 수 있다.
* label *
- 트랜잭션 애노테이션에 있는 값을 직접 읽어서 어떤 동작을 하고 싶을 떄 사용할 수 있다. 일반적으로 사용하지 않는다
* readOnly *
- 트랜잭션은 기본적으로 읽기 쓰기가 모두 가능한 트랜잭션이 생성된다
- `readOnly=true` 옵션을 사용하면 읽기 전용 트랜잭션이 생성된다.
이 경우 등록, 수정, 삭제가 안되고 읽기 기능만 작동한다. ( 드라이버나 DB에 따라 정상 작동하지 않는 경우도 있다 )
- 그리고 `readOnly` 옵션을 사용하면 읽기에서 다양한 성능 최적화가 발생할 수 있다.
*readOnly 옵션은 크게 3곳에 적용된다 *
1. * 프레임 워크 *
- JdbcTemplate은 읽기 전용 트랜잭션 안에서 변경 기능을 실행하면 예외를 던진다.
2. *JDBC드라이버*
- 참고로 여기서 설명하는 내용들은 DB와 드라이버 버전에 따라서 다르게 동작하기 때문에 사전 확인 필요
- 읽기 전용 트랜잭션에서 변경 쿼리가 발생하면 예외를 던진다
- 읽기, 쓰기(마스터, 슬레이브) 데이터베이스를 구분해서 요청한다. 읽기 전용 트랜잭션인 경우 읽기(슬레이브) 데이터베이스의 커넥션을 획득해서 사용한다
3. *데이터 베이스*
- 데이터베이스에 따라 읽기 전용 트랜잭션의 경우 읽기만 하면 되므로, 내부에서 성능 최적화가 발생
예외와 트랜잭션 커밋, 롤백
- 예외가 발생했는데, 내부에서 예외를 처리하지 못하고, 트랜잭션 범위(`@Transactional가 적용된 AOP`) 밖으로 예외를 던지면 어떻게 될까
- 예외 발생시 스프링 트랜잭션 AOP는 예외의 종류에 따라 트랜잭션을 커밋하거나 롤백한다.
ㆍ언체크 예외인 `RuntimeException`, `Error`와 그 하위 예외가 발생하면 트랜잭션을 롤백한다
ㆍ체크 예외인 `Exception`과 그 하위 예외가 발생하면 트랜잭션을 커밋한다
ㆍ물론 정상 응답(리턴)하면 트랜잭션을 커밋한다.
- 스프링은 왜 체크 예외는 커밋하고, 언체크(런타임) 예외는 롤백할까?
스프링은 기본적으로 체크 예외는 비즈니스 의미가 있을 때 사용하고, 런타임(언체크) 예외는 복구 불가능한 에외로 가정
ㆍ체크 예외 : 비즈니스 의미가 있을 때 사용
ㆍ언체크 예외 : 복구 불가능한 예외
- 참고로 꼭 이런 정책을 따를 필요는 없다. `rollbackFor` 라는 옵션을 사용해서 체크 예외도 롤백하면 된다
'Spring-Boot > 스프링 DB 2편 - 데이터 접근 기술' 카테고리의 다른 글
스프링 DB 데이터접근 기술 - 스프링 트랜잭션 전파 기본 (0) 2022.09.27 스프링 DB 데이터접근 기술 - 활용 방안 (0) 2022.09.23 스프링 DB 데이터접근 기술 - Querydsl (0) 2022.09.23 스프링 DB 데이터접근 기술 - 스프링데이터 JPA (0) 2022.09.22 스프링 DB 데이터접근 기술 - JPA (0) 2022.09.22