| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |
- Eclipse
- 문자열
- vscode
- Visual Studio Code
- CSS
- 자바스크립트
- list
- ArrayList
- 배열
- HashMap
- math
- javascript
- SpringBoot
- js
- IntelliJ
- junit
- 인텔리제이
- junit5
- 스프링부트
- input
- java테스트
- 자바문법
- string
- html
- Java
- 테스트자동화
- 단위테스트
- 정규식
- Array
- 자바
- Today
- Total
어제 오늘 내일
[Spring Boot] "내 돈 돌려줘!" 트랜잭션(@Transactional)의 동작 원리와 롤백 정책 완벽 정리 본문
[Spring Boot] "내 돈 돌려줘!" 트랜잭션(@Transactional)의 동작 원리와 롤백 정책 완벽 정리
hi.anna 2026. 3. 14. 01:51
DB를 다루는 애플리케이션에서 가장 중요한 것은 속도가 아니라 신뢰성입니다.
A가 B에게 100만 원을 송금하는 로직을 짰다고 가정해 봅시다.
- A의 계좌에서 100만 원 차감 (UPDATE)
- (여기서 서버 전원 꺼짐 💥)
- B의 계좌에 100만 원 입금 (UPDATE)
트랜잭션이 없다면 100만 원은 공중분해 됩니다. 이런 일을 막기 위해 "모두 성공하거나, 아니면 아예 없던 일로 하거나(All or Nothing)"를 보장해 주는 것이 바로 트랜잭션입니다.
1. 스프링은 어떻게 트랜잭션을 거나요? (AOP와 프록시)
스프링에서 트랜잭션을 적용하는 방법은 너무나 간단합니다. 메서드나 클래스 위에 @Transactional만 붙이면 끝이죠.
@Service
public class TransferService {
@Transactional // 이 메서드 시작할 때 트랜잭션 시작!
public void transfer(String fromId, String toId, int amount) {
memberRepository.decrease(fromId, amount);
memberRepository.increase(toId, amount);
} // 메서드 무사히 끝나면 커밋!
}
하지만 이 어노테이션 뒤에는 프록시(Proxy)라는 거대한 매커니즘이 숨어있습니다.
- 스프링은
TransferService를 감싸는 가짜 객체(Proxy)를 만듭니다. - 클라이언트가
transfer()를 호출하면, 사실은 프록시가 먼저 가로챕니다. - 프록시: "DB 커넥션 가져와! 트랜잭션 시작해!" (
setAutoCommit(false)) - 진짜 객체: 비즈니스 로직 실행 (
decrease,increase) - 프록시: "에러 없네? 커밋해!" (
commit) 혹은 "에러 났네? 롤백해!" (rollback)
2. 언제 롤백(Rollback) 되나요? ★면접 단골 질문★
개발자들이 가장 많이 실수하는 부분입니다. "에러가 났는데 롤백이 안 돼요!"
스프링의 기본 정책은 다음과 같습니다.
| 예외 종류 | 롤백 여부 | 예시 |
| 언체크 예외 (Unchecked Exception) | O (롤백 함) | RuntimeException, NullPointerException 등 |
| 에러 (Error) | O (롤백 함) | OutOfMemoryError 등 |
| 체크 예외 (Checked Exception) | X (롤백 안 함) | IOException, SQLException 등 |
왜 체크 예외는 롤백을 안 하나요?
스프링은 체크 예외를 "비즈니스적으로 의미 있는 예외(복구 가능한 예외)"로 간주합니다.
예를 들어 "잔액 부족" 같은 상황은 롤백보다는
"사용자에게 알림"을 보내는 흐름으로 처리하길 기대하는 것이죠.
"체크 예외도 롤백하고 싶어요!"
그럴 때는 옵션을 명시해주면 됩니다.
@Transactional(rollbackFor = Exception.class) // 모든 예외에 대해 롤백!
public void transfer(...) throws IOException { ... }
3. 성능을 위한 꿀팁: readOnly = true
데이터를 수정하지 않고 조회만 하는 메서드라면, 꼭 읽기 전용 모드를 켜주세요.
@Transactional(readOnly = true)
public List<Member> findAll() { ... }
이점:
- JPA 성능 최적화: 영속성 컨텍스트가 스냅샷을 만들지 않습니다. (변경 감지를 안 하니까 메모리 절약!)
- DB 부하 감소: 읽기 전용 트랜잭션에 최적화된 모드로 동작합니다. (Master-Slave 구조에서 Slave DB를 바라보게 할 수도 있음)
4. 주의사항: 트랜잭션이 적용되지 않는 경우 (Self-Invocation)
같은 클래스 안에 있는 메서드끼리 호출할 때는 트랜잭션이 동작하지 않습니다.
public class MyService {
public void externalCall() {
internalCall(); // 여기서 호출하면 트랜잭션 없음!
}
@Transactional
public void internalCall() {
// ...
}
}
이유: 프록시는 외부에서 들어오는 호출만 가로챌 수 있습니다. this.internalCall()은 프록시를 거치지 않고 직접 호출하기 때문에 어노테이션이 무시됩니다.
해결책: 메서드를 다른 클래스(Service)로 분리해서 호출하세요.
마치며
오늘의 결론입니다.
@Transactional은 프록시 기술을 통해 "시작-실행-종료(커밋/롤백)"을 관리한다.- 기본적으로
RuntimeException만 롤백된다. (체크 예외는rollbackFor옵션 필요) - 조회용 메서드에는
readOnly = true를 붙여 성능을 높이자. - 같은 클래스 내부 호출은 트랜잭션이 안 먹히니 주의하자.
이것으로 Spring Boot의 핵심 개념들은 탄탄하게 잡히셨을 겁니다!
다음 포스팅에서는 "테스트 코드가 없는 코드는 쓰레기다!" JUnit5와 Mockito를 활용한 단위 테스트(Unit Test) 작성법에 대해 알아보겠습니다.
