트랜잭션 (Transaction)
트랜잭션은 더 이상 쪼개질 수 없는 하나의 작업 단위를 말한다. 가장 예시를 많이 드는 것은 '계좌이체' 이다. 이 행위는 '출금'과 '입금'이라는 각 각의 작업이 하나의 단위를 이루고 있다. 출금은 정상적으로 처리되었는데, 입금하는 과정에서 예외가 발생하는 경우를 생각해 볼 수 있다. 이미 계좌에서 돈이 빠져나갔는데, 상대방의 계좌에 돈이 입금되지 않는다면 큰 문제가 될 것이다. 이 때문에 출금과 입금은 하나의 트랜잭션으로 관리하여 문제가 발생한 경우 모든 작업을 rollback 하는 것이 필요하다. 이처럼 여러 작업을 진행하다가 문제가 생기면 모든 작업을 이전 상태로 rollback 하기 위해 사용되는 것이 트랜잭션이다.
ACID 원칙
1) Atomicity (원자성)
하나의 트랜잭션은 모두 하나의 단위로 처리되어야한다. 예를 들어, 어떤 트랜잭션이 A와 B로 구성된다면, A와 B의 처리 결과는 항상 동일해야 한다.
2) Consistency (일관성)
트랜잭션이 성공했다면, 데이터베이스의 모든 데이터는 일관성을 유지해야만 한다. 트랜잭션으로 처리된 데이터와 일반 데이터 사이에는 전혀 차이가 없어야 한다.
3) Isolation (격리)
트랜잭션으로 처리되는 중간에 외부에서의 간섭은 없어야 한다.
4) Durability (영속성)
트랜잭션이 성공적으로 처리되면 그 결과는 영속적으로 보관되어야 한다.
@Transactional 어노테이션
`@Transactional` 은 스프링에서 트랜잭션 범위를 지정하는 어노테이션이다. 이 어노테이션이 붙은 메서드는 성공하면 모두 커밋, 실패하면 전부 롤백이 된다. 코드 바깥에서 선언적으로 트랜잭션을 켜고 속성을 주는 방식이라 선언적 트랜잭션이라고 한다.
@Transactional
public void transfer(Long fromId, Long toId, int amount) {
accountRepo.debit(fromId, amount); // 출금
accountRepo.credit(toId, amount); // 입금 중 예외 → 전체 롤백
}
Service 계층 메서드에 붙여 유스케이스 단위로 경계를 잡는 것이 일반적이다. Repository는 데이터 접근만 담당하고, Controller에는 붙이지 않는다.
구조
각 속성에 대한 내용을 살펴보기 전에 일단 `@Transactional` 의 작동 구조에 대해서 살펴보자.
스프링은 `@Transactional` 이 붙은 메서드 호출을 프록시가 가로채서 트랜잭션을 시작, 합류, 커밋, 롤백한다.
Controller/Caller
↓ (메서드 호출)
[Transaction Proxy] ── 트랜잭션 시작/합류 결정, 예외에 따라 커밋/롤백
↓
[실제 Service 메서드] ── 비즈니스 로직 실행(JPA flush 등)
동작 흐름
1. 프록시가 호출을 먼저 받는다. `@Transactional` 속성을 확인한다.
2. PlatformTransactionManager에 트랜잭션 시작을 요청한다.
3. 실제 메서드를 실행한다.
4. 정상 종료면 커밋을 하게 되고, 예외면 롤백한다.
5. 자원 정리한다.
6. 호출자에게 결과를 반환한다.
사용법
`@Transactional`을 사용할 때 아래와 같이 속성값을 넣어줄 수 있다.
1) propagation
비즈니스 로직에서의 트랜잭션 범위를 정의한다. 트랜잭션의 전파를 설정하는 부분이다.
- 기본값 : `Propagation.REQUIRED`
- 이미 트랜잭션이 있으면 그 안으로 합류, 없으면 새로 시작
- 대부분 일반적인 서비스 메서드에 적합하다.
- 자주 사용하는 값 : `Propagtion.REQUIRES_NEW`
- 항상 새 트랜잭션을 시작한다.
@Service
public class OrderService {
@Transactional // REQUIRED (기본) : 주문 저장과 결제 승인 하나의 트랜잭션
public void placeOrder(OrderCommand cmd) {
orderRepo.save(cmd.toEntity());
paymentService.approve(cmd.paymentInfo());
auditService.writeAudit(cmd);
}
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeAudit(OrderCommand cmd) {
auditRepo.save(AuditLog.of(cmd));
// 여기서 실패해도 주문(placeOrder)의 롤백과는 분리됨
}
}
2) isolation
트랜잭션의 격리 수준을 정의한다. 동시에 여러 개의 트랜잭션에 의한 변경사항을 어떻게 적용할지에 대한 설정이다. 여러 트랜잭션이 동시에 같은 데이터를 만질 때 생기는 문제를 얼마나 차단할지 정한다.
- 기본값 : `Isolation.DEFAULT` (DB의 기본값을 따른다.)
- 자주 쓰는 값 : `READ_COMMITTED` (커밋된 데이터만 읽음), `REPEATABLE_READ` (같은 행을 다시 읽어도 값이 바뀌지 않음), `SERIALIZABLE` (가장 엄격하지만 성능 비용이 큼)
3) @Transactional(readOnly=true)
'이 메서드는 읽기 전용' 이라는 의도를 JPA에 알려 불필요한 쓰기와 플러시를 줄인다.
- 장점 : 조회 성능에 유리하고 코드 의도가 명확하다.
- 주의 : DB가 쓰기 자체를 물리적으로 막아주지는 않는다. 이 메서드에서는 실제로 쓰기 호출을 하지 않는 것이 원칙이다.
@Transactional(readOnly = true)
public UserView getUser(Long id) {
return userQueryRepository.findViewById(id);
}
4) rollbackFor / noRollbackFor : 롤백 규칙
원래 기본 규칙은
- 런타임 예외(RuntimeException)/Error → 롤백
- 체크 예외(Checked) → 롤백 안함
그러나 필요하면 아래처럼 규칙을 바꿀 수도 있다.
// 체크 예외도 롤백하고 싶을 때
@Transactional(rollbackFor = Exception.class) // 또는 (rollbackFor = IOException.class)
public void importFile(Path path) throws IOException { ... }
// 특정 예외는 롤백하지 않고 커밋하고 싶을 때
@Transactional(noRollbackFor = IllegalArgumentException.class)
public void process(String input) { ... }
5) timeout : 제한 시간
지정 시간 내 끝내지 않으면 롤백한다.
@Transactional(timeout = 3) // 3초 초과 시 롤백
public void slowTask(...) { ... }
6) transactionManager : 트랜잭션 매니저 지정
트랜잭션을 실제로 시작, 커밋, 롤백해주는 실행자는 `TransactionManager` 이다.
- 단일 데이터 소스 + 스프링 부트 : 보통 아무것도 하지 않아도된다. (JPA를 쓰면 `JpaTransactionManager` , JDBC만 쓰면 `DataSourceTransationManager` 를 자동 설정해준다.
- 멀티 데이터 소스/ 멀티 DB : 어느 매니저를 쓸지 정해야한다.
'Spring' 카테고리의 다른 글
| [Spring] 연관관계 (0) | 2025.08.29 |
|---|---|
| [Spring] PSA (Portable Service Abstraction) (0) | 2025.08.27 |
| [Spring] 프록시(Proxy) (0) | 2025.08.13 |
| [Spring] AOP(Aspect Oriented Programming) (0) | 2025.08.11 |
| [Spring] 영속성 컨텍스트(Persistence Context) (3) | 2025.08.11 |