🌱 트랜잭션이란?
트랜잭션은 데이터베이스의 상태를 변환시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위 또는 한꺼번에 수행되어야 할 일련의 연산을 의미하며, 다음과 같은 4가지 특징(ACID)을 갖는다.
- 원자성 (Atomicity)
- 트랜잭션의 연산은 데이터베이스에 모두 반영되던지 아니면 모두 반영되지 않아야 한다.
- 트랜잭션 내의 모든 명령은 반드시 완벽히 수행되어야 하며, 하나라도 오류가 발생하면 트랜잭션 전부가 취소되어야 한다. - 일관성 (Consistency)
- 트랜잭션 처리 전과 처리 후 데이터 모순이 없는 상태를 유지하는 것을 의미한다.
- 예를 들어서 티스토리 게시판에 글을 쓰는데 제목의 글자 제한이 255 자라고 하자. 트랜잭션이 일어나면 이러한 조건을 만족해야 하는 것이다. 만약 이를 위반하는 트랜잭션이 있다면 거부해야 한다. - 독립성 (Isolation)
- 둘 이상의 트랜잭션이 동시에 실행되는 경우 다른 트랜잭션의 연산이 끼어들 수 없다.
- 수행 중인 트랜잭션은 완전히 완료될 때까지 다른 트랜잭션에서 수행 결과를 참조할 수 없다. - 지속성 (Durability)
- 성공적으로 완료된 트랜잭션의 결과는 영구적으로 반영되어야 한다.
🌱 @Transactional
@Transactional 어노테이션은 스프링에서 많이 사용되는 선언적 트랜잭션 방식이다.
클래스 또는 메서드 위에 @Transactional을 붙이면 트랜잭션 기능이 적용된 프록시 객체가 생성되며, 트랜잭션 성공 여부에 따라 Commit 또는 Rollback 작업이 이루어진다.
🌱 @Transactional 동작 원리
@Transactional은 Spring AOP를 통해 프록시 객체를 생성하여 사용된다.
스프링에서 Target 객체를 직접 참조하지 않고, 프록시 객체를 사용하는 이유는, Aspect 클래스에서 제공하는 부가 기능을 사용하기 위해서이다. Target 객체를 직접 참조하는 경우, 원하는 위치에서 직접 Aspect 클래스를 호출해야 하기 때문에 유지보수가 어려워진다.
스프링에서 사용하는 프록시 구현체는 JDK Proxy(Dynamic Proxy), CGLib 두 가지가 있다.
- JDK Dynamic Proxy
- Target 클래스가 인터페이스 구현체일 경우 생성되며, 구현 클래스가 아닌 인터페이스를 프록시 객체로 구현해서 코드에 끼워 넣는 방식이다. - CGLib Proxy
- 스프링에서 사용하는 디폴트 프록시 생성방식으로, Target 클래스를 프록시 객체로 생성하여 코드에 끼워 넣는 방식이다.
JDK 방식은 java.lang.Reflection을 이용해서 동적으로 프록시를 생성해 준다. 해당 방식의 단점은 AOP 적용을 위해 반드시 인터페이스를 구현해야 된다는 점, 리플렉션은 private 접근이 가능하다는 점 때문에 스프링 부트에선 기본 방식으로 CGLib 방식을 채택하였다. (스프링 레거시는 JDK 기본 동작)
CGLib는 바이트 코드를 조작하여 프록시 객체를 생성한다. 직접 원본 객체를 호출하지 않고 MethodInterceptor와 같은 프록시와 원본 객체 사이에 인터셉터를 두어 메서드 호출을 조작할 수 있도록 도와준다.
- Target에 대한 호출이 오면, AOP 프록시가 인터셉터 체인을 통해 가로채온 후 Transaction Advisor에게 전달한다.
- Transaction Advisor는 트랜잭션을 생성한다.
- Custom Advisor가 있다면, 실행한 후 비즈니스 로직을 호출한다.
- Transaction Advisor는 커밋 또는 롤백 등의 트랜잭션 결과를 반환한다.
코드 레벨로 보자면 아래와 유사한 작업이 이루어진다.
public class TransactionProxy {
private final TransactonManager manager = TransactionManager.getInstance();
public void transactionLogic() {
try {
// 트랜잭션 전처리(트랜잭션 시작, autoCommit(false) 등)
manager.begin();
// 다음 처리 로직(타겟 비스니스 로직, 다른 부가 기능 처리 등)
target.logic();
// 트랜잭션 후처리(트랜잭션 커밋 등)
manager.commit();
} catch ( Exception e ) {
// 트랜잭션 오류 발생 시 롤백
manager.rollback();
}
}
}
🌱 Transaction Options - 격리 수준 (Isolation Level)
트랜잭션에서 일관성 없는 데이터 허용 수준을 설정한다.
@Transactional(isolation = Isolation.XXX)
public void save(...) {
// do something
}
- DEFAULT
- 데이터베이스에서 설정된 기본 격리 수준을 따른다. - READ_UNCOMMITED
- 트랜잭션이 아직 커밋되지 않은 데이터를 읽을 수 있다.
- 세 가지 동시성 부작용(Dirty read, Nonrepeatable read, Phantom read)이 모두 발생한다. - READ_COMMITED
- Dirty Read를 방지하기 위해 Commit 된 데이터만 읽을 수 있다.
- 나머지 부작용(Nonrepeatable read, Phantom read)은 여전히 발생할 수 있다. - REPEATABLE READ
- 트랜잭션이 완료될 때까지 조회한 모든 데이터에 shared lock이 걸리므로 트랜잭션이 종료될 때까지 다른 트랜잭션은 그 영역에 해당하는 데이터를 수정할 수 없다.
- Phantom read 부작용은 여전히 발생한다. - SERIALIZABLE
- 가장 엄격한 트랜잭션 격리 수준으로, 완벽한 읽기 일관성 모드를 제공한다.
- 이 격리 수준에서는 PHANTOM READ 상태가 발생하지 않지만 동시성 처리 성능이 급격히 떨어질 수 있다.
📚 Dirty read
- 동시 트랜잭션의 커밋되지 않은 변경 내용을 조회하는 상황 (데이터 불일치)
📚 Nonrepeatable read
- 동시 트랜잭션이 동일한 행을 업데이트하고 커밋하는 경우, 행을 다시 조회할 때 다른 값을 얻는 상황
📚 Phantom read
- 다른 트랜잭션이 특정 범위의 행을 추가/제거할 경우, 커밋 전/후 조회 결과가 다른 상황
🌱 Transaction Options - 전파 옵션 (Propagation)
트랜잭션이 동작하는 도중 다른 트랜잭션이 실행될 때, 두 트랜잭션의 관계를 설정한다.
@Transactional(propagation = Propagation.XXX)
public void save(...) {
// do something
}
- REQUIRED (default)
- 이미 시작된 트랜잭션이 있으면 참여하고, 없으면 새로운 트랜잭션을 시작한다. - SUPPORTS
- 이미 시작된 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 처리한다. - MANDATORY
- 이미 시작된 트랜잭션이 있으면 참여하고, 없으면 예외를 발생시킨다.
- 독립적으로 트랜잭션을 진행하면 안 되는 경우 사용한다. - NEVER
- 트랜잭션을 사용하지 않도록 강제한다.
- 이미 진행 중인 트랜잭션 또한 허용하지 않으며, 있다면 예외를 발생시킨다. - NOT_SUPPORTED
- 트랜잭션을 사용하지 않고 처리하도록 합니다. 이미 진행 중인 트랜잭션이 있다면 잠시 보류시킨 후 트랜잭션 없이 진행한다. - REQUIRES_NEW
- 항상 새로운 트랜잭션을 시작한다.
- 이미 진행 중인 트랜잭션이 있다면 잠시 보류시킨다. - NESTED
- 이미 실행 중인 트랜잭션이 있다면 중첩하여 트랜잭션을 진행한다.
- 부모 트랜잭션은 중첩 트랜잭션에 영향을 주지만 중첩 트랜잭션은 부모 트랜잭션에 영향을 주지 않는다.
🌱 Transaction Options - rollbackFor, noRollbackFor
예외발생 시, Rollback이 동작하거나 동작하지 않게 설정한다.
- rollbackFor : 특정 예외가 발생하면 강제로 Rollback 한다.
- noRollbackFor : 특정 예외가 발생하더라도 Rollback 하지 않는다.
선언적 트랜잭션에서는 런타임 예외가 발생하면 롤백을 수행한다. 이는 예외가 발생하지 않거나 체크 예외가 발생하면 커밋을 한다는 의미이다. 단, rollbackFor옵션을 통해 체크 예외가 발생해도 롤백을 수행할 수 있다.
체크 예외인 Exception이 발생했을 때 롤백을 시키고자 한다면, 아래와 같이 작성할 수 있다.
@Transactional(rollbackFor = Exception.class)
public void save(...) {
// do something
}
반대로 롤백이 발생해야 하는 예외임에도 불구하고 롤백을 시키지 않을 수 있다.
@Transactional(noRollbackFor = NullPointerException.class)
public void save(...) {
// do something
}
🌱 Transaction Options - timeout
지정한 시간 내에 메서드 수행이 완료되지 않으면 Rollback 하도록 설정한다.
단위는 초이며, 설정값이 -1인 경우 시간제한을 두지 않는다.
@Transactional(timeout = 10) // (default : -1)
public void save(...) {
// do something
}
References.
1. 용로그 - [Spring] 트랜잭션과 @Transactional 총 정리
2. Aaron - [Spring] @Transactional 잘 사용해보기
3. yourjin.log - [Spring] AOP와 @Transactional의 동작 원리
4. Better.log - [Spring] 📚 @Transactional 이해하기
5. 제육's 휘발성 코딩 - 스프링 트랜잭션 동작 원리 (@Transactional, AOP)
6. 개발개발 공부로그 - 스프링 트랜잭션 @Transactional 개념 (+주요 설정값)
'IT' 카테고리의 다른 글
[SpringBoot] 테스트 개념과 종류 (1) | 2025.01.24 |
---|---|
[Spring] ORM, JPA, Hibernate, Spring Data JPA 개념 (1) | 2025.01.24 |
[Java] Checked Exception과 Unchecked Exception (3) | 2025.01.22 |
[Java] java.time 패키지 (2) | 2025.01.22 |
[Java] Optional (1) | 2025.01.22 |