
최근 목표는 트랜잭션을 공부 → 가설 세우기 → 실제 서비스에 적용 → 로그로 검증이었다.
이번 글은 그중 “범위(Scope)”에만 집중한다. 요지는 간단하다.
업로드 같은 I/O는 트랜잭션 밖에서 먼저 끝내고, DB에는 URL만 짧게 반영한다.
그리고
auto-commit=false+provider_disables_autocommit=true로 커넥션 점유 구간을 최소화한다.
@Transactional 없이 베이스라인 측정 → 커넥션을 언제 빌리는지 감 잡기REQUIRED vs REQUIRES_NEW → 경계 분리가 미치는 영향 확인auto-commit=false + hibernate.connection.provider_disables_autocommit=true → 점유 시간 큰 폭 감소
그림 — 왼쪽: A안(업로드가 트랜잭션 안이라 active=1 유지), 오른쪽: B안(업로드 먼저, DB는 짧게 active=1→0).
auto-commit=false + provider_disables_autocommit=true적용.
[요청] → first SQL(=post save) → post_images save → (업로드) → thumbnail update → commit[요청] → (업로드만) → first SQL(=post save) → post_images save → thumbnail update → commit커넥션 지연획득을 돕고 트랜잭션 경계를 최소화하기 위해 다음만 적용했다.
spring:
datasource:
hikari:
auto-commit: false
jpa:
properties:
hibernate.connection.provider_disables_autocommit: true
auto-commit=false: 커밋 시점을 애플리케이션이 통제.hibernate.connection.provider_disables_autocommit=true: 하이버네이트가 드라이버의 오토커밋을 건드리지 않아 커넥션 점유 구간 축소.PostImageService — 업로드/저장 분리 + 보상 삭제기존에는 업로드 + DB 저장을 하나의 메서드에서 처리했다. 이를 아래 3개 역할로 분리했다(코드는 비공개, 메서드 역할만 명시).
uploadImagesOnly(...)post_images만 DB 저장 — saveImageUrls(...)deleteS3ByUrlsQuietly(...)PostService — 전/후 비교가 명확@Transactional
public PostResponse createPost(MemberInfo memberInfo,
CreatePostRequest createPostRequest,
CreatePostImageRequest createPostImageRequest) {
Post post = createPostEntity(memberInfo, createPostRequest);
post = postRepository.save(post);
appLogger.info(new InfoLog("게시글 생성 완료")
.setStatus("201")
.setTargetId(post.getId().toString())
.addDetails("writer", post.getMember().getNickname(), "title", post.getTitle()));
// 업로드 + DB 저장이 같은 경계 안
List<PostImage> postImages = postImageService.savePostImages(createPostImageRequest, post);
if (!postImages.isEmpty()) {
post.updateThumbnail(postImages.get(0).getS3Url());
}
return PostResponse.from(post);
}
@Timed("post.create")
@Transactional
public PostResponse createPost(MemberInfo memberInfo,
CreatePostRequest createPostRequest,
CreatePostImageRequest createPostImageRequest) {
StepWatch sw = new StepWatch("PostService.createPost");
// 1) 업로드만 먼저 (S3) — 트랜잭션 밖 I/O
sw.start("1. upload only");
List<String> uploadedUrls = postImageService.uploadImagesOnly(createPostImageRequest);
sw.stop();
try {
// 2) post insert
sw.start("2. post insert");
Post post = createPostEntity(memberInfo, createPostRequest);
post = postRepository.save(post);
sw.stop();
// 3) post_images DB 저장
sw.start("3. postImages save");
List<PostImage> postImages = postImageService.saveImageUrls(post, uploadedUrls);
sw.stop();
// 4) 썸네일 업데이트
sw.start("4. thumbnail update");
if (!postImages.isEmpty()) {
post.updateThumbnail(postImages.get(0).getS3Url());
}
sw.stop();
return PostResponse.from(post);
} catch (Exception e) {
// DB 단계 실패 시 → 이미 업로드된 S3 보상 삭제
postImageService.deleteS3ByUrlsQuietly(uploadedUrls);
throw e; // 원래 예외를 그대로 전파하여 트랜잭션 롤백
}
}
포인트
deleteS3ByUrlsQuietly(...)로 S3 고아 객체 방지@Timed와 StepWatch로 단계별 시간을 남겨 병목을 확인| 구분 | Tx 길이* | DB 커넥션 점유** | 전체 처리시간(StepWatch) | 비고 |
|---|---|---|---|---|
| 설정 전 · A안 | ≈448ms | ≈431ms | ≈402–406ms | 업로드가 Tx 안에 포함 |
| 설정 전 · B안 | ≈533ms | ≈178ms | ≈479ms | 업로드 먼저 수행 |
| 설정 후 · A안 | ≈641ms | ≈607ms | ≈589–592ms | Tx/DB 점유 모두 증가 |
| 설정 후 · B안 | ≈208ms | ≈39ms | ≈194ms | 업로드 먼저 + 설정 적용 |
JpaTransactionManager 시작~커밋 로그 간격** DB 점유: p6spy SQL 시작~커밋 로그 간격
최적 비교(설정 전 · A안 → 설정 후 · B안)
트랜잭션/커넥션 점유 시간 감소가 응답 시간 개선으로 직결되었다.
핵심 포인트: 업로드가 트랜잭션 내부에 있어 커넥션이 긴 시간(active=1) 점유된다.
18:00:06.210 Creating new transaction ... [PostService.createPost]
# (트랜잭션 시작 직후부터 DB 점유)
18:00:06.223 select ... from members where nickname='흥미로운 허브'
18:00:06.333 insert into posts (...)
18:00:06.340 HikariPool-1 - Pool stats (total=10, active=1, idle=9, waiting=0) <-- active=1 유지 시작
# (업로드가 Tx 안에서 수행됨 → DB 커넥션 계속 점유)
18:00:06.458 S3 PUT /pinup/post/2620c004-..._1697261010140.jpg
18:00:06.515 S3 200 OK
18:00:06.568 S3 PUT /pinup/post/5565cfb8-..._full-stack-developer.jpg
18:00:06.583 S3 200 OK
# (이미지 메타 저장 + 커밋)
18:00:06.618 insert into post_images (...)
18:00:06.622 insert into post_images (...)
18:00:06.658 commit
18:00:06.658 HikariPool-1 - Pool stats (total=10, active=0, idle=10, waiting=0) <-- 커밋 후 반납
요약
핵심 포인트: 업로드를 먼저 끝낸 뒤 필요한 순간에만 DB 커넥션을 잠깐 빌린다.
18:37:31.792 Creating new transaction ... [PostService.createPost]
# (업로드 전용 구간) — DB 미사용
18:37:31.801 S3 PUT /pinup/post/3e140853-..._지도도.webp
18:37:31.823 S3 200 OK
18:37:31.851 S3 PUT /pinup/post/9d6ba0d9-..._통합본.jpg
18:37:31.929 S3 200 OK
18:37:31.930 HikariPool-1 - Pool stats (total=10, active=0, idle=10, waiting=0) <-- 업로드 동안 active=0
# (DB 구간 진입) — 짧게 빌리고 바로 반납
18:37:31.961 select ... from members where nickname='흥미로운 허브'
18:37:31.966 select ... from stores where id=9
18:37:31.975 HikariPool-1 - Pool stats (total=10, active=1, idle=9, waiting=0) <-- DB 커넥션 점유 시작(스파이크)
18:37:31.974 insert into posts (...)
18:37:31.980 insert into post_images (...)
18:37:31.985 insert into post_images (...)
18:37:31.991 update posts set ... thumbnail_url=...
18:37:32.000 commit
18:37:32.000 HikariPool-1 - Pool stats (total=10, active=0, idle=10, waiting=0) <-- 커밋 즉시 반납
요약
위 Pool stats 라인은 업로드 종료/첫 SQL 직후/커밋 직후 등 스텝 경계에서 출력했다. 이 지점만 찍어도 active=0 → 1 → 0 패턴이 선명히 드러난다.
auto-commit=false + hibernate.connection.provider_disables_autocommit=true 조합으로 커넥션 점유 구간을 줄인다.