1. @Transactional
@Transactional 어노테이션은 트랜잭션 처리를 위해서 Spring이 제공하는 어노테이션이다. @Transactional을 메서드 또는 클래스에 명시하여 메서드에 대해서 내부적으로 AOP를 통해 트랜잭션 처리코드가 실행된다.
다음과 같이 BooksImple 클래스의 addBook에 @Transactional을 선언했을 때
public class BooksImpl implements Books {
public void addBooks(List<String> bookNames) {
bookNames.forEach(bookName -> this.addBook(bookName));
}
@Transactional(rollbackFor = Exception.class)
public void addBook(String bookName) {
Book book = new Book(bookName);
bookRepository.save(book);
book.setFlag(true);
}
}
프록시 객체가 생성되며 다음과 같이 실행된다. 트랜잭션 소스 코드를 따로 넣지 않아도 AOP를 통해서 Exception이 발생 했을 경우 rollback을 해주는 소스 코드가 생성된다.
public class BooksProxy {
private final Books books;
private final TransactonManager manager = TransactionManager.getInstance();
public BooksProxy(Books books) {
this.books = books;
}
public void addBook(String bookName) {
try {
manager.begin();
books.addBook(bookName);
manager.commit();
} catch (Exception e) {
manager.rollback();
}
}
}
Spring 의 코드 삽입 방법은 크게 2가지다.
- 바이트 코드 생성 (CGLIB 사용)
- 프록시 객체 사용
2가지 방법중에 Spring은 기본적으로 프록시 객체를 사용하도록 선택 된다. 그렇기 때문에 interface가 반드시 필요하다.
SpringBoot는 기본적으로 바이트 코드 생성이(CGLib) 선택된다. 따라서 interface가 따로 필요 없다.
2. @Transactional 옵션
2.1 isolation
데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질 네가지가 존재한다. (ACID)
- 원자성(Atomicity): 한 트랜잭션 내에서 실행한 작업들은 하나로 간주함 (모두 성공 또는 모두 실패)
- 일관성(Consistency): 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 함
- 격리성(Isolation): 동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않아야 함
- 지속성(Durability): 트랜잭션을 성공적으로 마치면 결과가 항상 저장되어야 함
2.1.1 DEFAULT
사용하는 DB 의 기본 격리 수준을 따름
2.1.2 READ_UNCOMMITTED
한 트랜잭션이 처리 중인 커밋되지 않은 데이터를 다른 트랜잭션에서 접근 가능합니다.
DB 에 커밋하지 않은, 즉 존재하지 않는 데이터를 읽는 현상을 Dirty Read 라고 합니다.
데이터 정합성에 문제가 많아서 웬만하면 권장되지 않고 아예 지원하지 않는 경우도 있습니다.
Dirty Read 가 가능하기 때문에 잘못된 데이터를 읽을 수 있습니다.
- A 트랜잭션이 데이터 1 을 조회하여 2 로 변경하고 아직 커밋하지 않음
- B 트랜잭션이 동일한 데이터를 조회해서 2 라는 값을 받음 (Dirty Read)
- A 트랜잭션에서 오류가 발생해서 데이터를 롤백 (2 -> 1)
- 실제 데이터는 1 이지만 B 트랜잭션은 2 라는 잘못된 데이터를 읽은 셈
2.1.3 READ_COMMITTED
트랜잭션은 커밋한 데이터만 읽을 수 있습니다.
A 트랜잭션이 데이터를 변경해도 커밋하기 전이라면 B 트랜잭션은 변경되기 전의 데이터를 조회할 수 있습니다.
이 때, B 트랜잭션은 Undo 영역에서 데이터를 가져옵니다. (MVCC - Multi Version Concurrency Control 참조)
매 조회 시마다 새로운 스냅샷을 뜨기 때문에 다른 트랜잭션이 커밋한 후 다시 조회하면 변경된 데이터를 볼 수 있습니다.
대부분의 DB 기본 격리 수준이며 REPEATABLE_READ 와 함께 가장 많이 사용되는 방식입니다.
Non-Repeatable Read 현상이 발생할 수 있습니다.
트랜잭션에서 조회한 데이터가 트랜잭션이 끝나기 전에 다른 트랜잭션에 의해 변경되면 다시 읽었을 때 새로운 값이 읽히며 데이터 불일치하는 현상을 말합니다.
하나의 트랜잭션 내에서 똑같은 SELECT 쿼리를 실행했을 때 항상 같은 결과를 가져와야 한다는 REPEATABLE READ 정합성 정의에 어긋납니다.
- A 트랜잭션이 데이터 (row) 를 읽음
- B 트랜잭션이 같은 데이터를 수정하고 커밋
- A 트랜잭션이 다시 같은 데이터를 읽었는데 데이터가 달라짐
2.1.4 REPEATABLE_READ
간단히 말하면 하나의 트랜잭션은 하나의 스냅샷만 사용하는 겁니다.
A 트랜잭션이 시작하고 처음 조회한 데이터의 스냅샷을 저장하고 이후에 동일한 쿼리를 호출하면 스냅샷에서 데이터를 가져옵니다.
따라서 중간에 B 트랜잭션이 새로 커밋해도 A 트랜잭션이 조회하는 데이터는 변하지 않습니다.
Phantom Read 라는 다른 트랜잭션에서 수행한 작업에 의해 안보였던 데이터가 보이는 현상이 발생할 수 있습니다.
REPEATABLE_READ 격리 수준은 조회한 데이터에 대해서만 Shared Lock 이 걸리기 때문에 다른 트랜잭션이 새로운 데이터를 추가할 수 있습니다.
- A 트랜잭션이 조회한 데이터는 0 건
- B 트랜잭션이 새로운 데이터를 추가하고 커밋
- A 트랜잭션이 같은 쿼리로 다시 조회했더니 B 트랜잭션이 추가한 데이터까지 같이 조회됨
2.1.5 SERIALIZABLE
가장 단순하고 엄격한 격리 수준입니다.
이름 그대로 순차적으로 트랜잭션을 진행시키며 읽기 작업에도 잠금을 걸어 여러 트랜잭션이 동시에 같은 데이터에 접근하지 못합니다.
가장 안전하지만 성능 저하가 발생하기 때문에 극도의 안정성을 필요로 하지 않으면 자주 사용되지 않습니다.
2.2 propagation
현재 진행중인 트랜잭션 (부모 트랜잭션) 이 존재할 때 새로운 트랜잭션 메소드를 호출하는 경우 어떤 정책을 사용할 지에 대한 정의입니다.
예를 들어, 기존 트랜잭션에 참여해서 그대로 이어갈 수도 있고, 새로운 트랜잭션을 생성할 수도 있으며 non-transactional 상태로 실행할 수도 있습니다.
처음에 non-transactional 상태로 실행한다라는 개념에 대해 착각을 했었는데 트랜잭션은 존재하지만 커밋, 롤백이 되지 않는 상태입니다.
그래서 NOT_SUPPORTED 같은 트랜잭션은 TransactionSynchronizationManager.getCurrentTransactionName() 메소드로 조회했을 때 이름이 존재하지만 JPA Dirty Checking 은 동작하지 않습니다.
Spring 의 @Transactional 에서는 다음과 같은 propagation 옵션을 제공합니다.
- REQUIRED: 기본값이며 부모 트랜잭션이 존재할 경우 참여하고 없는 경우 새 트랜잭션을 시작
- SUPPORTS: 부모 트랜잭션이 존재할 경우 참여하고 없는 경우 non-transactional 상태로 실행
- MANDATORY: 부모 트랜잭션이 있으면 참여하고 없으면 예외 발생
- REQUIRES_NEW: 부모 트랜잭션을 무시하고 무조건 새로운 트랜잭션이 생성
- NOT_SUPPORTED: non-transactional 상태로 실행하며 부모 트랜잭션이 존재하는 경우 일시 정지시킴
- NEVER: non-transactional 상태로 실행하며 부모 트랜잭션이 존재하는 경우 예외 발생
- NESTED:
- 부모 트랜잭션과는 별개의 중첩된 트랜잭션을 만듬
- 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만 자신의 커밋과 롤백은 부모 트랜잭션에게 영향을 주지 않음
- 부모 트랜잭션이 없는 경우 새로운 트랜잭션을 만듬 (REQUIRED 와 동일)
- DB 가 SAVEPOINT 를 지원해야 사용 가능 (Oracle)
- JpaTransactionManager 에서는 지원하지 않음
2.3 readOnly
- 기본값: false
- 사용법: @Transactional(readOnly = true)
기본값은 false 이며 true 로 세팅하는 경우 트랜잭션을 읽기 전용으로 변경합니다.
만약 읽기 전용 트랜잭션 내에서 INSERT, UPDATE, DELETE 작업을 해도 반영이 되지 않거나 DB 종류에 따라서 아예 예외가 발생하는 경우도 있습니다. 성능 향상을 위해 사용하거나 읽기 외의 다른 동작을 방지하기 위해 사용하기도 합니다.
JPA 에는 Dirty Checking 이라는 기능이 있습니다.
개발자가 임의로 UPDATE 쿼리를 사용하지 않아도 트랜잭션 커밋 시에 1차 캐시에 저장되어 있는 Entity 와 스냅샷을 비교해서 변경된 부분이 있으면 UPDATE 쿼리를 날려주는 기능입니다.
하지만 readOnly = true 옵션을 주면 스프링 프레임워크가 하이버네이트의 FlushMode 를 MANUAL 로 설정해서 Dirty Checking 에 필요한 스냅샷 비교 등을 생략하기 때문에 성능이 향상됩니다.
2.4 rollbackFor
- 기본값: RuntimeException, Error
- 사용법: @Transactional(rollbackFor = {IOException.class, ClassNotFoundException.class})
사용할 때 @Transactional(rollbackFor = IOException.class) 처럼 Exception 을 하나만 지정한다면 중괄호를 생략할 수 있습니다.- 기본적으로 트랜잭션은 종료 시 변경된 데이터를 커밋합니다.
하지만 @Transactional 에서 rollbackFor 속성을 지정하면 특정 Exception 발생 시 데이터를 커밋하지 않고 롤백하도록 변경할 수 있습니다. 기본값은 {} 라고 나와있지만 사실 RuntimeException 과 Error 가 세팅되어 있습니다.
내부 로직으로 들어가 설명을 보면 둘 다 예측 불가능한 예외 상황이기 때문에 기본값으로 들어가 있다고 합니다.
중요한 점은 이 값은 그냥 기본값이 아니라 아예 지정된 값이기 때문에 rollbackFor 속성으로 다른 Exception 을 추가해도 RuntimeException 이나 Error 는 여전히 데이터를 롤백합니다. 만약 강제로 데이터 롤백을 막고 싶다면 noRollbackFor 옵션으로 지정해주면 됩니다.
2.5 timeout
- 기본값: -1
- 사용법: @Transactional(timeout = 2)
지정한 시간 내에 해당 메소드 수행이 완료되이 않은 경우 JpaSystemException 을 발생시킵니다.
JpaSystemException 은 RuntimeException 을 상속받기 때문에 데이터 역시 롤백 처리 됩니다.
초 단위로 지정할 수 있으며 기본값인 -1 인 경우엔 timeout 을 지원하지 않습니다.
3. 트랜잭션 사용 시 주의사항(1)
컨트롤러에서 트랜잭션 어노테이션을 선언하고 있는 service1, service2의 로직을 호출하고 있다고 생각해보자. 만약에 여기서 service1에서는 정상적으로 저장이되고, service2에서 런타임 오류가 발생했다면 어떻게 될까??
@RestController
@RequiredArgsConstructor
@RequestMapping("/sample")
class SampleController {
private final Service1 service1;
private final Service2 service2;
@PostMapping("/test")
public String transaction() {
service1.save("A"))
service2.save("B") // 여기에서 exception 발생시 위 save는 Rollback 되지 않는다.
return "transactionTest";
}
}
정답은 service1에서 저장한 내용은 DB에 반영이되고 service2에서는 런타임 오류가 발생했기 때문에 롤백이 된다. 즉 하지만 개발자는 service1도 롤백이 됐을꺼라고 착각할 수 있다. 1번절에서 작성한 프록시 객체를 보면 각각의 트랜잭션 별로 commit과 rollback이 될꺼라는 것을 알 수 있다.
둘중 하나라도 에러가 났을 때 롤백을 하고 싶다면 2개의 트랜잭션을 서비스 안에 함께 묶어서 실행하면 된다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/sample")
class SampleController {
private final Service3 service3;
@PostMapping("/test")
public String transaction() {
service3.save("A", "B"))
return "transactionTest";
}
}
4. 트랜잭션 사용 시 주의사항(2)
JPA를 사용할 때 변경 감지 기능을 많이 사용한다. 변경 감지 기능은 영속성 컨텍스트에서 관리되는 객체가 트랜잭션 안에서 값이 변경된다면 commit 시점에 자동으로 update를 해준다.
//아래 클래스와 같이
public class BooksImpl implements Books {
public void addBooks(List<String> bookNames) {
bookNames.forEach(bookName -> this.addBook(bookName));
}
@Transactional
public void addBook(String bookName) {
Book book = new Book(bookName);
bookRepository.save(book);
book.setFlag(true);
}
}
REFERENCE
https://bcp0109.tistory.com/322
https://velog.io/@kdhyo/JavaTransactional-Annotation-%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90-26her30h
'Spring Boot' 카테고리의 다른 글
[Spring Boot] Elastic Stack 연동 (ELK 연동) (0) | 2021.11.28 |
---|---|
[Spring Boot] API 스프링부트 캐싱 적용 (0) | 2021.11.21 |
[Spring Boot] Resource 추상화 (0) | 2020.08.27 |
[Spring Boot] @Valid를 이용한 유효성 검사 (0) | 2020.08.09 |
[Spring Boot] Swagger-UI 적용 (0) | 2020.05.31 |