1. κ°œμš”

λ„€νŠΈμ›Œν¬λŠ” μ–Έμ œλ‚˜ λΆˆμ•ˆμ •ν•˜λ©°, ν΄λΌμ΄μ–ΈνŠΈμ˜ μš”μ²­μ΄λ‚˜ μ„œλ²„μ˜ 응닡은 μ–Έμ œλ“ μ§€ μœ μ‹€λ  수 μžˆμŠ΅λ‹ˆλ‹€. ν΄λΌμ΄μ–ΈνŠΈκ°€ APIλ₯Ό ν˜ΈμΆœν–ˆλŠ”λ° νƒ€μž„μ•„μ›ƒμ΄ λ°œμƒν–ˆλ‹€λ©΄, ν΄λΌμ΄μ–ΈνŠΈλŠ” 이 μš”μ²­μ΄ μ„œλ²„μ—μ„œ μ²˜λ¦¬λ˜μ—ˆλŠ”μ§€ μ•Œ 길이 μ—†μ–΄ **μž¬μ‹œλ„(Retry)**λ₯Ό ν•˜κ²Œ λ©λ‹ˆλ‹€. μ΄λ•Œ μ„œλ²„κ°€ 같은 μš”μ²­μ„ 두 번 μ²˜λ¦¬ν•˜κ²Œ λ˜λ©΄μ„œ λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆλŠ”λ°, 이λ₯Ό λ°©μ§€ν•˜κΈ° μœ„ν•œ 핡심 κ°œλ…μ΄ λ°”λ‘œ **λ©±λ“±μ„±(Idempotency)**μž…λ‹ˆλ‹€.


2. λ©±λ“±μ„±(Idempotency)μ΄λž€?

μˆ˜ν•™μ΄λ‚˜ μ „μ‚°ν•™μ—μ„œ λ©±λ“±μ„±μ΄λž€ 연산을 μ—¬λŸ¬ 번 μ μš©ν•˜λ”λΌλ„ κ²°κ³Όκ°€ 달라지지 μ•ŠλŠ” μ„±μ§ˆμ„ μ˜λ―Έν•©λ‹ˆλ‹€. 즉, f(f(x)) = f(x) κ°€ μ„±λ¦½ν•˜λŠ” κ²½μš°μž…λ‹ˆλ‹€.

μ„œλ²„ API κ΄€μ μ—μ„œ 보면, **β€œλ™μΌν•œ μš”μ²­μ„ ν•œ 번 보내든, 100번 보내든 μ„œλ²„μ˜ μƒνƒœ(λ°μ΄ν„°λ² μ΄μŠ€ λ“±)κ°€ λ™μΌν•˜κ²Œ μœ μ§€λ˜λŠ” μ„±μ§ˆβ€**을 λ§ν•©λ‹ˆλ‹€.

2-1. HTTP λ©”μ„œλ“œμ™€ λ©±λ“±μ„±

RESTful APIλ₯Ό 섀계할 λ•Œ HTTP λ©”μ„œλ“œλ³„λ‘œ λ©±λ“±μ„± μ—¬λΆ€κ°€ λͺ…ν™•νžˆ κ΅¬λΆ„λ©λ‹ˆλ‹€.

  • GET: 데이터λ₯Ό 쑰회만 ν•˜λ―€λ‘œ μ—¬λŸ¬ 번 ν˜ΈμΆœν•΄λ„ μ„œλ²„ μƒνƒœλŠ” λ³€ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. (멱등함 O)
  • PUT: νŠΉμ • λ¦¬μ†ŒμŠ€λ₯Ό ν†΅μ§Έλ‘œ κ΅μ²΄ν•©λ‹ˆλ‹€. 같은 λ°μ΄ν„°λ‘œ μ—¬λŸ¬ 번 ꡐ체해도 μ΅œμ’… μƒνƒœλŠ” λ˜‘κ°™μŠ΅λ‹ˆλ‹€. (멱등함 O)
  • DELETE: λ¦¬μ†ŒμŠ€λ₯Ό μ‚­μ œν•©λ‹ˆλ‹€. 이미 μ‚­μ œλœ λ¦¬μ†ŒμŠ€λ₯Ό λ‹€μ‹œ μ‚­μ œν•˜λ € 해도 μ΅œμ’…μ μœΌλ‘œ κ·Έ λ¦¬μ†ŒμŠ€κ°€ μ—†λ‹€λŠ” μƒνƒœλŠ” κ°™μŠ΅λ‹ˆλ‹€. (멱등함 O)
  • POST: μƒˆλ‘œμš΄ λ¦¬μ†ŒμŠ€λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. μ—¬λŸ¬ 번 ν˜ΈμΆœν•˜λ©΄ μ—¬λŸ¬ 개의 λ¦¬μ†ŒμŠ€κ°€ μƒμ„±λ©λ‹ˆλ‹€. (λ©±λ“±ν•˜μ§€ μ•ŠμŒ X)
  • PATCH: λ¦¬μ†ŒμŠ€μ˜ 일뢀λ₯Ό μˆ˜μ •ν•©λ‹ˆλ‹€. λ‘œμ§μ— 따라 λ©±λ“±ν•  μˆ˜λ„, 아닐 μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. (예: age = 20은 λ©±λ“±ν•˜μ§€λ§Œ, age = age + 1은 λ©±λ“±ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.)

3. μ™œ μ€‘μš”ν•œκ°€? (μž¬μ‹œλ„μ™€ 데이터 μ •ν•©μ„±)

μ£Όλ¬Έ 결제 μ‹œμŠ€ν…œμ΄λ‚˜ μ˜ˆμ•½ μ·¨μ†Œ μ‹œμŠ€ν…œμ„ 생각해 λ΄…μ‹œλ‹€.

  1. μ‚¬μš©μžκ°€ β€˜κ²°μ œβ€™ λ²„νŠΌμ„ λˆ„λ¦…λ‹ˆλ‹€. (POST /payments)
  2. μ„œλ²„λŠ” 결제λ₯Ό μ™„λ£Œν•˜κ³  κ³„μ’Œμ—μ„œ λˆμ„ μ°¨κ°ν•©λ‹ˆλ‹€.
  3. ν•˜μ§€λ§Œ μ„œλ²„κ°€ β€œκ²°μ œ μ™„λ£Œβ€ 응닡을 보내기 직전에 λ„€νŠΈμ›Œν¬κ°€ λŠμ–΄μ§‘λ‹ˆλ‹€.
  4. ν΄λΌμ΄μ–ΈνŠΈλŠ” μ—λŸ¬λ₯Ό 보고 λ‹€μ‹œ β€˜κ²°μ œβ€™ λ²„νŠΌμ„ λˆ„λ¦…λ‹ˆλ‹€(μž¬μ‹œλ„).
  5. μ„œλ²„λŠ” λ‹€μ‹œ 결제λ₯Ό μ²˜λ¦¬ν•˜μ—¬ 돈이 이쀑 μ°¨κ°λ©λ‹ˆλ‹€.

μ΄λŸ¬ν•œ λ”μ°ν•œ 상황을 막기 μœ„ν•΄ APIλŠ” 멱등성을 보μž₯ν•˜λ„λ‘ μ„€κ³„λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€.

sequenceDiagram
    participant C as Client
    participant S as Server
    participant DB as Database

    C->>S: 1. μ˜ˆμ•½ μ·¨μ†Œ μš”μ²­ (Retry 1)
    S->>DB: 2. μƒνƒœ λ³€κ²½ (CONFIRMED -> CANCELLED)
    DB-->>S: 3. 반영 μ™„λ£Œ
    S--xC: 4. 응닡 전솑 쀑 λ„€νŠΈμ›Œν¬ μœ μ‹€!
    
    Note over C: Timeout λ°œμƒ, μž¬μ‹œλ„ μˆ˜ν–‰
    C->>S: 5. μ˜ˆμ•½ μ·¨μ†Œ μš”μ²­ (Retry 2)
    S->>DB: 6. μƒνƒœ 쑰회 (이미 CANCELLED)
    S-->>C: 7. 200 OK (λ©±λ“±μ„± 보μž₯: μΆ”κ°€ λ³€κ²½ μ—†μŒ)

4. 멱등성을 κ΅¬ν˜„ν•˜λŠ” 방법

4-1. κ³ μœ ν•œ Idempotency Key μ‚¬μš©

κ°€μž₯ 보편적인 방법은 ν΄λΌμ΄μ–ΈνŠΈκ°€ μš”μ²­μ„ 보낼 λ•Œ 헀더에 κ³ μœ ν•œ Idempotency-Key (UUID λ“±)λ₯Ό ν¬ν•¨ν•˜μ—¬ λ³΄λ‚΄λŠ” κ²ƒμž…λ‹ˆλ‹€. μ„œλ²„λŠ” 이 ν‚€λ₯Ό Redisλ‚˜ DB에 μ €μž₯ν•΄ 두고, λ™μΌν•œ ν‚€λ‘œ μš”μ²­μ΄ λ“€μ–΄μ˜€λ©΄ λ‘œμ§μ„ μˆ˜ν–‰ν•˜μ§€ μ•Šκ³  이전에 μ„±κ³΅ν–ˆλ˜ 응닡을 κ·ΈλŒ€λ‘œ λ°˜ν™˜ν•©λ‹ˆλ‹€. Stripe와 같은 결제 API듀이 이 방식을 μ±„νƒν•©λ‹ˆλ‹€.

4-2. λ°μ΄ν„°λ² μ΄μŠ€ μƒνƒœ 체크 (Optimistic λ©±λ“±μ„±)

λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 λ‹¨μ—μ„œ μ—”ν‹°ν‹°μ˜ ν˜„μž¬ μƒνƒœλ₯Ό μ²΄ν¬ν•˜μ—¬ 멱등성을 보μž₯ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, μ·¨μ†Œ μš”μ²­μ΄ 듀어왔을 λ•Œ λŒ€μƒμ˜ μƒνƒœκ°€ 이미 CANCELLED라면 μ—λŸ¬λ₯Ό λ˜μ§€κ±°λ‚˜ 성곡(200 OK)을 λ°˜ν™˜ν•˜κ³  λ‘œμ§μ„ μ‘°κΈ° μ’…λ£Œ(Return)ν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€.

@Transactional
public void cancelBooking(String bookingId) {
    Booking booking = repository.findById(bookingId);
    
    // λ©±λ“±μ„± 보μž₯ 둜직
    if (booking.getStatus() == BookingStatus.CANCELLED) {
        return; // 이미 μ·¨μ†Œλ˜μ—ˆμœΌλ―€λ‘œ 아무 μž‘μ—…λ„ ν•˜μ§€ μ•ŠμŒ
    }
    
    booking.cancel();
}

μ•ˆμ „ν•˜κ³  κ²¬κ³ ν•œ λΆ„μ‚° μ‹œμŠ€ν…œμ„ κ΅¬μΆ•ν•˜κΈ° μœ„ν•΄ 멱등성은 선택이 μ•„λ‹Œ ν•„μˆ˜μž…λ‹ˆλ‹€.