[Spring] λ°μ½”λ ˆμ΄ν„° νŒ¨ν„΄μ„ ν†΅ν•œ λ™μ‹œμ„± 문제 ν•΄κ²° 및 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직과 λ™μ‹œμ„± κ΄€λ ¨ 둜직 κ°„μ˜ 관심사 λΆ„λ¦¬ν•˜κΈ°

2024. 4. 22. 18:57ㆍBackend/Spring

κ°œμš”

ν˜„μž¬ 지인 3λΆ„κ³Ό ν•¨κ»˜ λ™μ‹œμ„± λ¬Έμ œμ— λŒ€ν•œ κ²½ν—˜μ„ μŒ“κΈ° μœ„ν•΄ ν‹°μΌ€νŒ… μ„œλΉ„μŠ€λ₯Ό κ°œλ°œν•˜λŠ” ν”„λ‘œμ νŠΈλ₯Ό μ§„ν–‰ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

 

GitHub - Tiketeer/Tiketeer-BE

Contribute to Tiketeer/Tiketeer-BE development by creating an account on GitHub.

github.com

ν•΄λ‹Ή ν”„λ‘œμ νŠΈλ₯Ό μ‹œμž‘ν•˜λ©΄μ„œ λ‹¬μ„±ν•˜κ³ μž ν–ˆλ˜ 핡심 λͺ©ν‘œ 쀑 ν•˜λ‚˜λŠ”, λ™μ‹œμ„± 문제λ₯Ό 직접 κ²ͺ어보고 이λ₯Ό ν•΄κ²°ν•˜λŠ” 방법둠에 λŒ€ν•œ ν•™μŠ΅κ³Ό 이λ₯Ό μ‹€μ œλ‘œ ν•΄κ²°ν•΄λ³Έ κ²½ν—˜μ„ κ°–λŠ” κ²ƒμ΄μ—ˆμŠ΅λ‹ˆλ‹€.

저희 ν”„λ‘œμ νŠΈ λ‚΄μ—μ„œ λ™μ‹œμ„± λ¬Έμ œκ°€ λ°œμƒν•˜λŠ” 지점은 λ°”λ‘œ ν‹°μΌ€νŒ…μ„ μˆ˜ν–‰ν•˜λŠ” λ‘œμ§μž…λ‹ˆλ‹€. μ„œλ²„ μƒμ—λŠ” ν•œμ •λœ 수의 티켓이 μ‘΄μž¬ν•˜κ²Œ λ˜λŠ”λ°, 이에 λŒ€ν•΄ 티켓보닀 더 λ§Žμ€ 고객이 λ™μ‹œλ‹€λ°œμ μœΌλ‘œ 티켓을 νšλ“ν•˜λ €λŠ” κ³Όμ •μ—μ„œ 데이터 μ •ν•©μ„± λ¬Έμ œκ°€ λ°œμƒν•˜λŠ” 것이죠. 저희 νŒ€μ€ 이 문제λ₯Ό λ‹€μ–‘ν•œ λ°©λ²•μœΌλ‘œ 접근해보기 μœ„ν•΄ 각자 비관적 락(P-lock), 낙관적 락(O-lock), λΆ„μ‚° 락(D-lock)을 μ΄μš©ν•˜μ—¬ 문제λ₯Ό ν•΄κ²°ν•΄λ³΄λŠ” 과정을 κ±°μ³€μŠ΅λ‹ˆλ‹€.

그리고 μ΅œμ’…μ μœΌλ‘œ 저희 상황에 κ°€μž₯ λΆ€ν•©ν•˜λŠ” ν•˜λ‚˜μ˜ 방법둠을 ν™•μ •ν•˜κΈ° μœ„ν•œ ν…ŒμŠ€νŠΈ 및 κ²°κ³Ό 뢄석 단계λ₯Ό μ•žλ‘κ³  μžˆμŠ΅λ‹ˆλ‹€.

μ €ν¬λŠ” μžλ™ν™”λœ ν…ŒμŠ€νŠΈλ₯Ό μ›ν–ˆκ³ , 이λ₯Ό μœ„ν•΄ 좔가적인 κ°œλ³„ μ½”λ“œ μˆ˜μ • 없이 μ½”λ“œ μ»¨ν…μŠ€νŠΈ μ™ΈλΆ€μ—μ„œ μ–΄λ–€ 방법둠을 μ‚¬μš©ν•˜μ—¬ 락을 μˆ˜ν–‰ν• μ§€ κ²°μ •ν•˜κ³ , μ£Όμž…ν•΄μ£ΌκΈ°λ₯Ό μ›ν–ˆμŠ΅λ‹ˆλ‹€.

μ΄λŸ¬ν•œ μš”κ΅¬μ‚¬ν•­μ€ κ²°κ΅­ 객체지ν–₯ 섀계 μ›μΉ™μ˜ ‘개방-폐쇄 원칙’에 ν•΄λ‹Ήλ˜λŠ” 문제이고, λ”°λΌμ„œ ν™•μž₯ κ°€λŠ₯ν•œ 객체지ν–₯ 섀계가 ν•„μš”ν•œ μƒν™©μ΄μ—ˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό μœ„ν•΄ 좔가적인 아킀텍쳐 λ―ΈνŒ…μ„ μ§„ν–‰ν•˜κ³  κ²°μ •λœ 아킀텍쳐에 맞게 각자 λ‘œμ§μ„ κ΅¬μ„±ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

ν•˜μ§€λ§Œ 이 κ³Όμ •μ—μ„œ λ‹€μ–‘ν•œ λ¬Έμ œλ“€μ΄ λ°œμƒν•˜μ—¬ μ΅œμ’…μ μœΌλ‘œλŠ” 초기 λ…Όμ˜ν–ˆλ˜ μ•„ν‚€ν…μ³μ™€λŠ” 사뭇 λ‹€λ₯Έ λ°©μ‹μœΌλ‘œ 개발이 μ§„ν–‰λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

ν•΄λ‹Ή κΈ€μ—μ„œλŠ” 개발 쀑에 κ²ͺ은 λ‹€μ–‘ν•œ λ¬Έμ œμ λ“€κ³Ό λ”λΆˆμ–΄ λ¬Έμ œκ°€ μ™œ λ°œμƒν–ˆλŠ”μ§€μ— λŒ€ν•œ 고찰을 μ§„ν–‰ν•˜κ³ , μ΅œμ’…μ μœΌλ‘œλŠ” 문제 상황듀을 κ³ λ €ν•œ μƒˆλ‘œμš΄ 아킀텍쳐 κ°œμ„ μ•ˆμ„ λ„μΆœν•΄λ‚΄κ³ μž ν•©λ‹ˆλ‹€.

 

λ“€μ–΄κ°€κΈ° μ•žμ„œ

κΈ€ λ‚΄μš©μ— λŒ€ν•΄ μ‰½κ²Œ μ΄ν•΄ν•˜κΈ° μœ„ν•΄μ„œλŠ” 락을 κ΅¬ν˜„ν•˜λŠ” λ°©λ²•λ‘ λ“€μ˜ μ’…λ₯˜μ™€ κ·Έ νŠΉμ§•μ„ μ•Œκ³  μžˆλŠ” 편이 μ’‹μŠ΅λ‹ˆλ‹€.

이λ₯Ό μœ„ν•΄ 각 방법둠에 λŒ€ν•΄ κ°„λ‹¨νžˆ 닀루면 μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

  • O-lock
    • 낙관적 락이라고 μΌμ»¬μ–΄μ§€λŠ” λ°©μ‹μž…λ‹ˆλ‹€.
    • ν…Œμ΄λΈ” λ‚΄ 버전 μΉΌλŸΌμ„ μΆ”κ°€ν•˜κ³  ν•΄λ‹Ή λ ˆμ½”λ“œκ°€ μˆ˜μ •λ  λ•Œλ§ˆλ‹€ 이 값을 ν•¨κ»˜ μˆ˜μ •ν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€.
      • 버전 칼럼의 경우, λ‹¨μˆœ Integer ν˜•μ‹μ„ μ‚¬μš©ν•  μˆ˜λ„, ν˜Ήμ€ μˆ˜μ •λœ μˆœκ°„μ„ κΈ°λ‘ν•˜λŠ” Timestamp ν˜•μ‹μ„ μ‚¬μš©ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.
      • 일반적으둜 JPAμ—μ„œ μ œκ³΅ν•˜λŠ” @Version μ–΄λ…Έν…Œμ΄μ…˜μ„ 톡해 κ°„λ‹¨νžˆ μ„€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
    • 둜직 λ‚΄μ—μ„œ μ—…λ°μ΄νŠΈλ₯Ό μœ„ν•΄ μ½μ–΄μ˜¨ 값을 DB둜 λ‹€μ‹œ λ™κΈ°ν™”ν•˜λŠ” κ³Όμ •μ—μ„œ 초기 μ½μ–΄μ™”λ˜ 버전 κ°’κ³Ό λ™κΈ°ν™”ν•˜λŠ” μˆœκ°„μ˜ 버전 값이 λ‹€λ₯Ό 경우 λ°œμƒν•˜λŠ” μ˜ˆμ™Έλ₯Ό λ”°λ‘œ μ²˜λ¦¬ν•˜λŠ” μ‹μœΌλ‘œ κ΅¬ν˜„ν•©λ‹ˆλ‹€.
      • μ‹€ν–‰ 쿼리 μ˜ˆμ‹œ
      • UPDATE users SET name = "Jamal", version = version + 1 WHERE id = 10 and version = ?;
    • μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ ˆλ²¨μ—μ„œ λ™μ‹œμ„±μ— λŒ€ν•΄ 닀루지 μ•Šκ³ , 일단 쿼리λ₯Ό λ‚ λ¦° λ’€ μ˜ˆμ™Έκ°€ λ°œμƒν•  경우 κ·Έμ œμ„œμ•Ό λ™μ‹œμ„± 핸듀링을 ν•˜λŠ” λ°©μ‹μœΌλ‘œ 쑰회 μ‹œ μ–΄λ– ν•œ 락도 μ‚¬μš©ν•˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ— μ„±λŠ₯ μ €ν•˜κ°€ μ—†μ§€λ§Œ λ§Œμ•½ λ™μ‹œμ„± λ¬Έμ œκ°€ λ°œμƒν•œ 상황이라면 λ„€νŠΈμ›Œν¬ λΉ„μš© + μ˜ˆμ™Έ 처리 λΉ„μš© + μž¬μš”μ²­ λΉ„μš©μ΄ μΆ”κ°€λ˜μ–΄ μ„±λŠ₯적으둜 큰 μ €ν•˜λ₯Ό κ²ͺ을 수 μžˆμŠ΅λ‹ˆλ‹€.
  • P-lock
    • 비관적 락이라고 μΌμ»¬μ–΄μ§€λŠ” λ°©μ‹μž…λ‹ˆλ‹€.
    • 데이터λ₯Ό μ½μ–΄μ˜¬ λ•Œ SELECT ~ FOR UPDATE 문을 μ΄μš©ν•˜μ—¬, νƒ€κ²Ÿ λ ˆμ½”λ“œμ— 배타락을 κ±°λŠ” λ°©μ‹μœΌλ‘œ λ™μž‘ν•©λ‹ˆλ‹€.
    • 일반적인 νŠΈλžœμž­μ…˜ κ²©λ¦¬μˆ˜μ€€(READ COMMITED, REPEATABLE READ)μ—μ„œλŠ” MVCC둜 인해 λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ—μ„œ 배타락이 κ±Έλ¦° λ ˆμ½”λ“œλ„ 읽을 수 μžˆμœΌλ‚˜(λ‹¨μˆœ 쑰회), 데이터 μˆ˜μ •μ΄λ‚˜ ν˜Ήμ€ λ‹€λ₯Έ SELECT ~ FOR UPDATE 문을 ν†΅ν•΄μ„œλŠ” λ™μ‹œ 접근이 λΆˆκ°€λŠ₯ν•΄μ§‘λ‹ˆλ‹€. 이둜 인해 μ„±λŠ₯의 μ €ν•˜κ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • D-lock
    • λΆ„μ‚° 락이라고 μΌμ»¬μ–΄μ§€λŠ” λ°©μ‹μž…λ‹ˆλ‹€.
    • ν•΄λ‹Ή 방식은 RDB, μ„œλ²„ μΈμŠ€ν„΄μŠ€, μŠ€λ ˆλ“œ 등에 쒅속적이지 μ•Šκ³  μ˜€λ‘œμ§€ 락을 μ €μž₯ν•  μ™ΈλΆ€ μ €μž₯μ†Œμ—λ§Œ μ˜μ‘΄μ„ ν•©λ‹ˆλ‹€. 이둜 인해 μ„œλ²„, RDB λ“±μ˜ μŠ€μΌ€μΌμ•„μ›ƒμœΌλ‘œ μΈν•œ λΆ„μ‚° ν™˜κ²½μ—μ„œλ„ 자유둭게 ν™œμš©μ΄ κ°€λŠ₯ν•©λ‹ˆλ‹€.
    • 일반적으둜 인메λͺ¨λ¦¬ DB인 λ ˆλ””μŠ€λ₯Ό 락 μ €μž₯μ†Œλ‘œ ν™œμš©ν•©λ‹ˆλ‹€.

 

κΈ°μ‘΄ 아킀텍쳐

μœ„ UML은 초기 λ―ΈνŒ…μ„ 톡해 κ΅¬μ„±ν•œ 일뢀 아킀텍쳐에 λŒ€ν•œ λ‚΄μš©μž…λ‹ˆλ‹€.

CreatePurchaseUseCase ν΄λž˜μŠ€μ— μ‹€μ§ˆμ μΈ ν‹°μΌ“ ꡬ맀에 λŒ€ν•œ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 μž‘μ„±λ˜μ–΄ μžˆλŠ”λ°, 이 λ•Œ λ™μ‹œμ„± λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆλŠ” μœ μ € - ν‹°μΌ“ λ°°μ • μž‘μ—…μ€ λ™μ‹œμ„±μ„ λ‹€λ£¨λŠ” TicketConcurrencyService에 μœ„μž„ν•˜μ—¬ μˆ˜ν–‰ν•˜λŠ” ꡬ쑰둜 μž‘μ„±λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.

μœ„ κ΅¬μ‘°μ—μ„œ νŠΈλžœμž­μ…˜ μ‹œμž‘ μ§€μ κΉŒμ§€ ν•¨κ»˜ ν‘œμ‹œν•˜λ©΄ μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

 

문제 λ°œμƒ

μœ„ 아킀텍쳐λ₯Ό 기반으둜 TicketConcurrencyService μΈν„°νŽ˜μ΄μŠ€μ˜ κ΅¬ν˜„μ²΄λ₯Ό νŒ€μ› λΆ„λ“€κ»˜μ„œ ν•˜λ‚˜μ”© λ‹΄λ‹Ήν•˜μ—¬ κ΅¬ν˜„ν•΄μ£Όμ…¨μŠ΅λ‹ˆλ‹€.

그리고 μ‹€μ œ ν…ŒμŠ€νŠΈλ₯Ό μ§„ν–‰ν•΄λ³΄λ‹ˆ, 이 쀑 DB에 μ’…μ†λ˜λŠ” λ°©μ‹μœΌλ‘œ κ΅¬ν˜„λ˜λŠ” 비관적 락을 μ œμ™Έν•œ λ‚˜λ¨Έμ§€ 두 방법이 μœ„ κ΅¬μ‘°μ—μ„œ 문제λ₯Ό λ°œμƒμ‹œμΌ°μŠ΅λ‹ˆλ‹€.

μ €λŠ” 이 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ ν…ŒμŠ€νŠΈ μ‹€ν–‰ μ‹œ λ°œμƒν•˜λŠ” λ‘œκ·Έλ“€μ„ 일일이 따라가며 디버깅을 μ§„ν–‰ν•˜μ˜€κ³  μ΅œμ’…μ μœΌλ‘œλŠ” λ¬Έμ œκ°€ λ°œμƒν•˜κ³  μžˆλŠ” 뢀뢄듀을 확인할 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

μ‹€μ œ κ΅¬ν˜„μ— λŒ€ν•œ κ°„λž΅ν•œ μ„€λͺ…κ³Ό λ°œμƒν–ˆλ˜ 문제λ₯Ό μ„€λͺ…ν•˜λ©΄ μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

  • O-lock (낙관적 락)
    • κ΅¬ν˜„
      • CreatePurchaseUseCaseμ—μ„œ μ „νŒŒλœ νŠΈλžœμž­μ…˜μ— TicketOptimisticLockConcurrencyService.assignPurchaseToTicket λ©”μ„œλ“œκ°€ μ°Έμ—¬ν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€.
      • 이 λ•Œ 낙관적 락 λ°©μ‹μ—μ„œ λ°œμƒν•˜λŠ” μ˜ˆμ™Έλ₯Ό assignPurchaseToTicket λ©”μ„œλ“œ λ‚΄μ—μ„œ try - catch둜 μž‘μ•„λ‚΄κ³  @Retryable μ–΄λ…Έν…Œμ΄μ…˜μ„ 톡해 FailOverλ₯Ό μˆ˜ν–‰ν•˜λŠ” λ°©μ‹μœΌλ‘œ κ΅¬ν˜„μ„ μ§„ν–‰ν–ˆμŠ΅λ‹ˆλ‹€.
      @Retryable(
      	retryFor = OptimisticLockingFailureException.class,
      	backoff = @Backoff(delay = 100),
      	maxAttempts = 100
      )
      public void assignPurchaseToTicket(UUID ticketingId, UUID purchaseId, int ticketCount) throws
      	ConcurrencyFailureException {
      	var purchase = purchaseCrudService.findById(purchaseId);
      	var tickets = ticketRepository.findByTicketingIdAndPurchaseIsNullOrderByIdWithOptimisticLock(
      		ticketingId, Limit.of(ticketCount));
      
      	if (tickets.size() < ticketCount) {
      		throw new NotEnoughTicketException();
      	}
      
      	tickets.forEach(ticket -> {
      		ticket.setPurchase(purchase);
      	});
      	ticketRepository.flush();
      }
    • 문제
      • 첫번째둜, REPEATABLE READ 고립 λ ˆλ²¨μ—μ„œλŠ” 정상 λ™μž‘ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. κ΅¬μ²΄μ μœΌλ‘œλŠ” ν•΄λ‹Ή λ©”μ„œλ“œμ—μ„œ FailOverκ°€ λ˜λ”λΌλ„ μƒμœ„ νŠΈλžœμž­μ…˜μ΄ λ‘€λ°±λ˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ— 처음 μ½μ—ˆλ˜ 버전이 λ³€κ²½λ˜μ§€ μ•ŠλŠ”λ°, κ·Έλ ‡κΈ° λ•Œλ¬Έμ— ν•œλ²ˆ μ‹€νŒ¨ν•  경우 λͺ¨λ“  μž¬μ‹œλ„ νšŸμˆ˜κ°€ μ „λΆ€ μ†Œλͺ¨λ˜λ”라도 κ³„μ†ν•΄μ„œ OptimisticLockException이 λ°œμƒν•©λ‹ˆλ‹€.
      • λ‘λ²ˆμ§Έλ‘œ, OptimisticLockException은 RuntimeException의 일쒅이기 λ•Œλ¬Έμ— ν˜„μž¬ νŠΈλžœμž­μ…˜μ— rollbackOnly λ§ˆν‚Ήμ΄ λ˜λŠ”λ°, 이 κ²½μš°μ—” 정상 컀밋이 λ˜λŠ” μƒν™©μ—μ„œλ„ λͺ…μ‹œμ μΈ 둀백이 μ‹€ν–‰λ©λ‹ˆλ‹€. λ”°λΌμ„œ ν•œλ²ˆμ΄λΌλ„ μ‹€νŒ¨ν•œ 경우 μž¬μ‹œλ„μ™€λŠ” 관련없이 μ΅œμ’…μ μΈ κ²°κ³ΌλŠ” 항상 Rollback이 λ˜λŠ” λ¬Έμ œκ°€ μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
  • D-lock (λΆ„μ‚° 락)
    • κ΅¬ν˜„
      • CreatePurchaseUseCaseμ—μ„œ μ „νŒŒλœ νŠΈλžœμž­μ…˜μ— TicketDistributedLockConcurrencyService.assignPurchaseToTicket λ©”μ„œλ“œκ°€ μ°Έμ—¬ν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€.
      • 이 λ•Œ 낙관적 락 λ°©μ‹μ—μ„œ λ°œμƒν•˜λŠ” μ˜ˆμ™Έλ₯Ό assignPurchaseToTicket λ©”μ„œλ“œ λ‚΄μ—μ„œ try - catch둜 μž‘μ•„λ‚΄κ³  @Retryable μ–΄λ…Έν…Œμ΄μ…˜μ„ 톡해 FailOverλ₯Ό μˆ˜ν–‰ν•˜λŠ” λ°©μ‹μœΌλ‘œ κ΅¬ν˜„μ„ μ§„ν–‰ν–ˆμŠ΅λ‹ˆλ‹€.
      public void assignPurchaseToTicket(UUID ticketingId, UUID purchaseId, int ticketCount) {
      	String lockName = ticketingId.toString();
      	RLock rLock = redissonClient.getLock(lockName);
      
      	final long waitTime = 10L;
      	final long leaseTime = 3L;
      	TimeUnit timeUnit = TimeUnit.SECONDS;
      	try {
      		boolean available = rLock.tryLock(waitTime, leaseTime, timeUnit);
      
      		if (!available) {
      			throw new TicketConcurrencyException();
      		}
      
      		var purchase = purchaseCrudService.findById(purchaseId);
      		var tickets = ticketRepository.findByTicketingIdAndPurchaseIsNullOrderById(
      			ticketingId, Limit.of(ticketCount));
      
      		if (tickets.size() < ticketCount) {
      			throw new NotEnoughTicketException();
      		}
      
      		tickets.forEach(ticket -> {
      			ticket.setPurchase(purchase);
      		});
      
      	} catch (InterruptedException e) {
      		throw new TicketConcurrencyException();
      	} finally {
      		TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
      			public void afterCompletion(int status) {
      				rLock.unlock();
      			}
      		});
      	}
      }
    • 문제
      • μ΄λ²ˆμ—λ„ REPEATABLE READ 고립 레벨이 λ¬Έμ œκ°€ λ˜μ—ˆμŠ΅λ‹ˆλ‹€. ꡬ체적으둜 μœ„μ˜ assignPurchaseToTicket λ©”μ„œλ“œλ₯Ό μ§λ ¬ν™”ν•œλ‹€κ³  해도 이미 κ·Έ μƒμœ„ λ©”μ„œλ“œμΈ CreatePurchaseUseCase.createPurchaseμ—μ„œ 이미 μ‘°νšŒκ°€ μΌμ–΄λ‚˜κΈ° λ•Œλ¬Έμ— 직렬화 직전에 μŠ€λƒ…μƒ·μ΄ μƒμ„±λ˜κ²Œ 되고 이둜 인해 μ§λ ¬ν™”λœ λ©”μ„œλ“œ λ‚΄μ—μ„œλ„ λͺ¨λ‘ λ™μΌν•œ λ²„μ „μ˜ 티켓듀을 μ‘°νšŒν•˜κ²Œ λ©λ‹ˆλ‹€. λ”°λΌμ„œ λ™μ‹œμ„± λ¬Έμ œκ°€ ν•΄κ²°λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

 

문제 ν•΄κ²° λ°©μ•ˆ

낙관적 락 λ°©μ‹μ—μ„œ λ°œμƒν•˜λŠ” λ¬Έμ œλŠ” κ²°κ΅­ Retryable을 톡해 μ™„μ „νžˆ μƒˆλ‘œμš΄ νŠΈλžœμž­μ…˜μ„ μ‹œμž‘μ‹œμΌœμ•Ό ν•΄κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ 곡유 락 λ°©μ‹μ—μ„œ λ°œμƒν•˜λŠ” λ¬Έμ œλŠ” μ΅œμƒμœ„ νŠΈλžœμž­μ…˜μ—μ„œ 첫번째 μ‘°νšŒκ°€ λ°œμƒν•˜κΈ° 전에 곡유 락 λ‘œμ§μ„ 감싸야 해결이 κ°€λŠ₯ν•©λ‹ˆλ‹€.

즉, 두 방식 λͺ¨λ‘ μ΅œμƒμœ„ νŠΈλžœμž­μ…˜μ— κ΄€λ ¨ 둜직이 λ™μž‘ν•˜λ„λ‘ μ½”λ“œλ₯Ό μˆ˜μ •ν•˜λ©΄ 해결이 λ˜λŠ” 문제인 κ²ƒμž…λ‹ˆλ‹€.

λ”°λΌμ„œ 둜직의 톡일성을 μœ„ν•΄ 락에 λŒ€ν•œ ν™•μž₯ 포인트λ₯Ό TicketConcurrencyService λ ˆλ²¨μ—μ„œ μž‘λŠ” 것이 μ•„λ‹Œ, CreatePurchaseUseCase λ ˆλ²¨μ—μ„œ μž‘λŠ” λ°©ν–₯으둜 μ½”λ“œλ₯Ό μˆ˜μ •ν•˜λŠ” 결둠을 λƒˆμŠ΅λ‹ˆλ‹€.

 

문제 ν•΄κ²° λ°©μ•ˆ 심화

κ°€μž₯ λ‹¨μˆœν•œ 해결책은 μ•„λ¬΄λž˜λ„ CreatePurchaseUseCaseλ₯Ό ꡬ체 ν΄λž˜μŠ€κ°€ μ•„λ‹Œ μΈν„°νŽ˜μ΄μŠ€λ‘œ λ³€κ²½ν•˜κ³  이듀에 각각 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직과 λ™μ‹œμ„± μ œμ–΄ λ‘œμ§μ„ ν•¨κ»˜ μΆ”κ°€ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€.

ν•˜μ§€λ§Œ 개인적으둜 이 방식은 CreatePurchaseUseCase μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜μ—¬ μƒˆλ‘œ 생겨날 3개의 ꡬ체 ν΄λž˜μŠ€μ— 쀑볡 둜직이 계속 λ°œμƒν•  것이라 μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€. CreatePurchaseUseCase λ‚΄μ—λŠ” λ™μ‹œμ„± μ œμ–΄ λ‘œμ§λ„ μžˆμ§€λ§Œ, λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§λ„ μ‘΄μž¬ν•˜λŠ” μƒν™©μž…λ‹ˆλ‹€.

// ν‹°μΌ“ ꡬ맀 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직
public CreatePurchaseResultDto createPurchase(CreatePurchaseCommandDto command) {
	var ticketingId = command.getTicketingId();
	var count = command.getCount();

	var member = memberCrudService.findByEmail(command.getMemberEmail());

	purchaseService.validateTicketingSalePeriod(ticketingId, command.getCommandCreatedAt());

	var ticketing = ticketingService.findById(ticketingId);

	memberPointService.subtractPoint(member.getId(), ticketing.getPrice() * count);

	var purchase = purchaseRepository.save(Purchase.builder().member(member).build());

	// #1 락 μ’…λ₯˜μ— 따라 λ‹€λ₯Έ λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•΄μ•Ό 함 
	var tickets = ticketRepository.findByTicketingIdAndPurchaseIsNullOrderById(ticketingId, Limit.of(count));

	if (tickets.size() < count) {
		throw new NotEnoughTicketException();
	}

	tickets.forEach(ticket -> {
		ticket.setPurchase(purchase);
	});

	return CreatePurchaseResultDto.builder()
		.purchaseId(purchase.getId())
		.createdAt(purchase.getCreatedAt())
		.build();
}

κ³Όμ—° 이 λ‘œμ§λ“€μ΄ μ–΄λ–€ 락을 μ‚¬μš©ν•˜λŠλƒμ— 따라 λ°”λ€Œμ–΄μ•Ό ν•  μ΄μœ κ°€ μžˆμ„κΉŒμš”? 개인적으둜 λ‚΄λ¦° 결둠은 “λΆˆν•„μš”ν•˜λ‹€.”μž…λ‹ˆλ‹€. 이에, μ•„λž˜μ˜ μš”κ΅¬μ‚¬ν•­λ“€μ„ μ •μ˜ν•˜κ³  μ–΄λ–€ 아킀텍쳐λ₯Ό κ°€μ Έκ°€λ©΄ 쒋을지 고민을 μ‹œμž‘ν–ˆμŠ΅λ‹ˆλ‹€.

  • 낙관적 락 둜직
    • 이전에 λ°œμƒν–ˆλ˜ 문제의 해결을 μœ„ν•΄, Retryable이 Transaction 생λͺ…μ£ΌκΈ°λ₯Ό μ™„μ „νžˆ κ°μ‹ΈλŠ” ν˜•νƒœκ°€ λ˜μ–΄μ•Ό 함.
    • μœ„ 둜직의 #1μ—μ„œ 낙관적 락 μ „μš© TicketRepository.findByTicketingIdAndPurchaseIsNullOrderByIdWithOptimisticLock λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•΄μ•Ό 함.
  • 비관적 락 둜직
    • μœ„ 둜직의 #1μ—μ„œ 비관적 락 μ „μš© TicketRepository.findByTicketingIdAndPurchaseIsNullOrderByIdWithPessimisticLock λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•΄μ•Ό 함.
  • 곡유 락 둜직
    • Redisson을 ν†΅ν•œ 곡유 락 νšλ“ - λ°˜ν™˜ 흐름이 Transaction 생λͺ…μ£ΌκΈ°λ₯Ό μ™„μ „νžˆ κ°μ‹ΈλŠ” ν˜•νƒœκ°€ λ˜μ–΄μ•Ό 함.
    • μœ„ 둜직의 #1μ—μ„œ 락이 μ μš©λ˜μ§€ μ•Šμ€ TicketRepository.findByTicketingIdAndPurchaseIsNullOrderById λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•΄μ•Ό 함.

μœ„ μš”κ΅¬μ‚¬ν•­μ„ μ’…ν•©ν•˜λ©΄, 1) ν™•μž₯ 포인트인 λ™μ‹œμ„± μ œμ–΄ 둜직이 λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ 감싸야 ν•˜κ³ , 2) λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 내에 μ–΄λ–»κ²Œ 후보 티켓을 κ°€μ Έμ˜¬μ§€μ— λŒ€ν•œ 방법둠을 μ£Όμž…ν•΄μ€„ 수 μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€.

이λ₯Ό 톡해 μ €λŠ” μš”κ΅¬μ‚¬ν•­μ„ λ§Œμ‘±μ‹œν‚€κΈ° μœ„ν•œ λ°©μ•ˆμœΌλ‘œ 객체지ν–₯ λ””μžμΈ νŒ¨ν„΄μ˜ λ°μ½”λ ˆμ΄ν„° νŒ¨ν„΄μ„ λ– μ˜¬λ ΈμŠ΅λ‹ˆλ‹€.

μ•„λž˜λŠ” λ°μ½”λ ˆμ΄ν„° νŒ¨ν„΄μ˜ 이해λ₯Ό 돕기 μœ„ν•œ 아킀텍쳐 μ˜ˆμ‹œμž…λ‹ˆλ‹€.

https://sourcemaking.com/design_patterns/decorator

 

이λ₯Ό ν†΅ν•˜λ©΄ μ½”μ–΄ λ‘œμ§μ€ κ·ΈλŒ€λ‘œ 내버렀 λ‘” 채 μ½”μ–΄ λ‘œμ§μ„ κ°μ‹ΈλŠ” 일련의 μž‘μ—…(λ™μ‹œμ„± μ œμ–΄)듀에 λŒ€ν•œ ν™•μž₯을 νŽΈν•˜κ²Œ κ°€λŠ₯ν•©λ‹ˆλ‹€. λ˜ν•œ λ°μ½”λ ˆμ΄ν„° λ ˆμ΄μ–΄μ—μ„œ μ½”μ–΄ λ‘œμ§μ„ ν˜ΈμΆœν•  λ•Œ λ©”μ„œλ“œ 인자둜 후보 티켓듀을 μ‘°νšŒν•˜λŠ” 방법둠에 λŒ€ν•œ ν•¨μˆ˜ν˜• μΈν„°νŽ˜μ΄μŠ€λ₯Ό μ „λ‹¬ν•˜λŠ” DIλ₯Ό 톡해 락에 따라 λ‹€λ₯Έ ν‹°μΌ“ 쑰회 방법둠에 λŒ€ν•œ μš”κ΅¬μ‚¬ν•­μ„ λ§Œμ‘±μ‹œν‚€κ³ μž ν–ˆμŠ΅λ‹ˆλ‹€.

 

μˆ˜μ • μ•„ν‚€ν…μ²˜

μˆ˜μ •λœ μ•„ν‚€ν…μ²˜λŠ” μœ„μ™€ κ°™μŠ΅λ‹ˆλ‹€.

κ°„λž΅νžˆ μ„€λͺ…을 λ§λΆ™μ΄μžλ©΄, CreatePurchaseUseCaseCore 내에 메인 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 ν¬ν•¨λ˜κ³  이λ₯Ό κ°μ‹ΈλŠ” λ°μ½”λ ˆμ΄ν„° 객체인 CreatePurchaseUseCaseOLock, CreatePurchaseUseCasePLock, CreatePurchaseUseCaseDLock이 각각 μ‘΄μž¬ν•©λ‹ˆλ‹€. λ˜ν•œ 각각의 λ™μ‹œμ„± μ œμ–΄ λ°©λ²•λ‘ μ—μ„œ μ‚¬μš©λ˜μ–΄μ•Ό ν•  TicketRepository λ©”μ„œλ“œλ₯Ό μ£Όμž…ν•΄μ£ΌκΈ° μœ„ν•΄ ListTicketStrategyλΌλŠ” ν•¨μˆ˜ν˜• μΈν„°νŽ˜μ΄μŠ€λ₯Ό μƒμ„±ν•˜κ³  각각의 λ°μ½”λ ˆμ΄ν„°μ—μ„œ μ½”μ–΄ λ‘œμ§μ„ ν˜ΈμΆœν•  λ•Œ 직접 μ£Όμž…ν•΄μ£ΌλŠ” λ°©μ‹μœΌλ‘œ 둜직이 μž‘μ„±λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.

ꡬ체적인 μ½”λ“œλŠ” μ•„λž˜ 링크λ₯Ό 톡해 확인이 κ°€λŠ₯ν•©λ‹ˆλ‹€.

 

Tiketeer-BE/src/main/java/com/tiketeer/Tiketeer/domain/purchase/usecase/decorator at develop · Tiketeer/Tiketeer-BE

Contribute to Tiketeer/Tiketeer-BE development by creating an account on GitHub.

github.com

 

κ²°λ‘ 

λ¦¬νŒ©ν† λ§ ν›„ μ½”λ“œ 라인 μˆ˜κ°€ 많이 쀄어든 λͺ¨μŠ΅

기쑴에 λ™μ‹œμ„± 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ κ΅¬μΆ•ν•œ CreatePurchaseUseCase - TicketConcurrency κ΅¬μ‘°μ—μ„œ λ°œμƒν•˜λŠ” 좔가적인 λ™μ‹œμ„± μ΄μŠˆμ— λŒ€ν•œ 핸듀링은 λ¬Όλ‘ , λ°μ½”λ ˆμ΄ν„° νŒ¨ν„΄μ„ ν†΅ν•œ λ¦¬νŒ©ν† λ§μœΌλ‘œ 둜직 μƒμ˜ λ³€ν•˜μ§€ μ•ŠλŠ” λΆ€λΆ„(μ½”μ–΄ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직)κ³Ό λ³€ν•˜λŠ” λΆ€λΆ„(λ™μ‹œμ„± μ œμ–΄ 방법둠)을 효과적으둜 λΆ„λ¦¬ν•˜μ—¬ κ²°λ‘ μ μœΌλ‘œλŠ” μ½”λ“œμ˜ μž¬μ‚¬μš©μ„±κ³Ό μœ μ§€λ³΄μˆ˜μ„±μ΄ λ›°μ–΄λ‚œ 아킀텍쳐λ₯Ό ꡬ성할 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.