부트캠프에서 진행한 스타벅스 클론에서 동시성 이슈를 비관적 락으로 해결하였다. 그런데 낙관적 락으로 해결을 왜 안 했냐는 질문을 받았고 주문에 대한 내용은 중요한 정보이기에 비관적 락으로 해결했다라는 나의 답은 근거가 없는 거라고 답변을 들었기에 2개를 비교하는 실험을 진행하려고 한다.
POST /api/v1/orders (주문 생성)OrderDailyCounter (매장별 일일 주문번호 카운터)1분 동안 100 RPS를 발생 시켰다. 당연히 실패율 0%에 비관적락 이상에 결과를 보여 줄 주 알았는데 아래 표와 같이 이상한 결과를 얻었다.
| 락 방식 | 부하 (req/s) | 총 요청수 | 실패율 | 평균 응답시간 | 최대 응답시간 | 결과 |
|---|---|---|---|---|---|---|
| 비관적 락 | 100 | 6000 | 0% | ~15ms | ~50ms | ✅ 성공 |
| 낙관적 락 | 30 | 1800 | 0.8-2.4% | ~15-18ms | ~487ms | ⚠️ 불안정 |
| 낙관적 락 | 100 | 121 | 99.2% | 39,623ms | 60,003ms | ❌ 실패 |
원인: 부하 테스트시 1개에 매장에만 요청을 보내는 것이 문제였다.
기존 테스트는 모든 주문이 1개에 매장으로 향한다. 이 경우 동시성 이슈가 있던 주문번호 생성 로직은 100% 충돌이 발생한다. 매장은 주문번호 생성을 위해서 주문이 들어오면 counter +1을 한 값을 주문번호로 만든다. 이때 동시에 요청이 들어오면 충돌이 발생했고 비관적 락은 충돌을 일어나는 것을 전제로 만든 로직이었고 낙관적 락은 그렇지 않았던게 문제 였다.
(낙관적 락은 충돌이 일어나지 않고 서로 다른 레코드에 적용해야 하는데 테스트 환경이 그렇지 못한 것이었다.)


// 문제 코드
function makeOrderPayload(email, idx) {
return JSON.stringify({
storeId: 1, -> 원인
pickupType: "STORE_PICKUP",
requestMemo: `주문자: ${email} - 얼음 많이, 샷 연하게 부탁드려요!`,
cardNumber: `1234-5678-9${String(idx).padStart(3, "0")}-0000`,
orderItems: [
{
//...
// 수정 코드, storeId를 고정이 아닌 1~10인 랜덤 값으로 설정 (DB에 매장은 10개만 존재)
function makeOrderPayload(email, idx) {
const randomStoreId = Math.floor(Math.random() * 10) + 1; // 1~10 사이의 랜덤 storeId
return JSON.stringify({
storeId: randomStoreId,
pickupType: "STORE_PICKUP",
requestMemo: `주문자: ${email} - 얼음 많이, 샷 연하게 부탁드려요!`,
cardNumber: `1234-5678-9${String(idx).padStart(3, "0")}-0000`,
orderItems: [
{
k6 코드 변경 후 드디어 원하던 결과를 얻었다.
| 락 방식 | 부하 (req/s) | 총 요청수 | 실패율 | 평균 응답시간 | 최대 응답시간 | 결과 |
|---|---|---|---|---|---|---|
| 비관적 락 | 100 | 6000 | 0% | ~15ms | ~50ms | ✅ 성공 |
| 낙관적 락 | 100 | 6000 | 0% | ~11ms | 107.63ms | ✅ 성공 |
처음 시도에서는 많이 당황했다. java 코드 문제라고 생각해서 AI에게 계속 물어도 해결지 않았던 문제였지만 낙관적 락을 다시 공부하면서 문제를 해결하였다. 이래서 CS 지식 즉 이론이 중요하다고 생각하게 되었다.