Develop/Java(JPA)

JPA - save() 시 데드락 장애 발견

째용이 2025. 5. 30. 12:27

원인 로직

    /**
     * 결제 이력으로 매출 저장
     * @param sttHist
     */
    private void registerSalesFromApi( SttHist sttHist, String couponId, Long contractNo, Frame frame, BigDecimal quantity, PriceDTO price ,String windowTime ) {
        CouponUsageScope couponUsageScope = null;
        String salesId = null;
        while( true ) {
            salesId = generateSalesId( SalesDivision.INCOME );
            if( !salesRepository.existsById( salesId ) )
                break;
        }
        ...
        salesRepository.save( sales );
    }

해당 로직 돌면서 salesRepository.save( sales ); 에서 장애 발생

✅ 1. 데드락 발생 가능 구간 요약

데드락 발생 지점:

while (true) { salesId = generateSalesId(SalesDivision.INCOME); if (!salesRepository.existsById(salesId)) break; }
salesRepository.save(sales);

가능성 높은 원인:

  • salesId를 직접 생성하고 중복 체크를 수동으로 수행하는 구조
  • 이 부분은 동시성 이슈로 데드락이 발생할 가능성이 매우 높음

이 코드는 동시에 여러 스레드에서 진입하면 existsById 쿼리와 save 쿼리가 충돌하거나 경합 가능성 높음.


✅ 2. 해결 방향 요약

ID 자동 생성으로 전환 (@GeneratedValue, UUID 등)

가능하다면 salesId를 DB나 애플리케이션 수준에서 자동 생성되도록 변경.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long salesId;

또는 UUID 기반:

sales.setSalesId(UUID.randomUUID().toString());

→ existsById() 루프를 없애면 락과 경합 자체가 사라져 데드락 위험이 급감.


트랜잭션 분리 또는 시간 최소화

registerSalesFromApi() 안에서 너무 많은 데이터를 다루고 있기 때문에:

  • 모든 DB 조회 (ctrRepository, natRepository, xrtRepository, cpnRepository)를 save 이전에 다 마친 후, 트랜잭션을 새로 시작하거나 또는 이 전체 로직을 @Transactional(propagation = REQUIRES_NEW)처럼 독립 트랜잭션으로 분리하는 것도 방법.

🔁 ③ 데드락 재시도 로직 추가

일시적인 데드락은 재시도로 해결됩니다. Spring에서 다음처럼 처리 가능:

@Retryable(
    value = { LockAcquisitionException.class },
    maxAttempts = 3,
    backoff = @Backoff(delay = 1000)
)
@Transactional
private void registerSalesFromApi(...) {
    ...
}

단, 위 두 가지 조치를 먼저 하고, 최후의 안전망으로 적용.


✅ 3. 추가 개선 제안

항목 개선 포인트

generateSalesId() 동시 요청에 안전하지 않음. Redis/DB 시퀀스 기반으로 개선 필요
salesRepository.existsById() 병목 가능성 있음. 가능한 한 제거
save() 전 데이터 가공 너무 많은 DB 호출이 한 트랜잭션 안에 있음. 분리 고려
트랜잭션 길이 로직이 길고 복잡하여 락 점유 시간 길어짐

결론: 실전 리팩토링 방향

  1. salesId 직접 생성 로직 제거 (UUID 등으로 자동화)
  2. existsById() 제거로 DB 경합 감소
  3. @Retryable로 예외에 대비
  4. 트랜잭션 내 DB 호출 최소화 / 분리
  5. salesRepository.save() 전후를 잘게 나누어 트랜잭션 범위 조절
  • salesId 값을 가져오는 곳에서 발생하는것으로 파악 generateSalesId()
        while( true ) {
            salesId = generateSalesId( SalesDivision.INCOME );
            if( !salesRepository.existsById( salesId ) )
                break;
        }

    private String generateSalesId( SalesDivision salesDivision ) {
        return String.format("%s%s%012d", salesDivision.getCode().charAt(0),
                LocalDateTime.now().format(DateTimeFormatter.ofPattern(CommonConstants.DATE_TIME_FORMAT_LONG_DATE)),
                RandomUtils.nextLong(0, 999999999999L));
    }

🔍 현재 방식의 문제점

java
복사편집
String.format("%s%s%012d",
    salesDivision.getCode().charAt(0),
    LocalDateTime.now().format(...),
    RandomUtils.nextLong(0, 999999999999L)
);

  • 충돌 가능성 있음: 동일한 salesDivision, LocalDateTime.now() (같은 시/초)에 여러 요청이 들어오면, Random 숫자가 우연히 중복될 수 있음.
  • 중복 체크(existsById)를 수동으로 하므로 데드락 유발: 동시 다발적으로 들어오면 서로 경합하면서 DB 락 발생.
  • 랜덤 + 타임스탬프 방식은 유니크 보장이 없음

해결 방안 3가지

1. UUID 사용 + 접두사

간단하고 확실하게 유니크한 ID 보장:

private String generateSalesId(SalesDivision salesDivision) {
    return salesDivision.getCode().charAt(0) + UUID.randomUUID().toString().replace("-", "");
}

예시 출력:

I38f12bcb1efc4c5ea7d8b0cf627d92b1

👉 UUID는 전 세계적으로 중복 확률이 거의 0에 수렴하므로 existsById() 제거 가능. 데드락도 사라짐.

 


2. DB 시퀀스 또는 자동 증가 키 사용

만약 RDBMS(MySQL/MariaDB/PostgreSQL 등)에서 ID를 숫자로 관리하고 싶다면

  • salesId를 @GeneratedValue(strategy = GenerationType.IDENTITY) 등으로 위임
  • 혹은 시퀀스를 만들어 @GeneratedValue(strategy = SEQUENCE, generator = "...") 방식 사용

3. Redis를 활용한 전역 시퀀스 생성기

만약 특정 포맷 유지가 꼭 필요하다면, Redis의 atomic increment를 사용하는 방법도 있음:

// Redis key: SALES_ID_SEQ:20250509
Long seq = redisTemplate.opsForValue().increment("SALES_ID_SEQ:" + LocalDate.now());
String salesId = String.format("I%s%06d", LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")), seq);

이 방식은 중복 없음 + 포맷 유지 가능 + 성능 빠름

단, Redis 연결 필요


📌 결론 및 적용 권장

방식 장점 단점 추천도

UUID 유니크 보장, 간단 가독성 떨어질 수 있음 ⭐⭐⭐⭐⭐
DB 시퀀스 자동 관리, 중복 없음 ID 포맷 제한 ⭐⭐⭐⭐
Redis 시퀀스 포맷 유지 + 빠름 Redis 도입 필요 ⭐⭐⭐⭐
랜덤+타임 중복 위험, 데드락 위험 불안정 ❌❌❌

 

 

결론

uuid 방식으로 변경