티스토리 뷰

스프링 프로젝트를 하며, db에 접근하게 되는 경우

@Transactional을 자주 사용하고 있습니다🤔

 

트랜잭션은 데이터베이스의 상태를 변경시키기 위해 수행하는 작업 단위로

완전히 성공하거나 완전히 실패하는 일련의 논리적 작업단위입니다.

 

트랜잭션은 크게 아래 3가지 목적으로 사용해왔습니다.

1. ACID 보장을 위해, 특정 실행 단위에서 오류 발생시 전체 실행 내용을 롤백해주는 기능

2. Dirty Checking 변경 감지 시, 수정 사항 바로 반영

3. ReadOnly 속성

 

프로젝트하며 마주쳤었던 LazyInitializationException 또한 Transactional 어노테이션에 대해

정확히 알지 못하고 사용하여 마주했던 문제라 어노테이션을 한번 뜯어보았습니다🧩

 


공식문서에서 설명하는 @Transactional

가장 신뢰도있는 공식 문서🙏

 

특정 메서드 또는 클래스 수준에서 트랜잭션 속성을 설명하는 어노테이션입니다.
클래스 수준에서 이 어노테이션이 선언되면 해당 클래스와 그 서브클래스의 모든 메서드에 기본적으로 적용됩니다.
상속된 메서드는 서브클래스 수준 어노테이션에 참여하려면 로컬로 다시 선언되어야 합니다.

 

👉어노테이션 적용 우선순위: 메서드 수준 > 클래스 수준 > 상속 수준

내부적으로 전파된다는 점을 고려하고, 어노테이션을 사용해야 함을 알 수 있었습니다.

 

@Transactional 속성

 

rollbackFor 및 noRollbackFor 속성이 구성되지 않은 경우, 트랜잭션은 RuntimeException 및 Error에서 롤백되지만 체크된 예외에서는 롤백되지 않습니다.
롤백 규칙은 특정 예외가 발생할 때 트랜잭션이 롤백되어야 하는지 여부를 결정하며 타입 또는 패턴을 기반으로 합니다.
롤백 규칙은 rollbackFor/rollbackForClassName 및 noRollbackFor/noRollbackForClassName을 통해 구성될 수 있으며,
이를 통해 각각 유형 또는 패턴으로 규칙을 지정할 수 있습니다.

 

👉기본적으로 롤백은 RuntimeException 및 Error에서 이루어지며, 롤백 규칙을 직접 지정하여, 특정 예외가 발생할 때 롤백하도록 설정할 수 있다.

 

트랜잭션 관리

 

일반적으로 org.springframework.transaction.PlatformTransactionManager에 의해 관리되는 스레드 바운드 트랜잭션과 함께 작동하며 현재 실행 중인 스레드 내의 모든 데이터 액세스 작업에 트랜잭션을 노출시킵니다.
주의: 이는 메서드 내에서 새로 시작된 스레드로 전파되지 않습니다.
또는 org.springframework.transaction.ReactiveTransactionManager에 의해 관리되는 리액티브 트랜잭션을 나타낼 수 있으며
이 경우 모든 참여 데이터 액세스 작업은 동일한 리액터 컨텍스트 내에서 실행되어야 합니다.

 

트랜잭션 옵션: propagation, isolation, timeout, readOnly, rollbackFor, noRollBackFor

1️⃣propagation 트랜잭션 전파 유형

현재 진행중인 트랜잭션이 존재할 때, 새로운 트랜잭션 메서드 호출하는 경우 어떤 정책을 사용할지를 결정합니다.
기존 트랜잭션을 이어받을지, 새로운 트랜잭션을 생성할지 설정할 수 있습니다.

 

1. REQUIRED🌿

  • 기본값
  • 부모 트랜잭션이 존재할 경우 참여하고 없는 경우 새 트랜잭션을 시작

 

2. SUPPORTS

  • 부모 트랜잭션이 존재할 경우 참여하고 없는 경우 non-transactional 상태로 실행

3. MANDATORY

  • 부모 트랜잭션이 있으면 참여하고 없으면 예외 발생

👉부모를 우선시 하는 유형

 

4. REQUIRES_NEW

  • 부모 트랜잭션을 무시하고 무조건 새로운 트랜잭션이 생성
  • 부모를 무시하는 유형!

5. NOT_SUPPORTED

  • non-transactional 상태로 실행하며 부모 트랜잭션이 존재하는 경우 일시 정지시킴

6. NEVER

  • non-transactional 상태로 실행하며 부모 트랜잭션이 존재하는 경우 예외 발생

👉 부모를 우선시 하지 않는 유형

 

NESTED

  • 부모 트랜잭션과는 별개의 중첩된 트랜잭션을 만듦.
  • 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만 자신의 커밋과 롤백은 부모 트랜잭션에게 영향을 주지 않음
  • 부모 트랜잭션이 없는 경우 새로운 트랜잭션을 만듬
  • DB 가 SAVEPOINT 를 지원해야 사용 가능 (Oracle)
  • JpaTransactionManager 에서는 지원하지 않는다.

 


2️⃣isolation 트랜잭션 격리 수준

기본값은 Isolation.DEFAULT으로 사용하는 DB의 기본 격리 수준을 따릅니다.

 

 

➕MySQL 8.0 버전의 기본 격리 수준은 Inno DB의 REPEATABLE_READ

 

👉격리성이란 동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않는 것을 의미합니다.

 

1. READ_UNCOMMITTED

  • 한 트랜잭션이 처리 중인 커밋되지 않은 데이터를 다른 트랜잭션에서 접근 가능

 

2. READ_COMMITTED

  • 트랜잭션은 커밋한 데이터만 읽을 수 있다.

 

3. REPEATABLE_READ

  • 하나의 트랜잭션은 하나의 스냅샷만 사용한다.
  • 조회한 데이터에서만 Shared Lock이 걸리기 때문에 다른 트랜잭션이 새로운 데이터를 추가할 수 있다.

 

4. SERIALIZABLE

  • 가장 단순하고 엄격한 격리 수준
  • 순차적으로 트랜잭션을 진행시키며 읽기 작업에도 잠금을 걸어 여러 트랜잭션이 동시에 같은 데이터에 접근하지 못한다.


3️⃣timeout

트랜잭션의 타임아웃을 초 단위로 지정합니다.
기본값은 -1로, timeout을 지원하지 않습니다.
지정한 시간 내에 해당 메서드 수행이 완료되지 않은 경우 JpaSystemException을 발생시킨다.

 


4️⃣readOnly

트랜잭션이 읽기 전용인지 여부를 나타냅니다.
기본값은 false로, true인 경우 읽기 전용으로 사용됩니다.
성능 향상을 위해 사용하거나 읽기 외의 다른 동작을 방지하기 위해 true로 설정하여 사용합니다.

 

🧐어떻게 성능이 향상되는걸까??

readOnly = true를 설정하게 되면 JPA는 해당 트랜잭션 내에서 조회하는 Entity를 조회용으로 인식하고

변경 감지를 위한 스냅샷을 따로 보관하지 않으므로 메모리가 절약된다고 합니다.

 

➕개발자가 읽기 전용 메서드라는 것을 파악 가능!

 

 

5️⃣rollbackFor, rollbackForClassName

롤백이 수행되어야 하는 예외 타입 또는 패턴을 지정합니다.
기본값은 RuntimeException, Error로
예외 타입을 지정하더라도 기본값으로 RuntimeException, Error은 여전히 적용됩니다.

 

강제로 데이터 롤백을 막고 싶다면(그런 경우가 있나?) noRollbackFor 옵션으로 지정해주면 됩니다.

 

6️⃣noRollbackFor, noRollbackForClassName

롤백이 수행되지 않아야 하는 예외 타입 또는 패턴을 지정합니다.

 

 

 


JPA 사용 시에 테스트 코드 작성 시 @Transactional 주의🚨

테스트 코드 작성 시에는,

데이터를 실제 db에 반영하지 않기에 @Transactional 사용을 통해 롤백을 시켜주는 경우가 많습니다.

 

테스트 코드에서 @Transactional을 사용하고, 서비스 코드에서 사용하지 않은 경우에 어떤 문제가 있을까?

 

1️⃣서비스 코드에 @Transactional 존재, 테스트 코드에 @Transactional 존재

게시글 생성 성공 Test Code

 

 

테스트 코드 통과

 

 

PostService

 

 

User

 

 

실제 서버 실행 후 요청 성공

 

2️⃣테스트 코드에 @Transactional 존재하지만, 서비스 코드에 까먹고 안적어준 경우🚨🚨

 

PostService

 

나머지는 동일

 

테스트 코드 통과

 

 

실제 서버 실행 후 요청 실패

2023-12-11T02:50:43.929+09:00 ERROR 6352 --- [nio-8080-exec-1] j.s.config.GlobalExceptionHandler        : failed to lazily initialize a collection of role: jehs.springbootboardjpa.entity.User.posts: could not initialize proxy - no Session

 

LazyInitailizationException가 발생한 것을 확인할 수 있습니다.

 

실제 실행 시에는

서비스 코드의 메서드에 @Transactional을 적용하지 않아

영속성 컨텍스트가 존재하지 않아서 Lazy Loading이 불가능하여 런타임 에러가 발생한 것이고,

 

테스트 코드는 

@Transactional 이 존재하기 때문에 영속성 컨텍스트가 존재하면서 Lazy Loading이 가능하여 테스트가 성공한 것입니다.

 

➕LAZY FETCH

  • 프록시 객체의 초기화는 영속성 컨텍스트의 도움을 받아야 가능합니다. 따라서 준영속 상태의 프록시를 초기화하면 LazyInitializationException 예외가 발생합니다.

 

이렇게 @Transactional를 테스트 코드에만 작성하여, 테스트를 성공하게 되더라도 실제 서비스 로직에 작성하지 않는 경우가 있을 수 있기 때문에 이러한 테스트 코드는 테스트를 위해 작성되었다고 하기 어렵습니다.

 

@Transactional을 사용하지 않고 테스트하는 방법이 있을까?

서비스 계층에서는 Fetch Join을 통해 데이터를 가져오도록 하면, 테스트 코드가 제대로 된 검증 역할을 수행합니다.

Fetch Join으로 가져오게 되면, Lazy loading이 필요하지 않으므로 테스트 코드가 성공합니다.

 

Fetch Join 적용

 

 

테스트 코드에 @Transactional 제거

 

 

테스트 코드 통과

 

실제 서버 실행 후 요청 성공

 

 


참고

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html

 

 

 

Transactional (Spring Framework 6.1.1 API)

Describes a transaction attribute on an individual method or on a class. When this annotation is declared at the class level, it applies as a default to all methods of the declaring class and its subclasses. Note that it does not apply to ancestor classes

docs.spring.io

 

 

Transactional (Spring Framework 6.1.1 API)

Describes a transaction attribute on an individual method or on a class. When this annotation is declared at the class level, it applies as a default to all methods of the declaring class and its subclasses. Note that it does not apply to ancestor classes

docs.spring.io

https://bcp0109.tistory.com/322

 

DB Transaction 의 특징과 Spring Boot @Transactional 옵션

Overview Spring 에서 @Transactional 을 사용할 때 지정할 수 있는 옵션들을 알아봅니다. isolation propagation readOnly rollbackFor timeout 1. isolation 데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기

bcp0109.tistory.com

https://javabom.tistory.com/103

 

JPA 사용시 테스트 코드에서 @Transactional 주의하기

서비스 레이어(@Service)에 대해 테스트를 한다면 보통 DB와 관련된 테스트 코드를 작성하게 된다. 이러면 테스트 메서드 내부에서 사용했던 데이터들이 그대로 남아있게 되어서 실제 서비스에 영

javabom.tistory.com