[7-1] [트랜잭션 전파 개념]

minpractice_jhj·2025년 9월 25일

Side Projects

목록 보기
8/16
post-thumbnail

스프링 트랜잭션 공부기: REQUIRED/REQUIRES_NEW · Self-Invocation · Hikari 지연 커넥션 (실험 로그)

요약

  • @Transactional프록시(proxy) 경유 시만 적용. 자기호출(Self-Invocation)미적용.
  • REQUIRES_NEW외부 빈 호출일 때만 부모 Suspend → 자식 새 트랜잭션. 부모 미커밋은 자식에서 가시성 없음(FK 실패 가능).
  • HikariCP auto-commit=false + hibernate.connection.provider_disables_autocommit=true첫 SQL 전까지 커넥션 미획득실제 로그로 확인(풀 점유/동시성에 유리).
읽기 전에 — PostService 트랜잭션 로그 10초 요약
로그 키워드의미 한 줄
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 정리

목차

  1. 베이스라인: 서비스 @Transactional 없이 어떻게 흘러가는가
  2. REQUIRED vs REQUIRES_NEW: 부모 REQUIRED → 자식 REQUIRES_NEW 실패, 부모 REQUIRED → 자식 REQUIRED 성공
  3. @Transactional“무시”되는 경우: 자기호출(Self-Invocation) & 접근제어자 이슈
  4. DB 커넥션 지연획득: autoCommit=false + provider_disables_autocommit=true 로그로 증명

1) 베이스라인: 서비스 @Transactional 미사용

핵심 정리

  • 서비스 레벨 트랜잭션이 없으면, Repository 호출 단위짧은 트랜잭션이 각각 생성/커밋된다(부분 커밋 가능).
  • 조회는 비트랜잭션읽기전용(readOnly) 짧은 트랜잭션이 혼재할 수 있다.
  • 외부 I/O(S3 업로드)는 DB 트랜잭션과 무관하게 수행된다.

실험 타임라인 & 로그 (원문)

1) 사전 인증(별도 트랜잭션) — 게시글 트랜잭션과 무관

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

2) 게시글 생성 진입(서비스 트랜잭션 해제 확인)

PostApiController : 게시글 생성 요청 수신: writer=흥미로운 허브, title=test
TX-PLAY: baseline - PostService.createPost (no @Transactional)

3) 조회 단계(비트랜잭션/읽기전용 트랜잭션 혼재)

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

4) posts 저장 — (①) 짧은 쓰기 트랜잭션 1회

Creating new transaction with name [SimpleJpaRepository.save]
insert into posts (...)
Completing transaction for [SimpleJpaRepository.save]
Initiating transaction commit
Committing JPA transaction on EntityManager [...]
commit

5) 이미지 업로드(S3) — 애플리케이션 외부 I/O

S3Service.uploadFile ... "이미지 업로드 성공" (HTTP 200)   // DB 트랜잭션과 무관

6) post_images 저장 — (②) 짧은 쓰기 트랜잭션 1회

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 짧은 트랜잭션으로 동작.


2) REQUIRED vs REQUIRES_NEW

실험 A: 부모 REQUIRED → 자식 REQUIRES_NEW (실패)

0) 사전 인증 (독립 트랜잭션)

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

1) 게시글 생성 진입 (부모 트랜잭션 REQUIRED 시작)

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)

2) 조회 (부모 REQUIRED 참여)

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

3) 게시글 저장 (부모 트랜잭션 내 INSERT)

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 생성(아직 부모 커밋 전)

4) 이미지 업로드 (외부 I/O)

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

5) 이미지 메타 저장 (자식 REQUIRES_NEW 시작 → 부모 Suspend)

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 위반

6) 롤백 전파

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 위반 → 실패 흐름이 로그로 명확히 증명.


실험 B: 부모 REQUIRED → 자식 REQUIRED (성공)

0) 사전 인증 (독립 트랜잭션)

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

1) 게시글 생성 진입 (부모 REQUIRED 시작)

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)

2) 조회 (부모 트랜잭션 참여)

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

3) 게시글 저장 (부모 트랜잭션 내 INSERT)

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

4) 이미지 업로드 (외부 I/O)

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

5) 이미지 메타 저장 (자식 REQUIRED, 부모와 동일 트랜잭션 참여)

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

6) 최종 커밋 & 후속 업데이트(썸네일 등)

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감사 로그/감사 테이블/재시도 단위처럼 완전히 별개인 업무에 신중 적용

3) @Transactional 이 “무시”되는 경우 — 자기호출(Self-Invocation)

핵심 정리

  • 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 경유를 고려.

4) DB 커넥션 지연획득 (Auto-commit 최적화)

TL;DR

목표: 첫 SQL 실행 직전까지 커넥션 미획득

설정:

spring:
  datasource:
    hikari:
      auto-commit: false
  jpa:
    properties:
      hibernate:
        connection:
          provider_disables_autocommit: true

전제: Hibernate 5.2.10+

A) 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 [...]

B) 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 동작을 검증하고 설계 상의 함의를 정리한 기록이다.

  • REQUIREDREQUIRES_NEW가시성 차이
  • 자기호출 무시 이슈와 해결 패턴
  • 지연 커넥션으로 풀 점유를 줄이는 방법까지, 모두 로그로 확인했다.
profile
운동처럼 개발도 작은 실천이 성장의 힘이 된다고 믿는 개발자 minpractice_jhj 기록

0개의 댓글