λ ˆμ½”λ“œ 랜덀 쑰회λ₯Ό ν†΅ν•œ 낙관적 락 μ„±λŠ₯ κ°œμ„ κΈ° (with μ‚¬μ΄λ“œ ν”„λ‘œμ νŠΈ)

2024. 5. 2. 15:48ㆍDatabase

μ„œλ‘ 

 

Tiketeer

Tiketeer has 5 repositories available. Follow their code on GitHub.

github.com

ν˜„μž¬ μ €λŠ” 지인 3λΆ„κ³Ό ν•¨κ»˜ κ°„λ‹¨ν•œ νŒ€ ν”„λ‘œμ νŠΈλ₯Ό 진행 쀑인데, κ·Έ κ³³μ—μ„œ 진행 쀑인 ‘ν‹°μΌ€νŒ… μ„œλΉ„μŠ€’ ν”„λ‘œμ νŠΈμ—μ„œ ν•œμˆœκ°„ λΆ€ν•˜κ°€ λͺ°λ¦¬λŠ” ν‹°μΌ€νŒ… ν™˜κ²½μ—μ„œλ„ ꡬ맀의 정합성을 보μž₯ν•˜κΈ° μœ„ν•΄ λ…Έλ ₯ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. 뿐만 μ•„λ‹ˆλΌ μ„±λŠ₯적인 κ°œμ„ λ„ 이루어내기 μœ„ν•΄ λ‹€μ–‘ν•œ λ…Όμ˜λ“€μ„ μ§„ν–‰ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. ν•΄λ‹Ή κΈ€μ—μ„œλŠ” μ„±λŠ₯ κ°œμ„ μ„ μœ„ν•΄ λ°μ΄ν„°λ² μ΄μŠ€ λ‚΄μ—μ„œ ν‹°μΌ“ λ ˆμ½”λ“œλ₯Ό μ½μ–΄μ˜€λŠ” 방식을 κ°œμ„ ν•˜κ³  κΈ°μ‘΄ 방식과 μ„±λŠ₯적으둜 λΉ„κ΅ν•œ λ‚΄μš©μ„ 닀루고, μ΅œμ’…μ μœΌλ‘œλŠ” 이λ₯Ό 톡해 저희 μ„œλΉ„μŠ€μ— 미친 영ν–₯에 λŒ€ν•΄ 이야기해보도둝 ν•˜κ² μŠ΅λ‹ˆλ‹€.

 

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

본격적인 μ„±λŠ₯ 비ꡐ에 μ•žμ„œ ν˜„μž¬ ν”„λ‘œμ νŠΈμ—μ„œ ν‹°μΌ€νŒ…μ΄ μ΄λ£¨μ–΄μ§€λŠ” 방식에 λŒ€ν•΄ κ°„λ‹¨νžˆ μ„€λͺ…ν•˜κ² μŠ΅λ‹ˆλ‹€.

μ„œλΉ„μŠ€ λ‚΄ ν‹°μΌ€νŒ… κ΄€λ ¨ λ‘œμ§μ— μ°Έμ—¬ν•˜λŠ” ν…Œμ΄λΈ” κ°„ κ°„λž΅ν™”ν•œ ERDλŠ” μœ„μ™€ κ°™μŠ΅λ‹ˆλ‹€.

μ•„λž˜λŠ” ν‹°μΌ“ ꡬ맀와 κ΄€λ ¨λœ λŒ€λž΅μ μΈ νλ¦„μž…λ‹ˆλ‹€.

  • νŒλ§€μžκ°€ ν‹°μΌ€νŒ…μ„ 생성할 λ•Œ νŒλ§€ν•  ν‹°μΌ“μ˜ 개수만큼 DB λ‚΄ λ ˆμ½”λ“œκ°€ μƒμ„±λ©λ‹ˆλ‹€.
  • κ΅¬λ§€μžλŠ” ꡬ맀할 ν‹°μΌ“μ˜ μˆ˜μ™€ ν•¨κ»˜ ꡬ맀 μš”μ²­μ„ λ³΄λ‚΄κ²Œ λ©λ‹ˆλ‹€.
  • 티켓에 λŒ€ν•œ λ™μ‹œμ„±μ„ κ³ λ €ν•˜κΈ° μœ„ν•΄ 티켓을 DBμ—μ„œ μ½μ–΄μ˜¬ λ•Œ 락을 톡해 ꡬ맀할 ν‹°μΌ“ 개수만큼 μ½μ–΄μ˜΅λ‹ˆλ‹€.
  • ꡬ맀 λ ˆμ½”λ“œλ₯Ό μƒμ„±ν•˜κ³  그와 ν•¨κ»˜ 티켓에 ν•΄λ‹Ή ꡬ맀λ₯Ό λ°°μ •ν•©λ‹ˆλ‹€.

이 κ³Όμ •μ—μ„œ μ œκ°€ ν™•μΈν•˜κ³ μž ν•˜λŠ” μ„±λŠ₯ κ°œμ„  ν¬μΈνŠΈλŠ” 락을 κ±Έκ³  티켓을 κ°€μ Έμ˜€λŠ” κ³Όμ •μž…λ‹ˆλ‹€.

 

 

λ³Έλ‘ 

κΈ°μ‘΄ 방식

기쑴에 κ΅¬ν˜„ν–ˆλ˜ 티켓을 κ°€μ Έμ˜€λŠ” λ ˆν¬μ§€ν† λ¦¬ λ©”μ„œλ“œλŠ” μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

(μ„œλΉ„μŠ€ λ‚΄ ν•΄λ‹Ή λ‘œμ§μ€ 비관적 락과 낙관적 락, 그리고 λΆ„μ‚°λ½μœΌλ‘œ λͺ¨λ‘ κ΅¬ν˜„λ˜μ–΄ μžˆμ§€λ§Œ 락의 νŠΉμ„± 상 ν•΄λ‹Ή κ°œμ„ μ€ 낙관적 락의 κ²½μš°μ— κ°€μž₯ 큰 차이가 μ‘΄μž¬ν•  것이라 μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€.)

μœ„ λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜λ©΄ μ•„λž˜ 쿼리가 λ‚˜κ°€κ²Œ λ©λ‹ˆλ‹€.

SELECT * 
FROM tickets 
WHERE 
    ticketing_id = :ticketingId 
  AND 
    purchase_id IS NULL 
ORDER BY ticket_id
LIMIT :limit;

이 μΏΌλ¦¬λŠ” λͺ©ν‘œ λ ˆμ½”λ“œλ₯Ό μ‘°νšŒν•˜λŠ”λ° κ²°κ΅­ ν‹°μΌ“ ID 순으둜 μ‘°νšŒν•˜κΈ° λ•Œλ¬Έμ— λ™μ‹œλ‹€λ°œμ μΈ ꡬ맀가 λ°œμƒν–ˆμ„ λ•Œ λͺ¨λ“  μœ μ €λ“€μ΄ 같은 티켓을 λ°”λΌλ³΄κ²Œλ©λ‹ˆλ‹€. λ”°λΌμ„œ λͺ¨λ“  μœ μ €λ“€μ˜ μš”μ²­μ΄ 같은 티켓에 낙관적 락을 κ±°λŠ” 것이기 λ•Œλ¬Έμ— λ™μ‹œμ„±μ΄ 많이 μ €ν•˜λ  κ²ƒμœΌλ‘œ μ˜ˆμƒν–ˆμŠ΅λ‹ˆλ‹€.

ORDER BYμ ˆμ€ λΊ€λ‹€λ©΄?
κ·Έλ ‡λ‹€λ©΄ ORDER BYλ₯Ό λΉΌλ©΄λ˜λŠ” 것 μ•„λ‹ˆλƒκ³  ν•  수 μžˆμ§€λ§Œ, MySQL의 νŠΉμ„± 상 ν΄λŸ¬μŠ€ν„°λ§ 인덱슀λ₯Ό μˆœνšŒν•˜κ²Œ 되고 μ•”λ¬΅μ μœΌλ‘œ ORDER BY ticket_idκ°€ μ‘΄μž¬ν•˜λŠ” κ²ƒμœΌλ‘œ λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€. λ”°λΌμ„œ ORDER BY μ ˆμ„ 빼더라도 λ™μΌν•˜κ²Œ λ™μž‘ν•©λ‹ˆλ‹€.

 

κ°œμ„  λ°©μ•ˆ

맀 μš”μ²­λ§ˆλ‹€ 같은 티켓에 락을 κ±°λŠ” 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄μ„œλŠ” ν‹°μΌ“ ν…Œμ΄λΈ” λ‚΄μ—μ„œ λ ˆμ½”λ“œλ₯Ό λžœλ€ν•˜κ²Œ μΆ”μΆœν•  ν•„μš”κ°€ μžˆλ‹€κ³  μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€.

MySQLμ—μ„œ 이λ₯Ό μœ„ν•΄μ„œλŠ” μ•„λž˜μ²˜λŸΌ 쿼리λ₯Ό μž‘μ„±ν•˜λ©΄ λ©λ‹ˆλ‹€.

SELECT * 
FROM tickets 
WHERE 
    ticketing_id = :ticketingId 
  AND 
    purchase_id IS NULL 
ORDER BY RAND()
LIMIT :limit;

그리고 ν•΄λ‹Ή 쿼리λ₯Ό μ‹€ν–‰μ‹œν‚€κΈ° μœ„ν•΄μ„œλŠ” μ•„λž˜μ²˜λŸΌ JPQL을 μž‘μ„±ν•˜λ©΄ λ©λ‹ˆλ‹€.

 

ν…ŒμŠ€νŠΈ λ°©μ•ˆ

λ„μΆœν•œ κ°œμ„  λ°©μ•ˆκ³Ό κΈ°μ‘΄ λ°©μ•ˆμ˜ μ„±λŠ₯을 λΉ„κ΅ν•˜κΈ° μœ„ν•΄ ν…ŒμŠ€νŠΈ 섀계가 ν•„μš”ν–ˆμŠ΅λ‹ˆλ‹€.

이λ₯Ό μœ„ν•΄ Executor와 CountDownLatchλ₯Ό ν™œμš©ν•œ ν…ŒμŠ€νŠΈλ₯Ό μ•„λž˜μ²˜λŸΌ μž‘μ„±ν•΄μ£Όμ—ˆμŠ΅λ‹ˆλ‹€.

κ·Έ ν›„ ν…ŒμŠ€νŠΈ μ‹œ 변인을 상정해야 ν–ˆλŠ”λ°, μ €λŠ” 이λ₯Ό νŠΉμ • ν‹°μΌ€νŒ… μ΄ν•˜μ˜ ν‹°μΌ“ 수둜 μž‘μ•˜μŠ΅λ‹ˆλ‹€.

κ·Έ μ΄μœ λŠ” 티켓을 κ°€μ Έμ˜€λŠ” 쿼리의 μ„±λŠ₯에 κ°€μž₯ 큰 영ν–₯을 λ―ΈμΉ˜λŠ” μš”μ†Œκ°€ λ°”λ‘œ 그것이기 λ•Œλ¬Έμž…λ‹ˆλ‹€. 이에 따라 μ‹€μ œ ν‹°μΌ€νŒ… 상황을 μƒμ •ν•˜μ—¬ 각각 300(μ˜ν™”κ΄€ 규λͺ¨), 3000(μ˜¬λ¦Όν”½ 홀 규λͺ¨), 30000(야ꡬμž₯ 규λͺ¨)의 ν‹°μΌ“ 수λ₯Ό 가지고 각각 ν…ŒμŠ€νŠΈλ₯Ό μ§„ν–‰ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

ν‹°μΌ“ 수 μ™Έ μš”μΈλ“€μ— λŒ€ν•΄μ„œλŠ” νŠΉμ • κ°’μœΌλ‘œ κ³ μ •ν•œ λ’€ ν…ŒμŠ€νŠΈλ₯Ό μ§„ν–‰ν–ˆμŠ΅λ‹ˆλ‹€.

  • λ™μ‹œ ν‹°μΌ€νŒ… μœ μ € 수: 20 (ν”„λ‘œμ νŠΈ λŒ€κΈ°μ—΄ 정책에 κΈ°λ°˜ν•œ κ°€λŠ₯ν•œ κ°’ λ‚΄ 선택)
  • 각 μœ μ € λ‹Ή ν‹°μΌ“ ꡬ맀 수: 3 (μž„μ˜μ˜ κ°’)

λ˜ν•œ 낙관적 락 방식이기 λ•Œλ¬Έμ— λ™μ‹œμ„± μ˜ˆμ™Έκ°€ λ°œμƒν–ˆμ„ 경우 이에 λŒ€ν•œ μ˜ˆμ™Έμ²˜λ¦¬κ°€ ν•„μš”ν•©λ‹ˆλ‹€. μ €λŠ” @Retryable μ–΄λ…Έν…Œμ΄μ…˜μœΌλ‘œ μ‹€νŒ¨ μ‹œ ꡬ맀 λ‘œμ§μ„ λ‹€μ‹œ μ‹€ν–‰μ‹œν‚€λŠ” 방식을 μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ 방식 별 비ꡐλ₯Ό μœ„ν•΄ μ΅œμ’… μž¬μ‹œλ„ 횟수λ₯Ό λ°˜ν™˜ν•΄μ£Όμ—ˆμŠ΅λ‹ˆλ‹€.

이 λ•Œ @Retryable κ΄€λ ¨ 섀정은 μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

  • backoff(μ‹€νŒ¨ μ‹œ μž¬μ‹œλ„κΉŒμ§€ λ”œλ ˆμ΄): 50ms
  • maxAttempts(μ΅œλŒ€ μž¬μ‹œλ„ 횟수): 100회

 

ν…ŒμŠ€νŠΈ κ²°κ³Ό

λͺ¨λ“  μœ μ €μ˜ ν‹°μΌ€νŒ…μ΄ μ™„λ£Œλ˜κΈ°κΉŒμ§€ κ±Έλ¦° μ‹œκ°„κ³Ό μž¬μ‹œλ„ 횟수λ₯Ό 각 μΌ€μ΄μŠ€ λ³„λ‘œ ν™•μΈν•˜λ©΄ μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

ν‹°μΌ“ 수 κΈ°μ‘΄ 방식 랜덀 μΆ”μΆœ 방식
μ†Œμš” μ‹œκ°„ κ²½ν•© λ°œμƒ 횟수 μ†Œμš” μ‹œκ°„ κ²½ν•© λ°œμƒ 횟수
300개 761ms 59회 98ms 2회
3000개 666ms 55회 106ms 1회
30000개 979ms 75회 120ms 0회

 

ν…ŒμŠ€νŠΈ 해석

κΈ°μ‘΄ λ°©μ‹μ˜ ν…ŒμŠ€νŠΈ κ²°κ³Όλ₯Ό ν•΄μ„ν•΄λ³΄μžλ©΄, κ²°κ΅­ ν‹°μΌ“μ˜ 수만 μ¦κ°€ν•˜κ³  μžˆλŠ” μ…‹μ—…μ—μ„œ μž‘μ—… μ†Œμš” μ‹œκ°„μ΄ λͺ¨λ‘ λΉ„μŠ·ν•©λ‹ˆλ‹€.

SELECT * 
FROM tickets 
WHERE 
    ticketing_id = :ticketingId 
  AND 
    purchase_id IS NULL 
ORDER BY ticket_id
LIMIT :limit;

이λ₯Ό ν•΄μ„ν•˜κΈ° μœ„ν•΄μ„œλŠ” κΈ°μ‘΄ λ°©μ‹μ˜ 쿼리가 λ™μž‘ν•˜λŠ” 방식을 이해해야 ν•˜λŠ”λ°, PK κΈ°μ€€μœΌλ‘œ μ •λ ¬λœ κ°’ 쀑 3개의 λ ˆμ½”λ“œλ₯Ό κ°€μ Έμ˜€λŠ” 것이 λͺ©μ μ΄κΈ° λ•Œλ¬Έμ— κ²°κ΅­μ—” ν΄λŸ¬μŠ€ν„°λ§ 인덱슀λ₯Ό ν’€μŠ€μΊ”ν•˜λ©΄μ„œ 쑰건을 λ§Œμ‘±ν•˜λŠ” λ ˆμ½”λ“œλ₯Ό 3개 μ°ΎλŠ” μˆœκ°„ κ³§λ°”λ‘œ 싀행이 μ’…λ£Œλ˜λŠ” μΏΌλ¦¬μž…λ‹ˆλ‹€.

λ”°λΌμ„œ λ ˆμ½”λ“œκ°€ λ§Žμ•„μ§€λ”λΌλ„ ν‹°μΌ€νŒ…μ΄ ν•˜λ‚˜ μžˆλŠ” μƒν™©μ—μ„œλŠ” λͺ¨λ‘ λΉ„μŠ·ν•œ μ„±λŠ₯을 λ³΄μž…λ‹ˆλ‹€. μ΄λŸ¬ν•œ 상황은 ν‹°μΌ€νŒ…μ΄ λ‘˜ 이상 μžˆλŠ” μƒν™©μ—μ„œλŠ” κΉ¨μ§€κ²Œ λ˜λŠ”λ° 이 λ˜ν•œ tickets ν…Œμ΄λΈ” λ‚΄ ticketing_id에 인덱슀λ₯Ό κ±Έμ–΄μ£ΌλŠ” λ°©μ‹μœΌλ‘œ μ΅œμ ν™”κ°€ κ°€λŠ₯ν•©λ‹ˆλ‹€.

즉, 티켓을 μ‘°νšŒν•˜λŠ” 것 μžμ²΄λŠ” μ–΄λŠμ •λ„ μ΅œμ ν™”κ°€ μ΄λ£¨μ–΄μ Έμžˆλ‹€κ³  λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ λ¬Έμ œλŠ” λ°”λ‘œ λ³Έλ‘  μ„œλ‘μ—μ„œ μ–ΈκΈ‰ν•œ λͺ¨λ“  νŠΈλžœμž­μ…˜μ΄ λ™μΌν•œ ν‹°μΌ“ λ ˆμ½”λ“œλ₯Ό λ°”λΌλ³΄λŠ” κ²ƒμž…λ‹ˆλ‹€. 이둜 인해 λΆˆν•„μš”ν•œ 경합이 λ°œμƒν•˜κ²Œ 되고, 이λ₯Ό ν†΅ν•œ 반볡적인 μž¬μ‹œλ„λ₯Ό 톡해 κ²°λ‘ μ μœΌλ‘œλŠ” μ„±λŠ₯이 쒋지 μ•Šμ€ λͺ¨μŠ΅μ„ 보이고 μžˆμŠ΅λ‹ˆλ‹€.

 

반면, μˆ˜μ •λœ μΏΌλ¦¬λŠ” 큰 μ°¨μ΄λŠ” μ•„λ‹ˆλ‚˜ ν‹°μΌ“μ˜ μˆ˜κ°€ 증가함에 따라 μΌμ •ν•˜κ²Œ μ‹€ν–‰ μ‹œκ°„μ΄ λŠ˜μ–΄λ‚˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

SELECT * 
FROM tickets 
WHERE 
    ticketing_id = :ticketingId 
  AND 
    purchase_id IS NULL 
ORDER BY RAND()
LIMIT :limit;

μ΄λŠ” μœ„ 쿼리가 PKλ₯Ό κΈ°μ€€μœΌλ‘œ μ •λ ¬λœ 3개의 λ ˆμ½”λ“œλ₯Ό κ°€μ Έμ˜€λŠ” 것이 μ•„λ‹ˆλΌ, 쑰건을 λ§Œμ‘±ν•˜λŠ” λͺ¨λ“  λ ˆμ½”λ“œμ— 랜덀 값을 λΆ€μ—¬ν•˜κ³  이λ₯Ό κΈ°μ€€μœΌλ‘œ 정렬을 μˆ˜ν–‰ν•˜κΈ° λ•Œλ¬Έμ— 이 κ³Όμ •μ—μ„œ λ°μ΄ν„°μ˜ 수만큼 μ—°μ‚°λŸ‰μ΄ λŠ˜μ–΄λ‚˜κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€. λ”°λΌμ„œ 이 뢀뢄은 이후 ν…Œμ΄λΈ” λ‚΄ 데이터가 λ§Žμ•„μ§μ— 따라 좔가적인 μ„±λŠ₯ κ°œμ„ μ˜ 포인트둜 μž‘μ„ 수 μžˆμ–΄λ³΄μž…λ‹ˆλ‹€.

λ‹€λ§Œ κ·ΈλŸΌμ—λ„ λΆˆκ΅¬ν•˜κ³ , κΈ°μ‘΄ 방식과 λΉ„κ΅ν–ˆμ„ λ•Œ μž¬μ‹œλ„ 횟수(κ²½ν•© 횟수)κ°€ μ—„μ²­λ‚˜κ²Œ 쀄어듀어 결둠적으둜 μ„±λŠ₯ κ°œμ„ μ€ ꡉμž₯히 많이 된 것을 λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€. (μ•½ 8λ°° κ°œμ„ )

 

 

κ²°λ‘ 

낙관적 락은 경합이 많이 λ°œμƒν• μˆ˜λ‘ μ„±λŠ₯이 λ“œλΌλ§ˆν‹±ν•˜κ²Œ λ‚˜λΉ μ§€λŠ” νŠΉμ„±μ„ κ°–κΈ° λ•Œλ¬Έμ—, 같은 μ‘°κ±΄μ—μ„œ 경합을 쀄일 수 μžˆλ‹€λ©΄ 낙관적 락 둜직의 μ„±λŠ₯ κ°œμ„ μ„ 이루어낼 수 μžˆλ‹€κ³  μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€.

μ €λŠ” 이λ₯Ό λ°”νƒ•μœΌλ‘œ 저희가 초기 κ΅¬ν˜„ν•œ λ°©μ‹μ˜ 경우 λΆˆν•„μš”ν•œ 경합이 κ³Όλ„ν•˜κ²Œ 많이 λ°œμƒν•  κ²ƒμœΌλ‘œ μ˜ˆμƒν•˜μ—¬ κΈ°μ‘΄κ³ΌλŠ” λ‹€λ₯Έ λ°©μ‹μœΌλ‘œ ν‹°μΌ“ μΆ”μΆœμ„ 진행해야 ν•œλ‹€κ³  μƒκ°ν•˜μ—¬ ν•΄λ‹Ή ν…ŒμŠ€νŠΈλ₯Ό μ§„ν–‰ν•˜μ˜€κ³ , 결둠적으둜 μœ„ ν…ŒμŠ€νŠΈ 결과듀을 톡해 λ ˆμ½”λ“œλ₯Ό λžœλ€ν•˜κ²Œ μΆ”μΆœν•˜λŠ” 방식이 경합을 μ€„μ΄λŠ”λ° 큰 도움을 μ€€λ‹€λŠ” 것을 확인할 수 μžˆμ—ˆμ„ 뿐 μ•„λ‹ˆλΌ 이λ₯Ό 톡해 κ²½ν•© μ‹œ ν‹°μΌ“ ꡬ맀 μ„±λŠ₯을 μ•½ 8λ°° κ°œμ„ ν•  수 μžˆμ—ˆκ³ , μ΅œμ’…μ μœΌλ‘œλŠ” 저희 μ„œλΉ„μŠ€κ°€ 낙관적, 비관적, λΆ„μ‚° 락 쀑 μ–΄λ–€ 락을 선택할지 κ³ λ €ν•˜λŠ” λ‹¨κ³„μ—μ„œ 낙관적 락을 μ„ νƒν•˜λŠ”λ° 큰 κΈ°μ—¬λ₯Ό ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

(ν”„λ‘œμ νŠΈ λ‚΄ 락 선택 κ΄€λ ¨ λ¬Έμ„œ: https://tiketeer.notion.site/cd390c746077406eb352947875df9457)