
읽기 전에 — PostService 트랜잭션 로그 10초 요약요약
@Transactional은 프록시(proxy) 경유 시만 적용. 자기호출(Self-Invocation) 은 미적용.REQUIRES_NEW는 외부 빈 호출일 때만 부모 Suspend → 자식 새 트랜잭션. 부모 미커밋은 자식에서 가시성 없음(FK 실패 가능).HikariCP auto-commit=false+hibernate.connection.provider_disables_autocommit=true로 첫 SQL 전까지 커넥션 미획득을 실제 로그로 확인(풀 점유/동시성에 유리).
| 로그 키워드 | 의미 한 줄 |
|---|---|
Found thread-bound EntityManager ... | 요청/스레드에 EM 바인딩 → 동일 EM 재사용 |
Creating new transaction [PostService.createPost]: REQUIRED | @Transactional 진입 → 기존 참여/없으면 생성 |
Exposing JPA transaction as JDBC ... | JPA 트랜잭션 JDBC 레벨 노출(Hibernate DB 통신 준비) |
Participating in existing transaction | 리포지토리는 상위 트랜잭션 참여 |
Initiating transaction commit | 커밋 시작(이때부터 DB 반영) |
Committing JPA transaction on EntityManager ... | flush → commit |
Suspending current transaction, creating new transaction ... | (REQUIRES_NEW 정상) 부모 Suspend → 자식 새 트랜잭션 |
Closing ... in OpenEntityManagerInViewInterceptor | 응답 직전 EM 정리 |
@Transactional이 “무시”되는 경우: 자기호출(Self-Invocation) & 접근제어자 이슈autoCommit=false + provider_disables_autocommit=true 로그로 증명핵심 정리
- 서비스 레벨 트랜잭션이 없으면, Repository 호출 단위로 짧은 트랜잭션이 각각 생성/커밋된다(부분 커밋 가능).
- 조회는 비트랜잭션과 읽기전용(readOnly) 짧은 트랜잭션이 혼재할 수 있다.
- 외부 I/O(S3 업로드)는 DB 트랜잭션과 무관하게 수행된다.
Creating new transaction with name [MemberService.isAccessTokenExpired]
No need to create transaction for [SimpleJpaRepository.findByEmailAndProviderTypeAndIsDeletedFalse]: This method is not transactional.
Completing transaction for [MemberService.isAccessTokenExpired]
Initiating transaction commit
Committing JPA transaction on EntityManager [...]
commit
Closing JPA EntityManager [...] after transaction
PostApiController : 게시글 생성 요청 수신: writer=흥미로운 허브, title=test
TX-PLAY: baseline - PostService.createPost (no @Transactional)
No need to create transaction for [SimpleJpaRepository.findByNickname]: This method is not transactional. // 비트랜잭션 조회
Creating new transaction with name [SimpleJpaRepository.findById] ... ,readOnly // 읽기전용 짧은 트랜잭션
Completing transaction for [SimpleJpaRepository.findById]
Initiating transaction commit
Committing JPA transaction on EntityManager [...]
commit
Creating new transaction with name [SimpleJpaRepository.save]
insert into posts (...)
Completing transaction for [SimpleJpaRepository.save]
Initiating transaction commit
Committing JPA transaction on EntityManager [...]
commit
S3Service.uploadFile ... "이미지 업로드 성공" (HTTP 200) // DB 트랜잭션과 무관
Creating new transaction with name [SimpleJpaRepository.saveAll]
insert into post_images (...) // 1건
insert into post_images (...) // 1건
Completing transaction for [SimpleJpaRepository.saveAll]
Initiating transaction commit
Committing JPA transaction on EntityManager [...]
commit
한 줄 결론
서비스 레벨 @Transactional 없이 진행되어, save/saveAll 호출마다 각각 별도의 트랜잭션 생성→커밋이 명확. 일부 조회는 완전 비트랜잭션, 일부는 readOnly 짧은 트랜잭션으로 동작.
2025-09-23T16:24:00.111+09:00 DEBUG ... JpaTransactionManager : Creating new transaction with name [MemberService.isAccessTokenExpired]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2025-09-23T16:24:00.115+09:00 TRACE ... TransactionInterceptor : Getting transaction for [MemberService.isAccessTokenExpired]
2025-09-23T16:24:00.251+09:00 INFO p6spy : select ... from members where email='pinup0106@gmail.com' and provider_type='GOOGLE' and not(is_deleted)
2025-09-23T16:24:00.252+09:00 DEBUG ... JpaTransactionManager : Committing JPA transaction on EntityManager [...]
2025-09-23T16:24:00.254+09:00 INFO p6spy : commit
2025-09-23T16:24:00.325+09:00 INFO PostApiController : 게시글 생성 요청 수신: writer=흥미로운 허브, title=test REQUIRES_NEW
2025-09-23T16:24:00.326+09:00 DEBUG ... JpaTransactionManager : Creating new transaction with name [PostService.createPost]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2025-09-23T16:24:00.327+09:00 INFO PostService : TX-PLAY: baseline - PostService.createPost ( @Transactional)
2025-09-23T16:24:00.327+09:00 TRACE ... No need to create transaction for [SimpleJpaRepository.findByNickname]
2025-09-23T16:24:00.333+09:00 INFO p6spy : select ... from members where nickname='흥미로운 허브'
2025-09-23T16:24:00.335+09:00 TRACE ... Getting transaction for [SimpleJpaRepository.findById]
2025-09-23T16:24:00.337+09:00 INFO p6spy : select ... from stores where id=9
2025-09-23T16:24:00.339+09:00 TRACE ... Getting transaction for [SimpleJpaRepository.save]
2025-09-23T16:24:00.400+09:00 INFO p6spy : insert into posts (...) values ('test REQUIRES_NEW','2025-09-23T16:24:00.361+0900',false,0,1,9,NULL,'test REQUIRES_NEW',0)
2025-09-23T16:24:00.415+09:00 INFO StructuredLogger : 게시글 생성 완료 targetId=10009
posts.id=10009 생성(아직 부모 커밋 전)
2025-09-23T16:24:00.482+09:00 DEBUG ... Sending Request: PUT /pinup/post/28f94855-....jpg
2025-09-23T16:24:00.509+09:00 DEBUG ... Received successful response: 200
2025-09-23T16:24:00.533+09:00 INFO StructuredLogger : 이미지 업로드 성공 url=http://127.0.0.1:4566/pinup/post/28f94855-....jpg
2025-09-23T16:24:00.415+09:00 DEBUG ... Suspending current transaction, creating new transaction with name [PostImageService.savePostImages]
2025-09-23T16:24:00.418+09:00 INFO PostImageService : TX-PLAY: baseline - PostImageService.savePostImages ( @Transactional REQUIRES_NEW)
2025-09-23T16:24:00.559+09:00 TRACE ... Getting transaction for [SimpleJpaRepository.saveAll]
2025-09-23T16:24:00.572+09:00 INFO p6spy : insert into post_images (created_at,post_id,url) values ('2025-09-23T16:24:00.560+0900',10009,'http://127.0.0.1:4566/pinup/post/28f94855-....jpg')
2025-09-23T16:24:00.575+09:00 ERROR SqlExceptionHelper : ERROR: insert or update on table "post_images" violates foreign key constraint ...
Detail: Key (post_id)=(10009) is not present in table "posts".
왜 실패? 부모는 커밋 전이므로 자식 트랜잭션에서는 posts(10009)가 보이지 않음 → FK 위반
2025-09-23T16:24:00.589+09:00 DEBUG ... Rolling back JPA transaction on EntityManager [...] // 자식 REQUIRES_NEW 롤백
2025-09-23T16:24:00.592+09:00 DEBUG ... Resuming suspended transaction after completion of inner transaction
2025-09-23T16:24:00.592+09:00 TRACE ... Completing transaction for [PostService.createPost] after exception ...
2025-09-23T16:24:00.593+09:00 INFO p6spy : rollback // 부모도 롤백
결론(실험 A)
트랜잭션 경계를 분리(REQUIRES_NEW)하면 부모 미커밋 데이터가 자식에서 가시성 없음 → FK 위반 → 실패 흐름이 로그로 명확히 증명.
2025-09-23T16:26:10.932+09:00 DEBUG ... Creating new transaction [MemberService.isAccessTokenExpired]
2025-09-23T16:26:11.066+09:00 INFO p6spy : select ... from members where email='pinup0106@gmail.com' and provider_type='GOOGLE' and not(is_deleted)
2025-09-23T16:26:11.070+09:00 INFO p6spy : commit
2025-09-23T16:26:11.167+09:00 INFO PostApiController : 게시글 생성 요청 수신: writer=흥미로운 허브, title=test REQUIRED
2025-09-23T16:26:11.168+09:00 DEBUG ... Creating new transaction with name [PostService.createPost]: PROPAGATION_REQUIRED
2025-09-23T16:26:11.169+09:00 INFO PostService : TX-PLAY: baseline - PostService.createPost ( @Transactional)
2025-09-23T16:26:11.175+09:00 INFO p6spy : select ... from members where nickname='흥미로운 허브'
2025-09-23T16:26:11.181+09:00 INFO p6spy : select ... from stores where id=9
2025-09-23T16:26:11.270+09:00 INFO p6spy : insert into posts (...) values ('test REQUIRED','2025-09-23T16:26:11.220+0900',false,0,1,9,NULL,'test REQUIRED',0)
2025-09-23T16:26:11.284+09:00 INFO StructuredLogger : 게시글 생성 완료 targetId=10010
2025-09-23T16:26:11.363+09:00 DEBUG ... Sending Request: PUT /pinup/post/7e2be727-....jpg
2025-09-23T16:26:11.392+09:00 DEBUG ... Received successful response: 200
2025-09-23T16:26:11.426+09:00 INFO StructuredLogger : 이미지 업로드 성공 url=http://127.0.0.1:4566/pinup/post/7e2be727-....jpg
2025-09-23T16:26:11.285+09:00 INFO PostImageService : TX-PLAY: baseline - PostImageService.savePostImages ( @Transactional REQUIRED)
2025-09-23T16:26:11.480+09:00 INFO p6spy : insert into post_images (...) values ('2025-09-23T16:26:11.472+0900',10010,'http://127.0.0.1:4566/pinup/post/7e2be727-....jpg')
2025-09-23T16:26:11.483+09:00 INFO p6spy : insert into post_images (...) values ('2025-09-23T16:26:11.481+0900',10010,'http://127.0.0.1:4566/pinup/post/04699b9c-....jpg')
2025-09-23T16:26:11.485+09:00 INFO StructuredLogger : 이미지 저장 완료 count=2
동일 트랜잭션이므로 부모 미커밋 posts(10010)가 즉시 가시 → FK OK
2025-09-23T16:26:11.485+09:00 TRACE ... Completing transaction for [PostService.createPost]
2025-09-23T16:26:11.508+09:00 INFO p6spy : update posts set ... thumbnail_url='http://127.0.0.1:4566/pinup/post/7e2be727-....jpg', updated_at='2025-09-23T16:26:11.491+0900', version=1 where id=10010 and version=0
2025-09-23T16:26:11.517+09:00 INFO p6spy : commit
결론(실험 B)
부모와 자식이 하나의 트랜잭션(REQUIRED) 에 참여하여, 부모 INSERT가 즉시 가시 → FK 만족 → 전체 정상 커밋.
블로그용 한 컷 요약
- REQUIRED: 현재 트랜잭션에 참여 → 미커밋 데이터라도 동일 트랜잭션 내 가시 → FK/일관성 유지에 유리
- REQUIRES_NEW: 새 트랜잭션(부모 suspend) → 부모 미커밋 데이터 가시성 없음 → FK/유니크 제약 쉽게 실패
설계 팁
- FK로 강결합된 부모-자식 쓰기는 동일 트랜잭션(REQUIRED) 으로 처리
- 외부 I/O(S3 등) 는 AFTER_COMMIT 이벤트/비동기/아웃박스로 분리
REQUIRES_NEW는 감사 로그/감사 테이블/재시도 단위처럼 완전히 별개인 업무에 신중 적용
핵심 정리
- Spring의 AOP는 프록시 경유 호출일 때만 적용된다.
- 같은 클래스 내부에서
this.persistAuditLog()로 호출하면 프록시를 우회 → 어노테이션 미적용.private/protected/static메서드는 프록시가 가로챌 수 없다(특히 static은 대상 아님).
// PostService.createPost(@Transactional) 내부에서 같은 클래스 메서드를 호출:
txPrivateNewProbe(); // private + REQUIRES_NEW
txProtectedNewProbe(); // protected + REQUIRES_NEW
txStaticNewProbe(); // static + REQUIRES_NEW
... JpaTransactionManager : Found thread-bound EntityManager [...] for JPA transaction
... JpaTransactionManager : Participating in existing transaction
... TransactionInterceptor : Getting transaction for [SimpleJpaRepository.count]
// 기대했던 "Suspending current transaction ..." / "Opened new EntityManager ..." 없음
결론
REQUIRES_NEW가 붙어 있어도 기존 트랜잭션 참여(=사실상 무시).
설정
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
exposeProxy=true: 현재 프록시를 AopContext.currentProxy()로 노출proxyTargetClass=true: CGLIB 클래스 프록시 강제호출
((PostService) AopContext.currentProxy()).txPublicNewProbe(); // public + @Transactional(REQUIRES_NEW)
정상 동작 로그 시퀀스(대표)
... JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(931946576<open>)] for JPA transaction
... JpaTransactionManager : Suspending current transaction, creating new transaction with name [kr.co.pinup.posts.service.PostService.txPublicNewProbe]
... JpaTransactionManager : Opened new EntityManager [SessionImpl(64740231<open>)] for JPA transaction
... TransactionInterceptor : Getting transaction for [PostService.txPublicNewProbe]
... PostService : txPublicNewProbe (public, REQUIRES_NEW) start
... SimpleJpaRepository.count
... JpaTransactionManager : Initiating transaction commit
... JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(64740231<open>)]
... p6spy : commit | connection 1
... JpaTransactionManager : Resuming suspended transaction after completion of inner transaction
... JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(931946576<open>)] for JPA transaction
체크리스트
- 로그에
Suspending current transaction …/Opened new EntityManager …가 보이는가?p6spy의 커넥션 번호가 부모/자식 분리되어 보이는가?- 내부 호출이면 대부분 미적용이다. 별도 빈 분리 또는
AopContext경유를 고려.
TL;DR
목표: 첫 SQL 실행 직전까지 커넥션 미획득
설정:
spring: datasource: hikari: auto-commit: false jpa: properties: hibernate: connection: provider_disables_autocommit: true전제: Hibernate 5.2.10+
auto-commit=true (조기 획득 패턴)트랜잭션 직후에도 active=1 → 슬립 중에도 커넥션 점유
2025-09-17T17:10:20.913+09:00 DEBUG ... Creating new transaction with name [com.example.demo.service.UserService.join]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2025-09-17T17:10:20.920+09:00 DEBUG ... Exposing JPA transaction as JDBC [...]
2025-09-17T17:10:33.608+09:00 DEBUG ... HikariPool-1 - Pool stats (total=10/10, idle=9/10, active=1, waiting=0) <-- 슬립 중 active=1
2025-09-17T17:11:01.016+09:00 DEBUG ... org.hibernate.SQL : insert into users (email,name,id) values (?,?,default)
2025-09-17T17:11:41.085+09:00 DEBUG ... Committing JPA transaction on EntityManager [...]
auto-commit=false + provider_disables_autocommit=true (지연 획득 유지)첫 SQL 전까지 active=0, SQL 시점에만 커넥션 빌림
2025-09-17T17:17:58.267+09:00 DEBUG ... Creating new transaction with name [com.example.demo.service.UserService.join]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2025-09-17T17:18:01.595+09:00 DEBUG ... HikariPool-1 - Pool stats (total=10/10, idle=10/10, active=0, waiting=0) <-- 슬립 중 active=0
2025-09-17T17:18:38.315+09:00 DEBUG ... org.hibernate.SQL : insert into users (email,name,id) values (?,?,default) <-- 첫 SQL
2025-09-17T17:19:01.602+09:00 DEBUG ... HikariPool-1 - Pool stats (total=10/10, idle=9/10, active=1, waiting=0) <-- SQL 직후 active=1
2025-09-17T17:19:18.364+09:00 DEBUG ... Committing JPA transaction on EntityManager [...]
원리 요약
- JDBC 기본은
autoCommit=true라 프레임워크가 트랜잭션 경계를 맞추려면 커넥션을 일찍 빌릴 수 있음.autoCommit=false이고 Hibernate에 “프로바이더가 관리한다”(=provider_disables_autocommit=true)고 알리면, Hibernate가setAutoCommit(false)를 중복 호출하지 않음 → 첫 SQL 전까지 커넥션 미획득이 가능.
언제 유용?
- 요청 초반에 검증/캐싱/외부 API 대기/슬립 등 DB를 안 쓰는 준비 단계가 긴 경우
- 풀 사이즈가 빡빡하고 동시성을 끌어올려야 하는 경우
- 배치에서 전처리 구간 길고 일부 구간만 DB를 쓰는 경우
주의/함정
- 조기 flush, Lazy 즉시 해제 등으로 SQL이 일찍 실행되면 그 시점에 커넥션을 빌린다.
- DB/드라이버별
setAutoCommit비용 상이(일부는 네트워크 왕복).- 하우스키퍼 로그 주기/환경에 따라 안 뜰 수 있음 → SQL 타임라인으로도 검증 가능.
이번 글은 문서 설명이 아니라, 핀업(Pin-Up) 프로젝트에서 직접 찍어낸 로그로 @Transactional 동작을 검증하고 설계 상의 함의를 정리한 기록이다.
REQUIRED와 REQUIRES_NEW의 가시성 차이