공공부하자개발 · 영어 학습 노트
자바
실무 확장Excel · 파일 업로드 · DB 연동0/22 완료
  • 01Excel(XLSX) 구조와 순수 JDK로 읽기/쓰기
  • 02Apache POI로 Excel 업로드/다운로드
  • 03파일 업로드/다운로드 서버 (HttpServer)
  • 04JDBC 기초와 트랜잭션 (H2)
  • 05MyBatis 어노테이션 매퍼로 쿼리 연동
  • 06MyBatis XML 매퍼 · Oracle 방언 · PageHelper · Spring Boot
  • 07REST API 서버와 JSON
  • 08Vue 3 SPA 와 Java 서버 연동
  • 09@Scheduled 운영
  • 10로깅 실무: 레벨·계층, MDC 추적, 예외·성능, 마스킹, 롤링, JSON 로그
  • 11외부 API 연동
  • 12테스트 실무
  • 13암호화·개인정보 보호
  • 14인코딩·한글 실무
  • 15@Transactional 심화
  • 16긴 작업 비동기 처리와 진행률
  • 17SFTP·FTP 파일 연계
  • 18로컬 캐시와 @Cacheable
  • 19메일·알림 발송
  • 20웹 보안 체크리스트
  • 21빌드 도구와 폐쇄망 의존성 반입
  • 22성능 측정: p50·p95·p99, 측정 계층, JFR, JMH 함정, 자체 부하 테스트, 병목 순위
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 확장 › 15 / 22

@Transactional 심화

전파·격리·readOnly·롤백 규칙·self-invocation·REQUIRES_NEW·락
섹션 7진행 0 / 22
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

2. 핵심 원리

2.1 트랜잭션 경계는 프록시가 만든다

Spring 의 @Transactional 은 마법이 아니라 프록시입니다. 빈을 감싸는 프록시가 메서드 호출 앞뒤로 트랜잭션 시작·커밋·롤백 코드를 끼워 넣습니다. 원리는 고급 과정의 리플렉션·프록시 레슨에서 다룬 JDK 동적 프록시와 같습니다.

즉 @Transactional 이 실제로 동작하려면 그 메서드 호출이 반드시 프록시를 거쳐야 합니다. 프록시를 거치지 않은 호출에는 트랜잭션이 붙지 않습니다. 이 사실 하나가 2.6 절의 self-invocation 함정을 만듭니다.

2.2 전파 속성 7가지, 실무는 둘만 쓴다

Spring 은 전파 속성을 7가지 제공하지만, 정산·이체 업무 코드에서 실제로 보는 것은 REQUIRED 와 REQUIRES_NEW 둘뿐입니다.

전파 속성 동작
REQUIRED 있으면 참여, 없으면 새로 시작(기본값)
REQUIRES_NEW 항상 새 트랜잭션, 기존은 잠시 멈춤
NESTED 세이브포인트로 부분 롤백 가능
SUPPORTS 있으면 참여, 없으면 트랜잭션 없이 실행
NOT_SUPPORTED 기존을 멈추고 트랜잭션 없이 실행
MANDATORY 기존 트랜잭션이 반드시 필요, 없으면 예외
NEVER 트랜잭션이 있으면 예외

나머지 다섯은 드라이버·DBA 정책에 따라 세이브포인트 지원이 갈리거나(NESTED), 실무에서 의도를 설명하기 어려워서(SUPPORTS, NOT_SUPPORTED) 잘 안 씁니다. "실패해도 반드시 남겨야 하는 로그"만 REQUIRES_NEW 로 분리하고 나머지는 REQUIRED 로 충분합니다.

2.3 격리 수준 4단계와 Oracle 기본값

격리 수준 막는 문제
READ_UNCOMMITTED 없음
READ_COMMITTED 더티 리드
REPEATABLE_READ 더티 리드, 반복 읽기 불일치
SERIALIZABLE 더티 리드, 반복 읽기, 팬텀 리드

Oracle 은 기본 격리 수준이 READ COMMITTED 입니다. 커밋되지 않은 값을 읽는 더티 리드는 없지만, 같은 트랜잭션 안에서 같은 행을 두 번 읽으면 다른 값이 나올 수 있고(반복 읽기 불일치), 조건에 맞는 행 개수가 달라질 수도 있습니다(팬텀 리드). 정산 배치처럼 같은 데이터를 여러 번 읽어 합산하는 로직에서는 이 특성을 염두에 둬야 합니다.

2.4 readOnly=true, 하는 일과 안 하는 일

@Transactional(readOnly = true) 는 성능 최적화 힌트이지 DB 가 강제하는 잠금이 아닙니다. MyBatis 는 이 힌트를 보고 커밋 호출을 생략할 수 있지만, Oracle 은 readOnly 플래그만으로 쓰기 SQL 실행 자체를 막아 주지 않습니다.

읽기 전용 조회 메서드에 붙여 두면 실수로 그 안에서 UPDATE 를 실행해도 여전히 실행됩니다. 다만 트랜잭션 매니저·드라이버 조합에 따라 예외가 날 수도 있으니, "안전장치"가 아니라 "의도 표시 + 약간의 성능 이득"으로 이해해야 합니다.

2.5 롤백 규칙: unchecked 만 롤백된다

Spring 의 기본 롤백 규칙은 RuntimeException 과 Error 만 롤백하고, checked Exception 은 커밋합니다. 이 규칙을 모르고 checked 예외로 실패를 표현하면 트랜잭션이 조용히 커밋되어 데이터가 반쯤 반영된 채 남습니다.

checked 예외도 롤백하려면 rollbackFor = Exception.class 를 명시해야 합니다. 이 레슨의 AccountService.transferLenient 와 transferStrict 가 이 차이를 그대로 재현합니다.

2.6 self-invocation 함정

같은 클래스 안에서 this.메서드() 로 다른 트랜잭션 메서드를 호출하면 프록시를 거치지 않습니다. 2.1 절에서 본 것처럼 트랜잭션은 프록시가 만들기 때문에, 이 호출에는 @Transactional 이 아예 적용되지 않습니다.

흔한 회피 방법은 세 가지입니다. 첫째, 자기 자신의 프록시를 필드로 주입받아 그 필드로 호출합니다(이 레슨의 방식). 둘째, AopContext.currentProxy() 로 현재 프록시를 꺼냅니다. 셋째, 그 메서드를 아예 다른 빈으로 분리합니다.

같은 이유로 private·final 메서드도 트랜잭션이 적용되지 않습니다. 인터페이스 기반 프록시는 인터페이스에 없는 메서드를 아예 노출하지 않고, 클래스 기반(CGLIB) 프록시도 final 메서드는 오버라이드할 수 없어 그대로 원본이 호출됩니다.

2.7 내부 예외를 catch 로 삼키면

REQUIRED 로 참여한 내부 메서드가 롤백 대상 예외를 던지면, 그 트랜잭션은 즉시 롤백되지 않고 "rollback-only" 로 표시만 됩니다. 커넥션을 공유하고 있어서 바깥 트랜잭션이 아직 할 일이 남았을 수도 있기 때문입니다.

문제는 바깥 메서드가 이 예외를 catch 로 삼키고 정상 종료해 버리는 경우입니다. 바깥은 정상 흐름이니 커밋하려 하지만, 트랜잭션이 이미 rollback-only 로 표시돼 있어서 커밋할 수 없습니다. 이때 Spring 은 UnexpectedRollbackException 을 던집니다. "예외를 잡았으니 안전하다"는 생각이 오히려 새로운 예외를 만드는 셈입니다.

2.8 트랜잭션 안에서 하면 안 되는 일

트랜잭션은 커넥션 하나를 붙잡고 있는 구간입니다. 이 구간이 길어질수록 커넥션 풀의 다른 요청이 커넥션을 못 받아 대기합니다.

외부 API 호출, 파일 업로드·다운로드, 긴 루프 연산을 트랜잭션 메서드 안에 넣으면 그 시간만큼 커넥션을 점유합니다. 외부 API 가 느려지면 커넥션 풀 전체가 고갈되고, 트랜잭션과 무관한 다른 화면까지 응답이 멈춥니다. 외부 호출은 트랜잭션 밖으로 빼거나, 결과를 먼저 받아 온 뒤 짧은 트랜잭션으로 저장만 하는 순서로 바꿔야 합니다.

2.9 timeout 과 배치 청크 트랜잭션

@Transactional(timeout = 5) 처럼 초 단위 제한을 걸어 두면, 트랜잭션이 그 시간을 넘길 때 강제로 롤백돼 커넥션을 오래 붙잡는 사고를 막습니다. 배치 작업에서는 전체를 한 트랜잭션으로 묶지 않고, 1000건 같은 청크 단위로 트랜잭션을 나눠 커밋합니다.

청크 하나가 실패해도 그 안의 실패 건 로그는 남아야 하므로, 로그 저장 메서드만 REQUIRES_NEW 로 분리합니다. 이 레슨의 logFailure 가 그 역할이고, 2.7 절의 원리를 그대로 씁니다.

2.10 동시성: 비관적 락 vs 낙관적 락

이체처럼 같은 행을 동시에 수정할 수 있는 업무에는 두 가지 락 전략이 있습니다.

전략 방식 특징
비관적 락 SELECT ... FOR UPDATE 조회 시점부터 잠금, 대기 발생
낙관적 락 version 컬럼 비교 갱신 시점에만 충돌 감지, 재시도 필요

비관적 락은 충돌이 잦은 계좌 이체에 적합하고, 낙관적 락은 충돌이 드문 화면 저장에 적합합니다. 두 계좌를 동시에 잠그는 순서가 요청마다 다르면 데드락(Oracle 오류 코드 ORA-00060)이 발생하므로, 항상 계좌 ID 오름차순처럼 고정된 순서로 잠급니다.

2.11 MyBatis 와 Spring 트랜잭션 조합

MyBatis 는 SqlSessionTemplate 을 통해 Spring 의 트랜잭션 동기화에 참여합니다. 같은 스레드에서 열린 Spring 트랜잭션이 있으면 MyBatis 도 그 트랜잭션의 커넥션을 그대로 씁니다.

@MapperScan 으로 매퍼 인터페이스를 스캔해 빈으로 등록하면, 서비스 계층에서 매퍼를 주입받아 호출할 때마다 별도 설정 없이 같은 규칙이 적용됩니다. 이 레슨의 Tx.currentConnection() 이 SqlSessionTemplate 이 커넥션을 찾는 방식을 단순화한 것입니다.

2.12 테스트에서 @Transactional 자동 롤백

테스트 클래스나 메서드에 @Transactional 을 붙이면, Spring 테스트 프레임워크는 테스트가 끝날 때 자동으로 롤백합니다. 테스트 데이터가 실제 DB 에 남지 않아 편리하지만, 커밋 이후 동작(캐시 갱신, 이벤트 발행)까지는 검증하지 못한다는 한계가 있습니다.

2.13 로그로 트랜잭션 확인

logging.level.org.springframework.transaction.interceptor=TRACE 를 설정하면 트랜잭션 시작·커밋·롤백 시점이 로그에 그대로 찍힙니다. 전파 속성이 의도대로 동작하는지 의심될 때 가장 먼저 켜 볼 로그입니다.

핵심 원리
  • 2.1 트랜잭션 경계는 프록시가 만든다
  • 2.2 전파 속성 7가지, 실무는 둘만 쓴다
  • 2.3 격리 수준 4단계와 Oracle 기본값
  • 2.4 readOnly=true, 하는 일과 안 하는 일
  • 2.5 롤백 규칙: unchecked 만 롤백된다
  • 2.6 self-invocation 함정
  • 2.7 내부 예외를 catch 로 삼키면
  • 2.8 트랜잭션 안에서 하면 안 되는 일
  • 2.9 timeout 과 배치 청크 트랜잭션
  • 2.10 동시성: 비관적 락 vs 낙관적 락
  • 2.11 MyBatis 와 Spring 트랜잭션 조합
  • 2.12 테스트에서 @Transactional 자동 롤백
  • 2.13 로그로 트랜잭션 확인
이전 섹션1 왜 배우는가2 / 7다음 섹션3 코드 예제