π λͺ©μ°¨
Spring Boot MSA κΈ°λ°μΌλ‘ νκ³΅κΆ ν°μΌν μλΉμ€λ₯Ό ꡬννλ μ€, νκ³΅κΆ μλ§€ μ λ°μν μ μλ λμμ± μ΄μλ₯Ό ν΄κ²°νκΈ° μν΄ λκΈ°μ΄ μλΉμ€μ Redisμ Redisson λΌμ΄λΈλ¬λ¦¬λ₯Ό μ΄μ©ν΄ λΆμ° λ½μ μ μ©νλ€.
μ΄κΈ° ꡬν λΉμ λκΈ°μ΄ μλΉμ€κ° μ¬μ©μ μμ² μμλ₯Ό 보μ₯νκ³ μμμ§λ§,
'λκΈ°μ΄ μλΉμ€μ μ μ©λ λμμ± μ μ΄κ° νκ³΅νΈ μλΉμ€μμμ μ’μ μκΉμ§ μμ νκ² λ³΄μ₯ν΄ μ€ μ μμκΉ?'λΌλ
μλ¬Έμ κ°μ§κ² λλ©΄μ μλΉμ€ κ° λμμ± μ μ΄μ μ± μ λΆλ¦¬μ λν λ¬Έμ μ μ μΈμνμ¬ κ°μ νλ κ³Όμ μ μ λ¦¬ν΄ λ³΄μλ€.
π λκΈ°μ΄ μλΉμ€μλ§ μ μ©λ λμμ± μ μ΄μ μλ¬Έμ κ°μ§κ³ λ κ³κΈ°
νκ³΅νΈ μλ§€ μλΉμ€μ κΈ°λ³Έ νλ¦μ λ€μκ³Ό κ°λ€.
1οΈβ£ μ¬μ©μκ° νκ³΅νΈ μμ½ μμ²
2οΈβ£ λκΈ°μ΄ μλΉμ€μμ ν΄λΉ ν곡νΈμ μ’μ μ μ‘°ν
3οΈβ£ μ’μ μκ° μΆ©λΆνλ©΄ λκΈ°μ΄μ μ§μ (μμ² μμ 보μ₯)
4οΈβ£ μμμ λ°λΌ νκ³΅νΈ μλΉμ€μ μ’μ μ°¨κ° μμ²(λκΈ° νΈμΆ)
5οΈβ£ μ’μ μ°¨κ° ν μμ½ μμ± μμ²
μ΄κΈ° ꡬν λΉμμλ λκΈ°μ΄ μλΉμ€κ° μ¬μ©μ μμ² μμλ₯Ό 보μ₯νκ³ μμκΈ° λλ¬Έμ νκ³΅νΈ μλΉμ€ λ΄λΆμμλ λ³λμ λμμ± μ μ΄ μμ΄ μ’μ μλ₯Ό μ°¨κ°ν΄λ μμ ν κ²μ΄λΌκ³ νλ¨νλ€.

...
if(seatCount <= remainingSeats) {
// νκ³΅νΈ μ’μ μ°¨κ°
flightClient.decreaseSeats(flightId, seatCount);
log.info("λκΈ°μ΄ μ μ μ μ±κ³΅ νμ΅λλ€. λ¨μ μ’μ μ: {}", remainingSeats - seatCount);
rankOps.remove(key, topUser);
producerService.sendReserveSuccess(flightId, userId, seatCount);
return QueueResponseDto.of(EventStatusEnum.SUCCESS, "λκΈ°μ΄ μ μ μ μ±κ³΅νμ΅λλ€.");
} else {
log.info("λ¨μ μ’μμ΄ μμ΅λλ€. λ¨μ μ’μ μ: {}", remainingSeats);
rankOps.remove(key, topUser);
deleteExistReserve(flightId, userId);
return QueueResponseDto.of(EventStatusEnum.FAILED, "λ¨μ μ’μμ΄ μμ΅λλ€.");
}
ν μ€νΈ μ€ λ°μν λ¬Έμ λ νΉμ ν μ€λ₯λ μμμ§λ§, μ’μ μμ κ°μμ μ¦κ°μ λν λΉλκΈ° μ²λ¦¬λ‘μ μ ν κ³Όμ μμ λ‘μ§μ μ κ²νλ μ€ λ€μκ³Ό κ°μ μλ¬Έμ΄ λ€μλ€.
'λκΈ°μ΄ μλΉμ€μμ μ¬μ©μ μμ² μμλ₯Ό 보μ₯ν΄ μ€λ€κ³ ν΄λ, νκ³΅νΈ μλΉμ€ λ΄λΆμ μ’μ μ κ°μ μμ κΉμ§ μμ νκ² λ³΄μ₯ν μ μμκΉ?'
μ΄λ¬ν μλ¬Έμ λ°νμΌλ‘ μλΉμ€ κ° μν μ λ€μ μ κ²ν κ²°κ³Ό, μ ν΄μ§ μμλλ‘ μ¬μ©μ μμ²μ κ΄λ¦¬νλ κ²μ λκΈ°μ΄ μλΉμ€μ μ± μμ΄μ§λ§ μ’μ μμ λ³κ²½μ ν곡 μλΉμ€μμ μ§μ μ μ΄ν΄μΌ ν μ± μμ΄κΈ° λλ¬Έμ μ’μ μμ μμμ±μ 보μ₯νλ λͺ©μ μ λμμ± μ μ΄κ° λ³λλ‘ νμνλ€λ κ²°λ‘ μ λμΆνκ² λμλ€.
π λκΈ°μ΄ μλΉμ€μ νκ³΅νΈ μλΉμ€κ° μ± μμ λΆλ¦¬
λκΈ°μ΄ μλΉμ€μλ λ¬λ¦¬ ν곡 μλΉμ€μ νκ³΅νΈ μ’μ μλ DB λ΄μμ λ³κ²½λλ 곡μ μμμΌλ‘ μ¬λ¬ μ€λ λλ νλ‘μΈμ€κ° λμμ μ κ·Όν κ²½μ°, λμμ± μ μ΄κ° λμ§ μλλ€λ©΄ μ’μ μκ° μλͺ» μ°¨κ°λμ΄ λ°μ΄ν° μ ν©μ± λ¬Έμ κ° λ°μν μ μλ€.
* μ’μ μμ μ°¨κ°λΏλ§ μλλΌ, μ’μ μμ 볡ꡬ λ‘μ§λ λ§μ°¬κ°μ§μ΄λ€.
μ΄μ λ°λΌ λ°μ΄ν°μ μΌκ΄μ±μ μ μ§νκΈ° μν΄ νκ³΅νΈ μλΉμ€ λ΄λΆμμ μ’μ μλ₯Ό λ³κ²½νλ λ‘μ§μ Redissonμ μ¬μ©νμ¬ λΆμ° λ½μ μ μ©νκΈ°λ‘ κ²°μ νλ€.
public class DistributedLockAop {
...
// μμΈ μ²λ¦¬ λ° λ½ νλμ μ¬μλνλ λ‘μ§
int retryCount = 0;
while (retryCount < distributedLock.maxRetryCount()) {
try {
boolean available = rLock.tryLock(distributedLock.waitTime(),
distributedLock.leaseTime(), distributedLock.timeUnit());
if (available) {
log.info("[DistributedLock]: Lock acquired with key: {}", key);
try {
return aopForTransaction.proceed(joinPoint);
} finally {
if (rLock.isLocked() && rLock.isHeldByCurrentThread()) {
log.info("[DistributedLock]: Lock released with key: {}", key);
rLock.unlock();
}
}
} else {
log.info("[DistributedLock]: Lock acquisition failed with key: {}", key);
retryCount++;
Thread.sleep(distributedLock.retryDelay());
}
} catch (InterruptedException e) {
log.error("[DistributedLock]: Lock interrupted", e);
Thread.currentThread().interrupt();
throw e;
}
}
log.warn("[DistributedLock]: Lock acquisition failed after {} retries with key: {}", retryCount, key);
throw new LockFailedAfterRetryException();
}
}
// μ’μ μ μ°¨κ°
@DistributedLock(key = "#flightId.toString()")
public void decreaseSeats(UUID flightId, Integer requiredSeats) {
...
}
// μ’μ μ 볡ꡬ
@DistributedLock(key = "#flightId.toString()")
public void increaseSeats(UUID flightId, Integer requiredSeats) {
...
}
AOP κΈ°λ°μΌλ‘ @DistibutedLock μ΄λ Έν μ΄μ μ μ μΈνμ¬ νκ³΅νΈ IDμ λ½μ κ±Έμ΄ λμμ± μ μ΄λ₯Ό ν μ μλλ‘ κ΅¬ννλ€. AOP κΈ°λ°μΌλ‘ λΆμ° λ½μ ꡬννλ κ³Όμ μ μλ ν¬μ€ν μμ νμΈν μ μλ€.
[Redis] μ΄μ»€λ¨Έμ€ μλΉμ€ - Redissonμ μ΄μ©ν λΆμ° λ½ κ΅¬νμΌλ‘ λμμ± μ΄μ ν΄κ²°
π λͺ©μ°¨ SpringBootλ₯Ό κΈ°λ°μΌλ‘ μ΄μ»€λ¨Έμ€ μλΉμ€λ₯Ό ꡬννλ©΄μ λμμ± μ΄μκ° λ°μνλ€.Redisμ RedissonλΌμ΄λΈλ¬λ¦¬λ₯Ό μ΄μ©ν΄ λμμ± μ΄μλ₯Ό ν΄κ²°νλ λ°©λ²μ λν΄ μμ보μ. π λμμ± μ μ΄λ? λμ
zzudev.tistory.com
π Redisson λΆμ° λ½μ μ μ©
μμμ μΈκΈνλ―μ΄ λκΈ°μ΄ μλΉμ€μ νκ³΅νΈ μλΉμ€μ μ± μμ λΆλ¦¬νμ¬ νκ³΅νΈ μλΉμ€ λ΄λΆμ μ’μ μ°¨κ° λ° λ³΅κ΅¬ λ‘μ§μ λΆμ° λ½μ μ μ©νμλ€. κ·Έλ¦¬κ³ λΆμ° λ½μ μ μ© μ κ³Ό ν, λμμ± μ μ΄κ° μ μμ μΌλ‘ λμνλμ§ νμΈνκΈ° μν΄ λ€μκ³Ό κ°μ 쑰건μΌλ‘ ν μ€νΈλ₯Ό μ§ννλ€.
π‘ ν μ€νΈ μ§ν 쑰건 π‘
βοΈ νκ³΅νΈ μ’μ μ: 10,000κ°
βοΈ λμ μ¬μ©μ: 100λͺ
βοΈ μμ² μ: 10μ΄ λμ λμ μμ²

μλμ κ°μ΄ μ’μ μ°¨κ° μ€ν¬λ¦½νΈλ₯Ό μμ±νμ¬ k6λ‘ ν μ€νΈλ₯Ό μ§ννλ€.
import http from 'k6/http';
import { check, fail } from 'k6';
export let options = {
vus: 100,
duration: '10s',
};
const token = 'Bearer '; // token μ
λ ₯ νμ
const flightId = '2129373a-1741-45a4-a2b0-56914adcc906';
export default function () {
const url = `http://localhost:19091/internal/v1/flights/${flightId}/seats/decrease?requiredSeats=1`;
const headers = {
Authorization: token,
};
let res = http.put(url, null, { headers });
const success = check(res, {
'status is 200': (r) => r.status === 200,
});
if (!success) {
console.error(`β μμ² μ€ν¨ | μνμ½λ: ${res.status} | μλ΅: ${res.body}`);
}
}


- ν μ€νΈ μλλ¦¬μ€ : μ΄ 428건μ μμ²μ΄ λͺ¨λ μ±κ³΅νμμΌλ μ’μμ μλ 10,000κ° → 9,572κ°κ° λμ΄μΌ νλ€.
- ν μ€νΈ κ²°κ³Ό : μ’μ μ 10,000κ° → 9,949κ°, μμ² μμ λ€λ₯Έ 51κ°μ μ’μ κ°μ *
λΆμ° λ½μ μ μ©νκΈ° μ μλ μ΄ 428건μ μμ²μ΄ νμΈλμκ³ , μμ²μ΄ λͺ¨λ μ±κ³΅νμΌλ μ’μμ μλ 428κ°κ° κ°μν 9,572κ°κ° λμ΄μΌ νλ€. νμ§λ§ μμκ³Ό λ€λ₯΄κ² μ’μμ μλ 10,000κ° → 9,949κ°λ‘ 51κ°μ μ’μλ§ κ°μνλ©° λ°μ΄ν°μ μ ν©μ±κ³Ό μμμ±μ΄ κΉ¨μ§ κ²μ νμΈν μ μμλ€.
βΌοΈ μ΄λ²μλ, λΆμ° λ½μ μ μ© ν ν, λμΌν 쑰건μΌλ‘ ν μ€νΈλ₯Ό μ§νν λ€ κ²°κ³Όλ₯Ό λΉκ΅ν΄ 보μ.



- ν μ€νΈ μλλ¦¬μ€ : μ΄ 2,971건μ μμ²μ΄ λͺ¨λ μ±κ³΅νμμΌλ μ’μμ μλ 10,000κ° → 7,029κ°κ° λμ΄μΌ νλ€.
- ν μ€νΈ κ²°κ³Ό : μ’μ μ 10,000κ° → 7,029κ°, μμ²μ μλ§νΌ μ’μ κ°μ *
λΆμ° λ½μ μ μ©ν νμλ μ΄ 2,971건μ μμ²μ΄ νμΈλμκ³ , μμ²μ΄ λͺ¨λ μ±κ³΅νμΌλ μ’μμ μλ 2,971κ°κ° κ°μν 7,029κ°κ° λμ΄μΌ νλ€. λ‘κ·Έλ₯Ό ν΅ν΄ λ½ νλ λ° ν΄μ κ° μ μμ μΌλ‘ λλ κ²μ νμΈνκ³ , μ’μμ μλ μμν κ²°κ³Όμ κ°μ΄ 10,000κ° → 7,029κ°λ‘ μ νν μμ²μ μλ§νΌ κ°μν κ²μ νμΈν μ μμλ€.
π‘ μμμ μ§νν ν μ€νΈ κ²°κ³Όλ₯Ό μ 리νμλ©΄ λ€μκ³Ό κ°λ€. π‘
βοΈ μ΄κΈ° μ’μ μ: 10,000κ° / ν μ€νΈ 쑰건: λμ 100λͺ , 10μ΄κ° μμ² / λꡬ: k6 βοΈ
β λΆμ° λ½ μ μ© μ
• μμ² μ: 428건
• μμ² μ±κ³΅λ₯ : 100%
• κΈ°λ μ’μ μ: 10,000 - 428 = 9,572κ°
• μ€μ μ’μ μ: 9,949κ° (51κ°λ§ μ°¨κ°λ¨)
→ μλ΅μ μ±κ³΅μ΄λΌ νμ§λ§, μ€μ DBμ λ°μλμ§ μμ
→ λ°μ΄ν° μ ν©μ± λ¬Έμ λ°μ
β λΆμ° λ½ μ μ© ν
• μμ² μ: 2,971건
• μμ² μ±κ³΅λ₯ : 100%
• κΈ°λ μ’μ μ: 10,000 - 2,971 = 7,029κ°
• μ€μ μ’μ μ: 7,029κ° (μ μ μ°¨κ°)
→ λ‘κ·Έμμ λ½ νλ λ° ν΄μ μ μ νμΈ, μ€μ DBμ μ±κ³΅μ μΌλ‘ λ°μ
→ λ°μ΄ν° μ ν©μ±κ³Ό μμμ± λ³΄μ₯ νμΈ
β κ²°κ³Ό μ 리
• μλΉμ€ κ²½κ³λ₯Ό λͺ νν μ΄ν΄ν΄μΌ ν¨
• μμ 보μ₯κ³Ό μμμ± λ³΄μ₯μ λ³κ°μ λ¬Έμ
• 곡μ μμμ λν μ ν©μ± 보μ₯ μ± μμ ν΄λΉ μμμ μ§μ λ€λ£¨λ μλΉμ€μ μμ΄μΌ ν¨
• λΆμ° λ½μ ν΅ν λμμ± μ μ΄λ νμμ
* μ’μ μμ μ¦κ°μ λν΄μλ κ°μ λ°©μμΌλ‘ ν μ€νΈλ₯Ό μ§ννμκ³ , μ μμ μΌλ‘ λμνλ κ²μ νμΈν μ μμλ€.
μ΄λ κ² λ¬Έμ μ κ°μ ν, ν μ€νΈλ₯Ό μ§ννμ¬ κ²°κ³Όλ₯Ό νμΈνκ³ μ μ μλνλ κ²κΉμ§ νμΈν΄ 보μλ€. μ΄λ₯Ό ν΅ν΄ μλΉμ€ κ° κ²½κ³λ₯Ό λͺ νν μ΄ν΄νκ³ , λ¨μν μμ² μμλ₯Ό μ μ΄νλ€κ³ ν΄μ λ°μ΄ν° μ ν©μ±μ΄ 보μ₯λμ§ μλλ€λ μ κ³Ό μ€μ μμμ μ μ΄νλ μͺ½μμ λͺ μμ μΌλ‘ λμμ± μ μ΄λ₯Ό ν΄μΌ νλ€λ μ μ λν΄ μ‘°κΈ λ κΉμ΄ μ΄ν΄νκ² λμλ€.
μ¬κΈ°κΉμ§ μλΉμ€ κ° λμμ± μ μ΄μ μ± μ λΆλ¦¬μ λν λ¬Έμ μ μ μΈμκ³Ό ν΄κ²°κ³Όμ μ λν΄ μ λ¦¬ν΄ λ³΄μλ€.