Backend

<Spring> AOP 기반 뢄산락

wjdtkdgns 2023. 10. 1. 22:31

πŸ“Œ 뢄산락 λ„μž… λ°°κ²½

jmeter둜 μ—¬λŸ¬ μœ μ €κ°€ λ™μ‹œμ— 같은 아킀이빙에 μŠ€ν¬λž©ν•˜λ„λ‘ ν•΄λ΄€λ‹€.

ν•˜μ§€λ§Œ count μ—…λ°μ΄νŠΈκ°€ μ΄μƒν–ˆλ‹€.

λΆ„λͺ…νžˆ μœ μ € 10λͺ…이 μŠ€ν¬λž©ν–ˆλŠ”λ°, 슀크랩 μˆ˜λŠ” 10이 μ•„λ‹ˆλΌ 1μ΄μ—ˆλ‹€.

scrap

 

scrap count

λ™μ‹œμ„± λ¬Έμ œκ°€ λ°œμƒν•œ 것이닀.

이둜 인해 데이터 정합성이 κΉ¨μ§€κ²Œ λ˜μ—ˆλ‹€.

이에 ν•„μžλŠ” 뢄산락을 λ„μž…ν•΄ 이λ₯Ό ν•΄κ²°ν•˜κ³ μž ν–ˆλ‹€.

 

πŸ“Œ λ™μ‹œμ„± 문제

πŸ”΅ λ™μ‹œμ„±

λ™μ‹œμ„±μ€ μ—¬λŸ¬ μž‘μ—…μ΄ κ²Ήμ³μ„œ μ‹€ν–‰λ˜λŠ” 것을 λ§ν•œλ‹€.

μ΄λ•Œ, μ‹€μ œλ‘œ μž‘μ—…μ΄ λ™μ‹œμ— μ‹€ν–‰λœλ‹€λŠ” λœ»μ€ μ•„λ‹ˆλ‹€.

μ‰¬μš΄ μ˜ˆμ‹œλ‘œ CPU의 μž‘μ—… 방식을 λ“€ 수 μžˆλ‹€. 

CPU의 μ½”μ–΄μ—μ„œλŠ” νŠΉμ • μ‹œμ μ— ν•˜λ‚˜μ˜ μž‘μ—…λ§Œ μˆ˜ν–‰ κ°€λŠ₯ν•˜λ‹€.

ν•˜μ§€λ§Œ CPUκ°€ μž‘μ—… κ°„ μ „ν™˜μ„ λΉ λ₯΄κ²Œ ν•˜κΈ° λ•Œλ¬Έμ—, μž‘μ—…μ΄ 거의 λ™μ‹œμ— μ‹€ν–‰λ˜λŠ” κ²ƒμ²˜λŸΌ 보일 뿐이닀.

 

πŸ”΅ λ™μ‹œμ„± 문제

2λͺ…μ˜ μœ μ €κ°€ 있고, 재고 κ°μ†Œ μš”μ²­μ„ λ™μ‹œμ— ν–ˆλ‹€κ³  κ°€μ •ν•˜μž.

λ§Œμ•½ μ„œλ²„μ— λ™μ‹œμ„± μ²˜λ¦¬κ°€ λ˜μ–΄μžˆμ§€ μ•Šλ‹€λ©΄, μ–΄λ–€ 상황이 λ²Œμ–΄μ§ˆκΉŒ?

μš”μ²­μ΄ μ •μƒμ μœΌλ‘œ μ²˜λ¦¬λ˜μ§€ μ•Šμ„ κ°€λŠ₯성이 λ†’λ‹€.

 

κ°œλ°œν•˜λ©΄μ„œ λ™μ‹œμ„±μ— κ΄€ν•œ λ¬Έμ œλŠ” ν”νžˆ μ ‘ν•  수 μžˆλ‹€.

μ•„λž˜μ™€ 같은 μ˜ˆμ‹œκ°€ μžˆμ„ 수 μžˆλ‹€.

  1. 같은 μš”μ²­μ„ λ™μ‹œμ— ν–ˆμ„ 경우 (ν”νžˆ, λ”°λ‹₯이라 λΆ€λ₯Έλ‹€)
  2. λ„€νŠΈμ›Œν¬ 혼작 λ•Œλ¬Έμ—, 두 μš”μ²­μ΄ λ™μ‹œμ— λ„μ°©ν•œ 경우
  3. λŒ€κ·œλͺ¨ νŠΈλž˜ν”½μ΄ λͺ°λ¦΄ 경우

μš°λ¦¬λŠ” 이런 상황에 Race Condition (경쟁 μƒνƒœ)κ°€ λ°œμƒν–ˆλ‹€κ³  ν•œλ‹€.

 

πŸ”΅ 해결법

μœ„μ—μ„œ λ°œμƒν•œ 문제의 경우, 문제λ₯Ό ν•΄κ²°ν•  수 μžˆλŠ” 방법은 λ‹€μ–‘ν•˜λ‹€.

  1. Java Synchronized, ReentrantLock
  2. DB Lock
  3. Redis, Zookeeper λ“± μ™ΈλΆ€ 인프라 μ΄μš©ν•œ 뢄산락

ν•˜λ‚˜ν•˜λ‚˜ μ•Œμ•„λ³΄μž

 

πŸ“Œ Java Synchronized

Javaμ—μ„œ λ™μ‹œμ„±μ„ κ΄€λ¦¬ν•˜κΈ° μœ„ν•΄ μ‚¬μš©ν•˜λŠ” λ©”μ»€λ‹ˆμ¦˜μ΄λ‹€.

 

μžλ°”μ— λ‚΄μž₯된 λ½μœΌλ‘œμ„œ 이λ₯Ό 암묡적인 락(Intrinsic Lock) ν˜Ήμ€ λͺ¨λ‹ˆν„°λ½(Monitor Lock)이라고 ν•œλ‹€.

synchronized ꡬ문을 톡해 λͺ¨λ‹ˆν„° μ˜μ—­μ„ 동기화할 수 μžˆλ‹€.

public synchronized void decrease(Long id, Long quantity) {
    Stock stock = stockRepository.findById(id).orElseThrow();
    stock.decrease(quantity);
    stockRepository.saveAndFlush(stock);
}

μœ„μ™€ 같이 μ‚¬μš©ν•  경우 Race Condition을 ν•΄κ²°ν•  수 μžˆλ‹€.

ν•˜μ§€λ§Œ λ‹€μŒκ³Ό 같은 κ²½μš°μ—μ„œ SynchronizedλŠ” μ˜λ„ν•œ λŒ€λ‘œ μž‘λ™ν•˜μ§€ μ•ŠλŠ”λ‹€.

 

1. @Transactionalκ³Ό λ™μ‹œμ— μ‚¬μš©

@Transactional
public synchronized void decrease(Long id, Long quantity) {
    Stock stock = stockRepository.findById(id).orElseThrow();
    stock.decrease(quantity);
    stockRepository.saveAndFlush(stock);
}

μœ„μ™€ 같이 μ‚¬μš©λ  경우 λ™μ‹œμ„± λ¬Έμ œκ°€ λ°œμƒν•œλ‹€.

μ΄λŠ” @Transactional의 λ™μž‘ 방식과 κ΄€λ ¨ μžˆλ‹€.

@Transactional은 ν”„λ‘μ‹œ λ°©μ‹μœΌλ‘œ λ™μž‘ν•œλ‹€. 

κ·ΈλŸ¬λ―€λ‘œ νŠΈλžœμž­μ…˜ μ‹œμž‘ -> 락 μ„€μ • -> μ„œλΉ„μŠ€ 호좜 -> 락 ν•΄μ œ -> νŠΈλžœμž­μ…˜ μ’…λ£Œ 순으둜 μ§„ν–‰λœλ‹€.

λ§Œμ•½ νŠΈλžœμž­μ…˜ μ’…λ£Œ μ „, λ‹€λ₯Έ μŠ€λ ˆλ“œκ°€ 같은 λ©”μ†Œλ“œμ— μ ‘κ·Όν•˜λ©΄ μ»€λ°‹λ˜μ§€ μ•Šμ€ 데이터에 접근이 κ°€λŠ₯ν•˜λ‹€. 

λ™μ‹œμ„± λ¬Έμ œκ°€ λ°œμƒ κ°€λŠ₯ν•˜λ‹€.

 

2. 닀쀑 μ„œλ²„μ—μ„œ μ‚¬μš©

synchronizedλŠ” 단일 ν”„λ‘œμ„ΈμŠ€μ—μ„œ 잘 μž‘λ™ν•œλ‹€.

ν•˜μ§€λ§Œ 닀쀑 μ„œλ²„μΌ 경우 μ΄λŠ” race condition을 ν•΄κ²°ν•˜μ§€ λͺ»ν•œλ‹€.

이에 닀쀑 μ„œλ²„μ˜ ν™˜κ²½μ— μ ν•©ν•˜μ§€ μ•Šμ€ 방법이닀.

 

πŸ“Œ DB Lock

말 κ·ΈλŒ€λ‘œ DBλ₯Ό μ΄μš©ν•œ Lock이닀.

λ‹€μ–‘ν•œ 방법이 μžˆλ‹€.

  1. Optimistic Lock : version을 μ΄μš©ν•˜μ—¬ 정합성을 λ§žμΆ”λŠ” 방법
  2. Pessimistic Lock : Exclusive Lock을 μ΄μš©ν•˜μ—¬ 정합성을 λ§žμΆ”λŠ” 방법
  3. Named Lock : 이름을 κ°€μ§„ Lock을 μ΄μš©ν•˜μ—¬ Lock을 κ±°λŠ” 방법

이 방법 이외에도, flag column을 μ΄μš©ν•œ 방법도 μžˆλ‹€.

μ΄λŠ” κΈ°μ‘΄ μ—°κ²°λœ DBλ₯Ό μ΄μš©ν•˜κΈ° λ•Œλ¬Έμ—, 좔가적인 인프라 λ„μž…μ„ κ³ λ €ν•˜μ§€ μ•Šμ•„λ„ λœλ‹€.

 

ν•˜μ§€λ§Œ 이 방법을 μ„ νƒν•˜μ§€ μ•Šμ•˜λ‹€.

μ• ν”Œλ¦¬μΌ€μ΄μ…˜κ³Ό RDB에 λΆ€ν•˜λ₯Ό μ΅œλŒ€ν•œ 적게 κ°€μ Έκ°€κ³  μ‹Άμ—ˆκΈ° λ•Œλ¬Έμ΄λ‹€.

 

낙관적 락의 경우, μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ 버전을 κ΄€λ¦¬ν•˜λ©° 이에 λŒ€ν•œ 처리 및 μ‹€νŒ¨ μ‹œ ν›„μ²˜λ¦¬λ₯Ό λ‹΄λ‹Ήν•΄μ•Ό ν•˜λ―€λ‘œ, 

νŠΈλžœμž­μ…˜ 컀밋에 μžˆμ–΄μ„œ λ³΅μž‘λ„κ°€ μ¦κ°€ν•œλ‹€.

 

비관적 락의 경우, λ°λ“œλ½μ˜ μœ„ν—˜μ΄ λ†’λ‹€κ³  μƒκ°ν–ˆλ‹€.

이 μœ„ν—˜μ„±μ„ μΈμ§€ν•˜κ³  μ„œλΉ„μŠ€λ₯Ό κ΅¬ν˜„ν•œλ‹€ ν•˜λ”λΌλ„, λ‚˜μ€‘μ—λŠ” 이 뢀뢄에 μžˆμ–΄μ„œ μ„œλΉ„μŠ€ κ΅¬ν˜„μ— μ œμ•½μ΄ 클 것이라고 μƒκ°ν–ˆλ‹€.

 

Named 락의 경우, DB μžμ›μ„ 더 μ‚¬μš©ν•΄μ•Ό ν•˜λŠ” λ¬Έμ œκ°€ λ°œμƒν•œλ‹€.

락 ν•΄μ œ μ‹œμ  κ΄€λ ¨ 문제 λ•Œλ¬Έμ—, λ‘œμ§μ— λŒ€ν•œ νŠΈλžœμž­μ…˜κ³Ό DB lock에 λŒ€ν•œ νŠΈλžœμž­μ…˜μ„ λΆ„λ¦¬ν•˜μ—¬ μ‚¬μš©ν•΄μ•Ό ν•œλ‹€.

이에 락을 μ μš©ν•œ νŠΈλžœμž­μ…˜μ˜ 경우 DB 컀λ„₯μ…˜μ„ 2개 μ‚¬μš©ν•΄μ•Ό ν•œλ‹€.

Redissonκ³Ό λΉ„μŠ·ν•˜κ²Œ μ‚¬μš©ν•  수 μžˆλŠ” λ°©λ²•μ΄μ§€λ§Œ, DB μžμ›μ„ 더 μ‚¬μš©ν•œλ‹€λŠ” 점을 κ³ λ €ν•˜μ—¬ μ‚¬μš©ν•˜μ§€ μ•Šμ•˜λ‹€.

 

λ˜ν•œ 이미 뢄산락을 κ΅¬ν˜„ κ°€λŠ₯ν•œ RedisλΌλŠ” 인프라 μ„œλΉ„μŠ€λ₯Ό λ„μž…ν•˜μ—¬ μ‚¬μš©ν•˜κ³  μžˆμ—ˆλ‹€.

이에 ν•„μžλŠ” DB Lock을 λ„μž…ν•˜μ§€ μ•Šμ•˜λ‹€.

 

πŸ“Œ μ™ΈλΆ€ 인프라λ₯Ό μ΄μš©ν•œ 뢄산락

뢄산락을 κ΅¬ν˜„ν•˜λŠ”λ° μ“°μ΄λŠ” 인프라 기술둜 Redis와 Zookeeperλ₯Ό λ“€ 수 μžˆλ‹€.

κ²°λ‘ λΆ€ν„° λ§ν•˜μžλ©΄ ν•„μžλŠ” Redisλ₯Ό μ„ νƒν•΄μ„œ κ΅¬ν˜„ν–ˆλ‹€.

RedisλŠ” 이미 μ‚¬μš©ν•˜κ³  μžˆμ—ˆκ³ , μ‹±κΈ€ μŠ€λ ˆλ“œλ‘œ μž‘λ™ν•˜κΈ° λ•Œλ¬Έμ— λ™μ‹œμ„± μ²˜λ¦¬μ— 적합할 거라 μƒκ°ν–ˆκΈ° λ•Œλ¬Έμ΄λ‹€.

λ˜ν•œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜κ³Ό DB λΆ€ν•˜λ₯Ό 쀄일 수 μžˆλŠ” 방법이라 μƒκ°ν–ˆλ‹€.

 

κ·Έλ ‡λ‹€λ©΄ ZookeeperλŠ” μ™œ μ„ νƒν•˜μ§€ μ•Šμ•˜μ„κΉŒ?

이미 Redisλ₯Ό μ‚¬μš©ν•˜κ³  μžˆμ—ˆλ‹€λŠ” 점이 크게 μž‘μš©ν–ˆλ‹€. 

λ˜ν•œ Zookeeperλ₯Ό λ„μž…ν•˜μ—¬ λ‹€λ₯Έ 관리 포인트λ₯Ό μΆ”κ°€ν•˜λŠ” 것이 λΆ€λ‹΄μœΌλ‘œ λ‹€κ°€μ™”λ‹€.

 

πŸ“Œ Redisλ₯Ό μ΄μš©ν•œ 뢄산락

Redis둜 뢄산락을 μ΄μš©ν•˜λŠ” 방법은 2κ°€μ§€ μžˆλ‹€.

ν΄λΌμ΄μ–ΈνŠΈλ‘œ Lettuceλ₯Ό μ‚¬μš©ν•˜κΈ°, ν΄λΌμ΄μ–ΈνŠΈλ‘œ Redisson을 μ‚¬μš©ν•˜κΈ°

 

1. Lettuce

LettuceλŠ” 기본적으둜 락 κΈ°λŠ₯을 μ œκ³΅ν•˜μ§€ μ•ŠλŠ”λ‹€.

λ•Œλ¬Έμ— μ‚¬μš©μžκ°€ 직접 setnx, setex 같은 λͺ…λ Ήμ–΄λ₯Ό 톡해 뢄산락을 κ΅¬ν˜„ν•΄μ•Ό ν•œλ‹€.

μ΄λ•Œ 락은 Spin Lock 방식이닀.

μ‹±κΈ€ μŠ€λ ˆλ“œμΈ Redis에 Spin Lock은 큰 λΆ€ν•˜λ‘œ μ΄μ–΄μ§ˆ 수 μžˆλ‹€.

λ˜ν•œ setnxκ°€ expired time을 μ§€μ •ν•  수 μ—†μ–΄μ„œ Lock ν•΄μ œκ°€ λΆˆκ°€λŠ₯ν•˜λ‹€. 

이 λ•Œλ¬Έμ— Dead Lock λ°œμƒ κ°€λŠ₯성이 μ‘΄μž¬ν•œλ‹€.

 

2. Redisson

Redisson은 락 κΈ°λŠ₯을 지원해 μ€€λ‹€.

pub/sub 방식을 μ‚¬μš©ν•˜μ—¬ 락이 ν•΄μ œλ  λ•Œλ§ˆλ‹€ subscribe ν•˜λŠ” ν΄λΌμ΄μ–ΈνŠΈλ“€μ—κ²Œ μ•Œλ¦Όμ„ μ£ΌλŠ” ꡬ쑰이닀.

Lettuce에 λΉ„ν•΄ Redis에 λΆ€ν•˜κ°€ 더 적게 μš΄μ˜ν•  수 μžˆλ‹€.

 

ν•΄λ‹Ή μ„œλΉ„μŠ€μ— λ ˆλ””μŠ€λ₯Ό μ΄μš©ν•œ μ„œλΉ„μŠ€κ°€ λ§Žμ•˜κΈ°μ—, λ ˆλ””μŠ€μ— μ£Όμ–΄μ§€λŠ” λΆ€ν•˜κ°€ 적은 방법을 선택해야 ν–ˆλ‹€.

κ·Έλž˜μ„œ ν•„μžλŠ” Redisson을 μ„ νƒν–ˆλ‹€.

 

πŸ“Œ AOP 기반 Redisson 뢄산락

πŸ”΅ Propagation.NEVER

Redisson 뢄산락을 인터넷에 검색할 경우, νŠΈλžœμž­μ…˜ μ „νŒŒ 속성을 REQUIRES_NEWλ₯Ό μ‚¬μš©ν•˜λŠ” 것을 λ³Ό 수 μžˆλ‹€.

이 경우 둀백에 λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆλ‹€.

μ›λž˜ νŠΈλžœμž­μ…˜μ—μ„œ 뢄산락 처리λ₯Ό μœ„ν•΄ μƒˆλ‘œμš΄ νŠΈλžœμž­μ…˜μ„ λ§Œλ“€λ©΄ 2개의 νŠΈλžœμž­μ…˜μ΄ μ‘΄μž¬ν•œλ‹€.

μ΄λ•Œ, μ–΄λ–€ ν•œ νŠΈλžœμž­μ…˜μ΄ μ‹€νŒ¨ν•˜μ—¬ λ‘€λ°±λœλ‹€κ³  κ°€μ •ν•΄ 보자.

κ·Έλ ‡λ‹€λ©΄ λ‹€λ₯Έ νŠΈλžœμž­μ…˜λ„ λ‘€λ°±ν•  수 μžˆμ„κΉŒ?

ν•  수 μ—†λ‹€.

μ„œλ‘œ λ…λ¦½λœ νŠΈλžœμž­μ…˜μ΄λ―€λ‘œ, μ„œλ‘œκ°€ μ„œλ‘œμ—κ²Œ 영ν–₯을 λ―ΈμΉ  수 μ—†λ‹€.

 

이 문제λ₯Ό 락이 μ„€μ •λœ 단일 νŠΈλžœμž­μ…˜μ„ μ΄μš©ν•œλ‹€λ©΄ λ™μ‹œμ„± 문제λ₯Ό ν•΄κ²°ν•  수 μžˆμ„ 것이라 μƒκ°ν–ˆλ‹€.

단일 νŠΈλžœμž­μ…˜μ΄λΌλ©΄, μΌλΆ€λΆ„μ˜ μž‘μ—…μ΄ μ‹€νŒ¨ν•˜λ”λΌλ„ λͺ¨λ‘ 둀백이 κ°€λŠ₯ν•˜κΈ° λ•Œλ¬Έμ—,

μœ„μ—μ„œ λ°œμƒ κ°€λŠ₯ν•œ λ‘€λ°± κ΄€λ ¨ 문제λ₯Ό 극볡 κ°€λŠ₯ν•  것이닀.

 

이λ₯Ό κ΅¬ν˜„ν•˜κΈ° μœ„ν•΄ μ „νŒŒ 속성을 NEVERλ₯Ό μ΄μš©ν–ˆλ‹€.

μ „νŒŒ 속성 NEVER에 λŒ€ν•œ μ„€λͺ…에 μ„ ν–‰ νŠΈλžœμž­μ…˜μ΄ μ‘΄μž¬ν•˜λ©΄ μ—λŸ¬λ₯Ό λ°œμƒμ‹œν‚¨λ‹€λΌκ³  μ ν˜€μžˆλ‹€.

이 말은 NEVER μ „νŒŒ 속성을 μ΄μš©ν•˜λ©΄, μ„ ν–‰ νŠΈλžœμž­μ…˜μ΄ μ—†μŒμ„ 보μž₯ν•  수 μžˆλ‹€.

κ·Έ ν›„ REQUIREDλ₯Ό 톡해 νŠΈλžœμž­μ…˜μ„ μƒμ„±ν•˜λ©΄, μ„ ν–‰ νŠΈλžœμž­μ…˜μ΄ μ‘΄μž¬ν•˜μ§€ μ•Šμ€ μƒνƒœμ—μ„œ νŠΈλžœμž­μ…˜μ„ 생성할 수 μžˆλ‹€.

λ˜ν•œ 이 νŠΈλžœμž­μ…˜μ—μ„œ λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ„ λ°œμƒμ‹œν‚€μ§€ μ•ŠλŠ”λ‹€λ©΄, 단일 νŠΈλžœμž­μ…˜μ„ μœ μ§€ν•˜λŠ” 것이 κ°€λŠ₯ν•˜λ‹€.

κ·ΈλŸ¬λ―€λ‘œ ν•„μžλŠ” NEVER 속성을 μ΄μš©ν•˜μ—¬ 뢄산락을 κ΅¬ν˜„ν•  것이닀.

 

πŸ”΅ AOP

락을 λͺ¨λ“  λ‘œμ§μ— μ μš©ν•˜μ§€ μ•ŠλŠ”λ‹€.

νŠΉμ • λ‘œμ§μ—λ§Œ μ μš©ν•΄μ•Ό ν–ˆλ‹€.

ν•˜μ§€λ§Œ 이λ₯Ό μ μš©ν•˜κΈ° μœ„ν•΄ 뢄산락 λ‘œμ§μ„ 맀번 λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ— λ„£λŠ”λ‹€λ©΄, λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 μ˜€μ—Όλ  κ°€λŠ₯성이 μ‘΄μž¬ν•œλ‹€.

이 지점을 AOPλ₯Ό μ‚¬μš©ν•˜μ—¬ κ·Ήλ³΅ν–ˆλ‹€.

락이 ν•„μš”ν•œ 뢀뢄에 @DistributedLock을 μ μš©ν•˜λ©΄, μ£Όμ–΄μ§„ 값에 λ”°λΌμ„œ 락이 μ„€μ •λ˜λ„λ‘ κ΅¬ν˜„ν–ˆλ‹€.

κ΅¬ν˜„μ— κ΄€ν•œ μžμ„Έν•œ λ‚΄μš©μ€ λ‹€μŒμ„ 보면 λœλ‹€.

 

πŸ”΅ DistributedLock AOP

DistributedLock

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
    // 락 νƒ€μž…
    DistributedLockType lockType();

    // 뢄산락을 κ±Έ νŒŒλΌλ―Έν„° λ„€μž„
    String[] identifier();
}

μœ„μ—μ„œ μ„€λͺ…ν•œ κ²ƒμ²˜λŸΌ AOPλ₯Ό μ΄μš©ν•˜κΈ° μœ„ν•΄ μ–΄λ…Έν…Œμ΄μ…˜μ„ κ΅¬ν˜„ν–ˆλ‹€.

μ΄λŠ” DistributedLock을 μ •μ˜ν•˜λŠ” μ–΄λ…Έν…Œμ΄μ…˜μ΄λ‹€.

락 νƒ€μž…κ³Ό, νŒŒλΌλ―Έν„° 이름 인자둜 λ°›μ•„μ„œ 닀쀑 킀에 λŒ€ν•œ 락에 λŒ€μ‘ν–ˆλ‹€.

 

DistributedLockAop

@Aspect
@Component
@RequiredArgsConstructor
public class DistributedLockAop {
    private final LockManager lockManager;

    @Around("@annotation(allchive.server.domain.common.aop.distributedLock.DistributedLock)")
    public Object lock(final ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);
        return lockManager.lock(joinPoint, getKey(joinPoint, distributedLock));
    }

    private String getKey(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String[] methodParameterNames = methodSignature.getParameterNames();
        return REDISSON_LOCK_PREFIX
                + distributedLock.lockType().getLockName()
                + "-"
                + createDynamicKey(
                        methodParameterNames, joinPoint.getArgs(), distributedLock.identifier());
    }

    private String createDynamicKey(
            String[] methodParameterNames, Object[] methodArgs, String[] identifiers) {
        List<String> resultList = new ArrayList<>();
        for (String identifier : identifiers) {
            int indexOfKey = Arrays.asList(methodParameterNames).indexOf(identifier);
            Object arg = methodArgs[indexOfKey];
            if (arg == null) {
                throw InvalidLockIdentifierException.EXCEPTION;
            }
            resultList.add(arg.toString());
        }
        return String.join(":", resultList);
    }
}

DistributedLock μ–΄λ…Έν…Œμ΄μ…˜μ˜ μ‹€μ§ˆμ μΈ λ™μž‘μ„ κ΅¬ν˜„ν•œ μ½”λ“œμ΄λ‹€.

AOPμ—μ„œλŠ” ν‚€λ₯Ό λ§Œλ“œλŠ” μž‘μ—…κΉŒμ§€λ§Œ μ§„ν–‰ν•˜κ³ , Lock에 λŒ€ν•œ κ΅¬ν˜„μ€ LockManagerκ°€ μ²˜λ¦¬ν•œλ‹€.

ν‚€λŠ” μ–΄λ…Έν…Œμ΄μ…˜μ— μ£Όμ–΄μ§„ 인자 이름을 κ°€μ§„ νŒŒλΌλ―Έν„° 값을 κ°€μ Έμ™€μ„œ λ™μ μœΌλ‘œ ν‚€λ₯Ό μƒμ„±ν•˜λ„λ‘ κ΅¬ν˜„ν–ˆλ‹€.

이둜써 더 ꡬ체적인 값을 μ΄μš©ν•˜μ—¬ 락을 κ±Έ 수 μžˆμ—ˆκ³ , 락을 μ„€μ •ν•  수 μžˆλŠ” λ²”μœ„λ₯Ό λ„“κ²Œ κ°€μ Έκ°ˆ 수 μžˆμ—ˆλ‹€.

AOPκ°€ μ²˜λ¦¬ν•˜λŠ” μž‘μ—…κ³Ό Lock을 μ‹€μ§ˆμ μœΌλ‘œ μ„€μ •ν•˜λŠ” μž‘μ—…μ„ λΆ„λ¦¬ν•˜κΈ° μœ„ν•΄,

Lock μ„€μ • κ΄€λ ¨ μž‘μ—…μ€ LockManagerκ°€ μ§„ν–‰ν–ˆλ‹€.

 

LockManager

@Component
public interface LockManager {
    public Object lock(ProceedingJoinPoint joinPoint, String key) throws Throwable;
}

락을 λ‹΄λ‹Ήν•˜λŠ” μΈν„°νŽ˜μ΄μŠ€μ΄λ‹€.

 

LockManagerImpl

@Component
@RequiredArgsConstructor
@Slf4j
public class LockManagerImpl implements LockManager {
    private final RedissonClient redissonClient;
    private final TransactionTemplate txTemplate;

    public Object lock(ProceedingJoinPoint joinPoint, String key) throws Throwable {
        RLock rLock = redissonClient.getLock(key);
        log.info("lock : {}", key);
        try {
            boolean available = rLock.tryLock(LOCK_WAIT_TIME, LOCK_LEASE_TIME, TimeUnit.SECONDS);
            if (!available) {
                return false;
            }

            TransactionCallback txCallback =
                    status -> {
                        try {
                            return joinPoint.proceed();
                        } catch (Throwable e) {
                            throw TxTemplateExecutionFailException.EXCEPTION;
                        }
                    };
            return executeTransaction(
                    status -> executeTransaction(txCallback, PROPAGATION_REQUIRED),
                    PROPAGATION_NEVER);
        } catch (InterruptedException e) {
            throw InterruptRedissonException.EXCEPTION;
        } finally {
            try {
                log.info("unlock : {}", key);
                rLock.unlock();
            } catch (IllegalMonitorStateException e) {
                throw AlreadyRedissonUnlockException.EXCEPTION;
            }
        }
    }

    private <T> Object executeTransaction(TransactionCallback<T> block, int propagationBehavior) {
        txTemplate.setPropagationBehavior(propagationBehavior);
        return txTemplate.execute(block);
    }
}

LockManager의 κ΅¬ν˜„μ²΄λ‘œ μ•žμ„œ λ§ν•œ 것과 같이 Lock에 λŒ€ν•œ μž‘μ—…μ„ μˆ˜ν–‰ν•œλ‹€.

μž‘μ—… μˆ˜ν–‰ μˆœμ„œλŠ”

락 μ„€μ • -> tx NEVER -> tx REQUIRED -> μž‘μ—… -> 컀밋 -> 락 ν•΄μ œ

이닀.

 

νŠΈλžœμž­μ…˜ κ΄€λ ¨ μž‘μ—… λͺ¨λ‘κ°€ 락의 λ‚΄λΆ€μ—μ„œ μ§„ν–‰λ˜μ–΄μ•Ό ν•œλ‹€.

이런 상황이 μžˆλ‹€κ³  κ°€μ •ν•΄ 보자.

λ§Œμ•½ 컀밋이 변경은 ν–ˆκ³  락이 ν•΄μ œλ˜μ—ˆμ§€λ§Œ μ»€λ°‹λ˜μ§€ μ•Šμ€ 상황을 λ‹€λ₯Έ μœ μ €μ˜ μž…μž₯μ—μ„œ 보면,

락이 ν•΄μ œλ˜μ—ˆμœΌλ―€λ‘œ 이 μž‘μ—…μ„ 본인이 μˆ˜ν–‰ν•  수 μžˆλŠ” 것이닀.

즉, μ»€λ°‹λœ 데이터λ₯Ό 기반으둜 μž‘μ—…ν•˜λŠ” 게 μ•„λ‹ˆλΌ 컀밋 이전 데이터λ₯Ό 기반으둜 μž‘μ—…ν•˜κ²Œ λœλ‹€.

λ™μ‹œμ„± λ¬Έμ œκ°€ λ°œμƒν•œλ‹€.

락 λ‚΄λΆ€μ—μ„œ νŠΈλžœμž­μ…˜μ΄ μ „λΆ€ μ§„ν–‰λ˜μ§€ μ•ŠμœΌλ©΄ μ΄λŸ¬ν•œ 일이 λ°œμƒν•œλ‹€.

μ΄λŸ¬ν•œ 상황을 νŠΈλžœμž­μ…˜ μž‘μ—… λͺ¨λ‘κ°€ 락 λ‚΄λΆ€μ—μ„œ 이루어지도둝 ν•˜λ©΄ ν•΄κ²° κ°€λŠ₯ν•˜λ‹€.

락 λ‚΄λΆ€μ—μ„œ μž‘μ—…μ„ λͺ¨λ‘ λ‹€ ν•  경우 λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ΄ ν˜„μž¬μ˜ νŠΈλžœμž­μ…˜μ˜ 영ν–₯을 λ°›μ§€ μ•ŠμœΌλ―€λ‘œ, λ™μ‹œμ„±μ„ μœ μ§€ν•  수 μžˆλ‹€.

κ·ΈλŸ¬λ―€λ‘œ 락 λ‚΄λΆ€μ—μ„œ νŠΈλžœμž­μ…˜ μž‘μ—…μ΄ λͺ¨λ‘ μ§„ν–‰λ˜μ–΄μ•Ό ν•œλ‹€.

 

λ˜ν•œ νŠΈλžœμž­μ…˜ NEVER둜 μ‹€ν–‰ ν›„, νŠΈλžœμž­μ…˜ REQUIREDλ₯Ό 또 μ‹€ν–‰ν–ˆλ‹€.

μ΄μœ λŠ” NEVER의 νŠΉμ„±μ— μžˆλ‹€.

NEVERλŠ” μ„ ν–‰ νŠΈλžœμž­μ…˜μ΄ μ—†μŒμ„ 보μž₯ν•΄ μ€€λ‹€.

ν•˜μ§€λ§Œ νŠΈλžœμž­μ…˜μ΄ 없을 λ•Œ, μƒˆλ‘œμš΄ νŠΈλžœμž­μ…˜μ„ λ§Œλ“€μ–΄μ£Όμ§€ μ•ŠλŠ”λ‹€.

κ·ΈλŸ¬λ―€λ‘œ 이어진 νŠΈλžœμž­μ…˜ μž‘μ—…λ“€μ€ 독립적인 νŠΈλžœμž­μ…˜μœΌλ‘œ μ²˜λ¦¬λ˜μ–΄ 버린닀.

둜직 λ‚΄λΆ€ νŠΈλžœμž­μ…˜μ„ ν•˜λ‚˜λ‘œ λ¬Άμ–΄μ£ΌλŠ” μž‘μ—…μ΄ ν•„μš”ν–ˆκ³ , 이λ₯Ό REQUIRED둜 μ²˜λ¦¬ν–ˆλ‹€.

 

μœ„ 두 과정을 톡해 락이 μ„€μ •λ˜κ³  μ„ ν–‰ νŠΈλžœμž­μ…˜μ΄ μ‘΄μž¬ν•˜μ§€ μ•Šμ€ μƒνƒœμ—μ„œ νŠΈλžœμž­μ…˜μ„ μƒμ„±ν•˜μ—¬ λ‘œμ§μ„ μ²˜λ¦¬ν•  수 μžˆμ—ˆλ‹€.

 

μœ„ μ½”λ“œλŠ” μ•„λž˜μ™€ 같이 μ‚¬μš©λ  것이닀.

@DistributedLock(
        lockType = DistributedLockType.ARCHIVING_PIN,
        identifier = {"archivingId", "userId"})
public void execute(Long archivingId, Boolean cancel, Long userId) {
    validateExecution(archivingId, userId, cancel);
    archivingDomainService.updatePin(archivingId, userId, !cancel);
}

 

이와 같은 방법을 톡해 ν•„μžκ°€ μ›ν–ˆλ˜ AOP 기반 뢄산락을 κ΅¬ν˜„ν•  수 μžˆμ—ˆλ‹€.

 

πŸ“Œ 마무리

μœ„μ™€ 같은 λ°©λ²•μœΌλ‘œ 문제라 μƒκ°ν–ˆλ˜ 지점을 극볡할 수 μžˆμ—ˆλ‹€.

 

ν•˜μ§€λ§Œ 이 방법이 μ™„λ²½ν•œ 방법은 μ•„λ‹ˆλΌ μƒκ°ν•œλ‹€.

μ•„λž˜μ™€ 같은 λ¬Έμ œκ°€ μžˆμ„ 수 μžˆλ‹€κ³  μƒκ°ν•œλ‹€.

  1. μ„œλΉ„μŠ€ 둜직 전체에 μ μš©ν•΄μ•Ό ν•œλ‹€.
    νŠΈλžœμž­μ…˜μ΄ μ„€μ •λœ μƒνƒœμ—μ„œ μˆ˜ν–‰λ˜λŠ” 둜직의 μˆ˜ν–‰ μ‹œκ°„μ΄ κΈΈμ–΄μ§„λ‹€λ©΄, 락 μ„€μ • μ‹œκ°„μ΄ κΈΈμ–΄μ§„λ‹€.
    μ΄λŠ” κ³§ ν•΄λ‹Ή 락에 κ΄€λ ¨λœ μ„œλΉ„μŠ€μ— μž‘μ—…μ΄ λΆˆκ°€λŠ₯ν•˜λ‹€λŠ” 말이고,
    이둜 인해 전체적인 μ„œλΉ„μŠ€ ν’ˆμ§ˆμ΄ μ €ν•˜λ  수 μžˆλ‹€.
  2. NEVER μ„€μ • κ°’ λ•Œλ¬Έμ—, λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 μˆ˜ν–‰ 쀑 μΌλΆ€λΆ„μ—λ§Œ μ μš©ν•  수 μ—†λ‹€.
    이λ₯Ό μ²˜λ¦¬ν•˜κΈ° μœ„ν•΄μ„  비동기 처리λ₯Ό 톡해 μΌλΆ€λΆ„λ§Œ μ‹€ν–‰ν•΄μ•Ό ν•œλ‹€.
    λ‘€λ°± 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ κ³ μ•ˆν•œ 방법이 였히렀 λ‘€λ°± κ΄€λ ¨ 문제λ₯Ό λ°œμƒμ‹œν‚¬ 수 μžˆμ–΄μ§„λ‹€.

 

더 쒋은 방법이 μžˆμ„μ§€ κ³ λ―Όν•΄ 봐야겠닀.