[ThatzFit] 트랜잭션 최적화: 쓰기 경계 분리

2025. 9. 1. 07:10·프로덕트 문제 해결 기록

배경

가상피팅 요청은 외부 I/O(머신러닝 모델을 통한 카테고리 예측, S3 업·다운로드, 폴링)와 DB 저장이 한 흐름 안에서 섞여 수행되고 있었습니다. 트랜잭션 경계가 넓게 설정될 경우 커넥션 점유 시간이 길어지고, 읽기 전용 구간도 쓰기 트랜잭션으로 실행되어 불필요한 더티 체크가 발생할 가능성이 있었습니다. 이를 최적화하는 작업을 수행했습니다.

최적화 중 나타난 문제

로그에 “cannot execute INSERT in a read-only transaction” 오류가 발생하였습니다. TryOn엔티티 저장 시점에 INSERT가 읽기 전용 트랜잭션에서 실행된 것이 원인이었습니다.

2025-08-30 14:36:33.879 [http-nio-8080-exec-9] ERROR t.a.c.e.GlobalExceptionHandler - ⚠️ [GlobalExceptionHandler] 런타임 예외 발생
org.springframework.orm.jpa.JpaSystemException: could not execute statement [ERROR: cannot execute INSERT in a read-only transaction] [/* insert for tryonu.api.domain.Cloth */insert into clothes (category,created_at,image_url,is_deleted,product_page_url,updated_at) values (?,?,?,?,?,?)]

원인

읽기 전용 트랜잭션을 기본으로 세팅하기 위해 TryOnServiceImpl 클래스에 @Transactional(readOnly = true)를 적용했었습니다. 내부에서 호출된 저장 로직이 같은 읽기 전용 트랜잭션에 참여하면서 INSERT가 차단되었고, 결과적으로 예외가 발생하였습니다. 스프링에서 @Transactional의 기본 세팅은 propagation = REQUIRED이기 때문에 현재 컨텍스트에서 실행되고 있는 트랜잭션이 있다면 거기에 참여하게 됩니다. 이 경우 readonly 트랜잭션에 최적화를 위해 분리한 쓰기 트랜잭션이 참여해서 해당 에러를 만난 것입니다.

해결

쓰기 로직을 별도 서비스로 분리하고 짧은 트랜잭션으로 실행하도록 변경하였습니다. TryOnWriteService.saveAndBuildResponse에 @Transactional(propagation = REQUIRES_NEW)를 적용하여 상위의 읽기 전용 트랜잭션을 일시 중단하고, 새로운 쓰기 트랜잭션에서 DB 작업을 처리하도록 하였습니다.

// TryOnWriteServiceImpl (핵심)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public TryOnResponse saveAndBuildResponse(...) {
    // Cloth 저장 -> TryOnResult 저장 -> User 최근 모델 업데이트
}

구현 포인트

  • 클래스 기본 전략 정리
    • 서비스 구현 클래스 전반에 @Transactional(readOnly = true)를 적용하고, 실제 변경 메서드에만 @Transactional을 부여하였습니다.
  • TryOn 쓰기 경계 분리
    • 외부 I/O와 폴링은 TryOnServiceImpl에서 트랜잭션 밖에서 수행하도록 하였습니다.
    • 엔티티 저장과 사용자 최근 모델 업데이트는 TryOnWriteService로 위임하였습니다.
    • TryOnWriteServiceImpl.saveAndBuildResponse는 REQUIRES_NEW로 짧은 쓰기 트랜잭션만 수행하도록 하였습니다.
  • 예외 처리
    • 예외는 삼키지 않고 상위로 전파하여 실패를 명확히 드러내도록 유지하였습니다.

효과

  • 트랜잭션 길이가 단축되어 커넥션 점유와 락 보유 시간이 줄어들었습니다. S3 업로드/다운로드, 가상피팅 실행(ML), 백그라운드 제거(ML)과 같은 무거운 작업들이 한 트랜잭션에 같이 있어서 약 13초라는 어무마시한 시간동안 락을 잡고 있었습니다. 기본적으로는 가벼운 read-only로 실행되고, 쓰기 시에만 잠깐 점유하는 형태로 개선되었습니다.
  • 외부 I/O 대기와 DB 쓰기가 분리되어 타임아웃과 데드락 위험이 완화되었습니다.
  • 읽기 전용 구간에서 더티 체크가 생략되어 조회 경로의 오버헤드가 감소하였습니다.

측정 방법 공부

  • 내부 계측
    • 엔드투엔드 지연시간과 오류율을 측정합니다. 예를 들어 /api/try-on/fitting의 p50, p95, p99, 실패율, RPS를 확인합니다.
    • 쓰기 트랜잭션 시간 분포를 별도로 수집합니다. TryOnWriteService.saveAndBuildResponse의 실행 시간을 타이머나 로그로 기록합니다.
    • HikariCP 지표(active, pending, acquire time, usage time)를 비교합니다.
  • 부하테스트
    • 익숙한 도구(Locust 등)로 동일한 시나리오를 사용하여 최적화 전·후를 A/B로 비교합니다.
  • 모킹/스텁
    • S3, 가상피팅 API, 카테고리 예측을 스텁 또는 WireMock·페이크 구현으로 대체하여 외부 변동성을 제거하고, DB 쓰기 경계 최적화 효과만 분리하여 관찰합니다.

회고

읽기 구간은 기본 readOnly, 쓰기만 짧고 명확한 트랜잭션으로 실행하는 단순한 원칙이 효과적이었습니다. 외부 I/O와 DB 쓰기를 분리하면 트랜잭션이 의도치 않게 길어지는 문제를 피할 수 있습니다. 전파 속성(REQUIRES_NEW)과 프로필 기반 스텁을 함께 설계하면 측정과 운영 모두 수월해집니다.

다만 현재 개발 사이클이 매우 빠르게 돌고있고 기획 입장에선 최적화보다 중요한 기능개발 태스크들이 많아서 미처 측정까지 하지 못했습니다. 다음에는 외부 의존성을 stubbing하는 구현까지 도입해서 수치를 측정하고 글을 남기도록 하겠습니다.

참고한 글

  • [Spring] 트랜잭션과 @Transactional 정리
  • Spring 트랜잭션은 언제 어떻게 롤백 될까? -1편

'프로덕트 문제 해결 기록' 카테고리의 다른 글

WebClient와 CachingFilter 사용 시 InputStream 재사용 오류 해결  (0) 2025.09.03
[ThatzFit] AOP로 로깅 일원화  (0) 2025.09.02
[ThatzFit] Spring Data JPA 비관적 락 리팩토링: 문제 해결 기록  (1) 2025.09.01
[ThatzFit] API 에러 모니터링: 체계적인 슬랙(Slack) 알림 시스템 구축기  (0) 2025.08.14
[WIMI] Spring에서 "No acceptable representation" 오류  (0) 2025.03.28
'프로덕트 문제 해결 기록' 카테고리의 다른 글
  • [ThatzFit] AOP로 로깅 일원화
  • [ThatzFit] Spring Data JPA 비관적 락 리팩토링: 문제 해결 기록
  • [ThatzFit] API 에러 모니터링: 체계적인 슬랙(Slack) 알림 시스템 구축기
  • [WIMI] Spring에서 "No acceptable representation" 오류
givemethatsewon
givemethatsewon
  • givemethatsewon
    프로덕트 중심 개발자
    givemethatsewon
  • 전체
    오늘
    어제
    • 분류 전체보기 (11)
      • 컴퓨터 (4)
        • CS (4)
        • 에러 트러블 슈팅 (0)
        • 알고리즘 (0)
      • 프로덕트 문제 해결 기록 (6)
        • Clody (0)
        • 8fit (0)
        • WIMI (0)
      • etc (1)
        • 경험 (0)
        • 정보 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    스프링
    UML
    공유메모리
    rust
    인터페이스
    udp
    Jackson
    파이프
    spring
    의존성
    tcp
    socket
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
givemethatsewon
[ThatzFit] 트랜잭션 최적화: 쓰기 경계 분리
상단으로

티스토리툴바