GOORM-DEEP DIVE 백엔드 3회차<스타벅스 클론> 주문 번호 구현, 동시성 오류 정리

Cori1304·2025년 7월 16일
0

프로젝트 회고

목록 보기
1/2
post-thumbnail

📝 배경

구름 딥 다이브 프로젝트에서 스타벅스 앱 클론을 개발하던 중,
주문번호(order number) 중복 에러를 발견했습니다.

  • 천천히 1번씩 주문하면 문제 없음
  • 하지만 k6로 대량 주문 API 호출 시, 100번 중 96번 실패!
  • 대표 에러:
    [ERROR: duplicate key value violates unique constraint "orders_order_number_key”]

이 글은 문제 해결 과정을 기록한 블로그 포스트입니다.


🔢 주문번호 조건

  • 매장이 다르면 주문번호가 같아도 됨
  • 같은 매장, 같은 날짜에는 주문번호 중복 불가
  • 매일 주문번호는 1번부터 다시 시작
  • 고객과 매장은 동일 주문번호를 확인
  • A~Z 매장코드는 옵션, 실제 구현은 A로 통일
ID(PK)메뉴고객 주문번호매장ID(FK)
1[아메리카노 1잔, 라떼 2잔]A-10100
2[아메리카노 3잔, 모카 1잔]A-10101
3[자바칩 프라푸치노 2잔]B-33103

🚀 1차 프로젝트 구현

order_daily_counter 테이블의 카운터 값을 이용해 주문번호 생성

@Transactional
public String generateOrderNumber(OrderCreateRequest request) {
    LocalDate today = LocalDate.now();
    OrderDailyCounterId id = new OrderDailyCounterId(today, request.storeId());

    OrderDailyCounter counter = orderDailyCounterRepository.findById(id)
            .orElseGet(() -> new OrderDailyCounter(id, 0));

    counter.increment();
    orderDailyCounterRepository.save(counter);

    return "A-" + counter.getCount();
}

❓ 의문점

generateOrderNumber()에서 이미 저장을 하는데
왜 주문번호가 중복(동일)하게 나올까? 🧐


🛑 원인 분석

  1. 🟠 @Transactional은 트랜잭션의 원자성만 보장, 동시성 제어는 별도 필요
  2. 🟠 여러 트랜잭션이 동시에 같은 값을 읽고, 각각 증가 후 저장 → race condition 발생
  3. 🟠 JPA/Hibernate의 save/merge 방식은 동시성 문제에 취약


🛠️ 문제 해결 접근 단계

  1. 요구사항·제약 재정의
  2. 시스템 환경 및 기술 스택 확인
  3. 적용 가능한 동시성 제어 기법 조사

1️⃣ 요구사항·제약 재정의

  • 같은 매장, 같은 날짜에는 주문번호 중복 불가
  • 다른 매장끼리는 주문번호 중복 허용
  • 초당 100건 이상의 주문이 들어올 수 있음 (1000이상까지 목표)
  • order_daily_counter 테이블은 (date, store_id) 복합키 사용

2️⃣ 시스템 환경 및 기술 스택

  • 백엔드: Spring Boot, JPA(Hibernate)
  • 트랜잭션 관리: Spring의 @Transactional
  • DB: PostgreSQL
  • 운영환경: JVM 기반, Prometheus 지원

3️⃣ 적용 가능한 동시성 제어 기법

방법 (영문)요약장점단점
🟢 DB Atomic Operation(원자적 연산)UPDATE ... SET count = count + 1과 같이 DB 명령어를 활용구현 간단, DB가 동시성 제어DB 부하 집중, 트랜잭션 충돌 가능성
🔵 DB Sequence데이터베이스의 시퀀스 객체를 사용하여 고유값 생성고유값 보장, Race Condition 없음날짜별 리셋 등 비즈니스 로직과 다를 수 있음
🟣 Optimistic Lock(낙관적 락)@Version 필드로 버전 충돌 감지 후 재시도트래픽 낮으면 성능 유리, Deadlock 위험 낮음충돌 많으면 재시도 비용 증가
🟡 Pessimistic Lock(비관적 락)SELECT FOR UPDATE 구문으로 데이터 읽기 시점에 락을 선점충돌 발생률이 높을 때 데이터 무결성 확실히 보장락 대기로 인해 성능 저하, Deadlock 위험 높음
🟠 Redis INCR(증가 연산)Redis의 INCR (Atomic Increment) 명령 활용고성능, 확장성 우수Redis 장애 시 대체 로직 필요, 영속성 주의
🟤 DB Upsert(삽입/갱신)INSERT ... ON CONFLICT DO UPDATE와 같은 구문 활용단일 쿼리로 동시성 강함DB별 문법 상이, 트랜잭션 내 주의 필요

💡 결론

  • 단순 @Transactional + save/merge만으로는 동시성 문제 해결 불가
  • 반드시 DB atomic 연산, Optimistic Lock, Upsert 등 별도의 동시성 제어 필요!
  • 시스템 환경과 요구사항에 맞는 방법을 신중히 선택해야 함

실제 적용 예시, 해결법 상세 구현, 트러블슈팅 경험 등은 별도 포스트로 이어집니다!

profile
개발 공부 기록

0개의 댓글