지난 포스팅에는 API Gateway를 생성하고, Gateway 엔드포인트를 통해 ALB를 거쳐 실제 ECS 서비스의 API를 호출하도록 하는 설정까지 완료했다.
모든 엔드포인트에 대한 배포는 완료되었고, 남은 배포 작업들을 리스트업해보았다.
- AWS 코드 디플로이 통해서 BLUE/GREEN 배포 하기
- ECS에 Grafana/Prometheus 설치 및 메트릭 수집 및 대시보드 구성
- Auto Scailing (트래픽 몰리면 서버 늘어나도록 설정)
한 열흘 올려놨다고 요금이 이정도로 나온다.. (MSK Serverless 클러스터 중간에 삭제하고, 다른 EC2 인스턴스에 있는 Kafka 쓰도록 바꾸었는데도..)

그러나 현재로서는 AWS 요금이 너무 나왔기도 하고.. 현재의 Rolling Update에서 BLUE/GREEN을 적용하려면 기존 ECS 서비스들을 다 뜯어고치는 작업이 필수적이었다. 무엇보다 지금처럼 모든 서비스가 실서버에서 잘 돌아가기까지 걸린 시간과 과정이 꽤 복잡했기에 괜히 더 건드렸다가 꼬여서 서버에 이상이 생기는 상황은 가능하면 피하고 싶었다. (지원금 플렉스..)
그리고 꼭 MSA에서만 BLUE/GREEN 배포를 할 수 있는 것이 아니기 때문에 이번 배포에서는 현재의 롤링 배포로만 적용하기로 하였다. (롤링 업데이트 환경에서도 무중단 배포는 정상적으로 동작하기 때문에)
그리하여 이번 포스팅에서는 AWS ECS 환경에서 Auto Scailing을 적용해보는 과정을 정리해보고자 한다.
ECS Service에 Auto Scailing 적용하기
Auto Scailing은 가장 트래픽이 많이 몰릴 것으로 예상되는 queue-service(대기열 서비스)에서 적용해보기로 했다. 절차는 아래와 같다.
- ECS 클러스터 접속 -> 해당 서비스 클릭 -> 서비스 업데이트
- 서비스 자동 크기 조정(Service auto scaling) 영역에서 "서비스 자동 크기 조정 사용" 체크
- 최소 태스크 개수 : 1 (평소), 최대 태스크 개수 : 5 (트래픽이 몰릴 때)로 설정
- 하단 조정 정책 추가 버튼 선택
- 대상 추적 체크 : 목표값을 유지하도록 자동 조정
- 정책 이름 : queue-service-cpu-scaling (예시)
- ECS 서비스 지표 : ECSServiceAverageCPUUtilization 선택 (CPU 기준)
- 대상 값 : 30 (평균 CPU 사용률을 30%로 유지, 30% 넘으면 Task 추가) // 테스트용
- 확장 휴지 기간(Scale-out cooldown period) : 60초 (너무 빠른 확장을 방지하기 위해 확장 후 60초 대기)
- 축소 휴지 기간(Scale-in cooldown period) : 300초 (안정화 위해 축소 후 300초 대기)
- 업데이트 버튼 클릭
Auto Scailing 부하 테스트 결과 분석
현재 Queue 서비스의 대상 그룹에서는 Healthy 상태인 태스크 개수(IP)가 처음에는 1개로 설정되어 있음을 확인할 수 있다.

테스트 환경
- 인프라: AWS ECS (Fargate, t3.small급), AWS ALB, API Gateway, ElastiCache (Redis)
- ECS 서비스 스펙 : 0.5 vCPU / 1GB RAM
- 테스트 도구: k6, AWS CloudWatch
- 시나리오 (Stress Test):
- 목표: 타임딜 오픈 직후 트래픽 폭주 상황 가정
- 과정: 8분 동안 가상 사용자(VUs)를 0명에서 1,000명까지 점진적으로 증가
- 검증: 대기열 진입 성공률, 응답 속도(2초), Auto Scaling 동작 여부
테스트 개요
총 지속 시간: 8분
가상 사용자(VUs): 0명에서 시작해 1,000명까지 증가 (Ramp-up)
테스트 엔드포인트 : 대기열 진입, 대기열 순번 조회 API
k6 테스트 시나리오 코드
터미널에서의 실행 명령어는 아래와 같다.
K6_WEB_DASHBOARD=true k6 run timedeal-rush.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
import { htmlReport } from "https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js";
import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.1/index.js';
// ============================================
// 1. 커스텀 메트릭 정의 (결과 측정용)
// ============================================
const errorRate = new Rate('errors');
const enterQueueSuccess = new Rate('enter_queue_success');
const USER_ID_OFFSET = 10000;
// ============================================
// 2. 테스트 설정 (시나리오 정의)
// ============================================
export const options = {
systemTags: ['status', 'method', 'url', 'name', 'group', 'check', 'error', 'iter', 'scenario', 'vu'],
scenarios: {
timedeal_rush: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '1m', target: 300 }, // 1분 동안 300명까지 워밍업
{ duration: '1m', target: 800 }, // 1분 동안 800명까지 증가
{ duration: '5m', target: 1000 }, // [핵심] 5분 동안 1000명 유지 (이때 스케일 아웃 발생!)
{ duration: '1m', target: 0 }, // 1분 동안 종료
],
gracefulRampDown: '10s',
},
},
// 임계값 설정 (테스트 실패 기준)
thresholds: {
'http_req_duration': ['p(95)<3000'], // 95% 요청이 3초 이내
'http_req_failed': ['rate<0.05'], // 에러율 5% 미만
'errors': ['rate<0.05'],
},
};
// ============================================
// 3. 환경 변수 (수정 필요)
// ============================================
const BASE_URL = __ENV.BASE_URL || 'https://{api-gate-way번호}.execute-api.ap-northeast-2.amazonaws.com';
const PRODUCT_ID = __ENV.PRODUCT_ID || '1e76c9ef-ed5a-4707-9aa2-bc3c50693240';
// ============================================
// 4. JWT 토큰 생성 (간소화 버전)
// ============================================
function generateTestToken(userId) {
// 실제로는 백엔드에서 발급받아야 하지만,
// 테스트용으로 간단히 userId만 포함
// TODO: 실제 JWT 발급 API 호출로 변경
return `Bearer test-token-user-${userId}`;
}
// ============================================
// 5. 메인 테스트 함수 (각 VU가 실행)
// ============================================
export default function () {
// 각 가상 유저(VU)는 고유한 userId를 가짐
// const userId = __VU; // Virtual User ID (1, 2, 3, ...)
const userId = __VU + USER_ID_OFFSET;
// JWT 토큰 생성
const token = generateTestToken(userId);
// ============================================
// 테스트 1: 대기열 진입 요청
// ============================================
const enterQueuePayload = JSON.stringify({
productId: PRODUCT_ID,
});
const enterQueueParams = {
headers: {
'Content-Type': 'application/json',
// 'Authorization': `Bearer test-token-user-${userId}`,
'X-User-Id': `${userId}`,
'X-User-Role': 'USER',
'X-User-Email': `user_${userId}@test.com`
},
timeout: '10s', // 10초 타임아웃
tags: { name: '01_Enter_Queue' },
};
const enterResponse = http.post(
`${BASE_URL}/api/v1/queues/enter`,
enterQueuePayload,
enterQueueParams
);
// 409(이미 대기 중)는 에러로 치지 않기 위한 로직
if (enterResponse.status === 409) {
// 이미 대기열에 있다면, 실패가 아니라 "Pass"로 간주하고 루프 종료
// (Polling 단계로 넘어갈 수도 있지만, 토큰이 없으므로 여기서 멈춤)
return;
}
// ★ 수정 포인트: 응답 시간 체크를 분리했습니다.
// 응답 시간이 1초 넘어도 201이면 일단 "기능적 성공"으로 칩니다.
const isStatusOk = enterResponse.status === 201;
// 응답 검증
// 성능 체크용 (로그만 찍거나 메트릭엔 반영하되 로직은 진행)
check(enterResponse, {
'응답 시간 < 2초': (r) => r.timings.duration < 2000,
});
const isTokenOk = check(enterResponse, {
'대기열 진입 성공 (201)': (r) => r.status === 201,
'토큰 발급됨': (r) => {
try {
const body = JSON.parse(r.body);
return body.data && body.data.token;
} catch {
return false;
}
},
});
// 기능적으로 실패했을 때만 에러 처리
if (!isStatusOk || !isTokenOk) {
errorRate.add(1);
console.error(`[VU ${userId}] 진입 실패: ${enterResponse.status} - ${enterResponse.body}`);
return; // 진짜 실패했으니 여기서 종료
}
// 성공했으니 메트릭 기록하고 폴링으로 넘어감
enterQueueSuccess.add(1);
let queueToken = null;
try {
// 1. 응답 본문이 비어있는지 먼저 확인
if (!enterResponse.body) {
throw new Error("응답 본문(Body)이 비어있습니다!");
}
// 2. 파싱 시도
const responseBody = JSON.parse(enterResponse.body);
// 3. 토큰 추출 시도
if (responseBody.data && responseBody.data.token) {
queueToken = responseBody.data.token;
} else {
// JSON은 맞는데 token 필드가 없는 경우
throw new Error(`토큰 없음! 응답 구조 확인 필요: ${enterResponse.body}`);
}
const rank = responseBody.data.rank;
console.log(`[VU ${userId}] 대기열 진입 성공! 순번: ${rank}, 토큰: ${queueToken.substring(0, 8)}...`);
// ============================================
// 테스트 2: 대기 순번 조회 (Polling 시뮬레이션)
// ============================================
sleep(1); // 1초 대기 (실제 사용자가 기다리는 시간)
const rankParams = {
headers: {
// 'Authorization': token,
'X-Queue-Token': queueToken,
'X-User-Id': `${userId}`,
'X-User-Role': 'USER',
'X-User-Email': `user_${userId}@test.com`
},
// ★ 여기가 핵심: 그래프에서 조회 요청은 따로 표시됨
tags: { name: '02_Check_Rank' },
};
const rankResponse = http.get(
`${BASE_URL}/api/v1/queues/rank?productId=${PRODUCT_ID}`,
rankParams
);
const rankSuccess = check(rankResponse, {
'순번 조회 성공 (200)': (r) => r.status === 200,
});
if (!rankSuccess) {
// ★ 수정된 부분: rankRes -> rankResponse로 변수명 수정
console.error(`[VU ${userId}] 조회 실패! Status: ${rankResponse.status} / Msg: ${rankResponse.body}`);
errorRate.add(1);
} else {
console.log(`[VU ${userId}] 순번 조회 성공! 순번: ${rank}, 토큰: ${queueToken.substring(0, 8)}...`);
}
} catch (error) {
console.error(`[VU ${userId}] 응답 파싱 실패: ${error}`);
// 서버가 JSON 대신 뭘 보냈는지 눈으로 확인하는 로그
console.error(`---------------------------------------------------`);
console.error(`[VU ${userId}] 파싱/로직 에러 발생!`);
console.error(`[에러 메시지]: ${error}`);
console.error(`[서버 응답 상태]: ${enterResponse.status}`);
console.error(`[서버 응답 본문]: ${enterResponse.body}`); // <--- 이걸 봐야 범인을 잡습니다.
console.error(`---------------------------------------------------`);
errorRate.add(1);
}
// 요청 간 간격 (1~3초 랜덤)
sleep(Math.random() * 2 + 1);
}
// ============================================
// 6. 테스트 종료 후 요약 출력
// ============================================
export function handleSummary(data) {
return {
// 1. 콘솔에는 텍스트 요약 출력
'stdout': textSummary(data, { indent: ' ', enableColors: true }),
// 2. 브라우저로 볼 수 있는 HTML 파일 생성 (이게 핵심!)
'summary.html': htmlReport(data),
};
}
K6 테스트 결과 로그
✗ 응답 시간 < 2초
↳ 92% — ✓ 2106 / ✗ 160
✓ 대기열 진입 성공 (201)
✓ 토큰 발급됨
✓ 순번 조회 성공 (200)
checks.....................: 98.23% ✓ 8904 ✗ 160
data_received..............: 133 MB 276 kB/s
data_sent..................: 38 MB 79 kB/s
enter_queue_success........: 100.00% ✓ 2266 ✗ 0
✓ errors.....................: 0.00% ✓ 0 ✗ 0
http_req_blocked...........: avg=165.72µs min=0s med=1µs max=631.61ms p(90)=1µs p(95)=2µs
http_req_connecting........: avg=67.79µs min=0s med=0s max=487.99ms p(90)=0s p(95)=0s
✓ http_req_duration..........: avg=1.15s min=20.24ms med=1.21s max=6.04s p(90)=1.78s p(95)=1.99s
✗ http_req_failed............: 98.43% ✓ 284829 ✗ 4532
http_req_receiving.........: avg=155.34µs min=15µs med=60µs max=875.28ms p(90)=153µs p(95)=236µs
http_req_sending...........: avg=147.4µs min=27µs med=107µs max=106.01ms p(90)=233µs p(95)=300µs
http_req_tls_handshaking...: avg=96.35µs min=0s med=0s max=255.93ms p(90)=0s p(95)=0s
http_req_waiting...........: avg=1.15s min=4.62ms med=1.21s max=6.04s p(90)=1.78s p(95)=1.99s
http_reqs..................: 289361 602.818631/s
iteration_duration.........: avg=1.19s min=21.28ms med=1.22s max=8.85s p(90)=1.8s p(95)=2.07s
iterations.................: 287095 598.097929/s
vus........................: 8 min=2 max=1000
running (8m00.0s), 0000/1000 VUs, 287095 complete and 0 interrupted iterations
timedeal_rush ✓ [======================================] 0000/1000 VUs 8m0s
주요 지표 설명
위 테스트 결과에 대한 요약은 아래와 같다.

http_req_duration (응답 속도)
응답 받기까지의 속도로는 평균 1.15초가 걸렸다.
P(95)는 전체 요청 중 느린 편에 속하는 상위 95% 유저로, 1,000명이 몰린 혼잡한 상황에서도 95%의 유저는 2초 이내(1.99초)에 응답을 받았다는 의미이다. 이는 대기열 시스템의 병목 현상을 어느 정도 잘 막아주었다고 볼 수 있는 신호이다.
enter_queue_success
대기열 토큰 발급 로직이 단 한 번도 실패하지 않았음을 확인할 수 있다.
errors
errors 지표가 0으로 측정된 것은 단 한 명의 유저도 로직 상의 오류(토큰 발급 실패, 진입 실패 등)를 겪지 않았다는 의미로 시스템 비즈니스 로직의 안정성을 확인할 수 있는 지표이다.
http_req_failed (요청 실패율)
요청 실패율이 98%로 꽤나 높은 비율을 보이고 있는데, 이는 k6가 기본적으로 200~299 상태 코드가 아니면 실패로 간주하기 때문이다.
대기열 시스템 특성상 Polling 과정이나 대기 상태에서 409(Conflicted) 등의 코드를 받는 등의 상황이 있기에 실패로 집계될 수 있다.
(뿐만 아니라 k6의 default function은 루프 형태로 무한 반복한다. 똑같은 ID로 대기열 진입을 다시 요청한다는 뜻이다.) 따라서, 8분 동안 1명의 유저가 1번 성공하고, 나머지 100번은 409 에러를 받게 된다.
또는 이미 대기열에 있는 유저가 다시 진입하려 할 때 409 Conflict를 반환하기에 이 또한 에러로 집계될 수 있다.

따라서 위의 요청 실패율은 다음과 같은 의미로 해석될 수 있다.
k6의 http_req_failed는 98%지만, 실제 비즈니스 로직 성공 여부를 체크하는 enter_queue_success와 errors 지표는 정상적으로 동작했다. 서버가 500 에러를 낸게 아닌, 대기열 로직에 따라 트래픽을 제어하며 409 응답을 내보낸 것이므로 시스템의 동작에는 큰 이상이 없다고 볼 수 있다.


Auto Scailing 동작 검증(안정화) 확인
CloudWatch CPU 사용률 그래프를 보면 파란 선이 빨간 점선(80%)을 뚫고 급상승하는 구간들이 보인다. 이는 아래 2번째 사진의 ECS 이벤트 로그(Successfully set desired count to 5)가 찍힌 시간대와 겹친다.
CPU 알람을 감지하고, ECS가 스스로 Desired Count를 5로 변경하여 대응했음을 확인할 수 있다.
아래 지표 사진에서 5시 29분 시점에는 CPU 사용률이 95% 이상으로 치솟는 것을 확인할 수 있다. 이러한 급상승하는 구간 이후에 CPU 그래프는 20% 미만으로 뚝 떨어지는데 이는 기존에 1개로 실행되던 서버가 5개로 늘어나서 일을 나눠가졌기 때문이다.
결론적으로는 트래픽이 줄어들어서 CPU가 내려간 게 아니라, 트래픽은 그대로인데 Auto Scailing 설정을 통한 Scale Out 덕분에 CPU점유율이 내려간 것이다.
이는 초기 부하로 CPU가 임계치를 넘었으나, 스케일링 이후 CPU 부하가 분산되어 10% 미만의 CPU 사용률로 안정화된 패턴을 증명했다고 볼 수 있다.



ECS 서비스(Queue-Service) 지표 확인
트래픽 몰리는 상황 (트래픽 폭주와 CPU 임계치 초과)

- 시점: 05:32 (부하 테스트 피크 구간)
- 현상: TaskCpuUtilization 최대값이 98.01% 까지 치솟음.
- 해석: 1,000명의 유저가 동시에 몰리자, 단일 태스크(서버 1대)였던 서버(Task)가 처리 한계에 도달하여 CPU가 포화 상태가 되었다. 이 상태가 지속되면 서버 다운(502 Bad Gateway)이나 응답 지연이 발생할 수 있다.
시스템 대응 (Auto Scaling 트리거 및 확장)


- 시점: 05:31 (CPU 알람 발생 직후)
- 현상:
- 파란선 (DesiredTaskCount): AWS가 "서버 5대로 늘려"라고 명령을 내림 (1 -> 5).
- 주황선 (RunningTaskCount): 실제로 컨테이너가 하나둘씩 켜지면서 1개에서 5개로 실행 중인 서버가 늘어남.
- 해석: CPU 사용률이 임계치(예: 30% 또는 50%)를 넘자, CloudWatch Alarm이 울리고 ECS Service가 자동으로 스케일 아웃(Scale-out)을 발동하였다. (Fargate의 빠른 프로비저닝 능력 덕분에 트래픽 유입 후 약 1~2분 내에 확장이 완료되었음)
Scale Out 직후 안정화 (부하 분산)


- 시점: 05:33 (Scale-Out 완료 직후, 실행 서버가 5대가 된 시점)
- 현상:
- TaskCpuUtilization(CPU 사용률) 평균값이 23.6%에서 3.97%로 급격히 하락
- 그래프 파형이 100% 천장을 찍다가 바닥으로 뚝 떨어짐
- 해석 :
- 트래픽(k6 VUs)은 여전히 1,000명으로 동일하나, 하지만 서버가 1대에서 5대로 늘어나면서 한 대가 감당하던 부하가 1/5로 줄어들었다.
- 서버가 5대로 늘어나면서 병목이 해소되어, 1,000명의 동시 접속 상황에서도 목표했던 2초 이내 응답 속도를 준수할 수 있게 됨
- 개별 서버는 평균 CPU 4%대를 유지하며 안정적으로 서비스를 제공할 수 있게 되었다.
개념 정리 (AWS에서의 태스크)
이와 같은 부하 테스트와 설정 등 처리들을 하고 나니 ECS 서비스에서의 Task가 그냥 실행되는 서버와 같은 것을 뜻하는건지 의문이 들었다.
결과적으로는 저 Task 개수가 실제 서버 개수라고 보면 된다. 하나의 Task는 고유한 IP를 가지며 리소스가 격리되어 있기 때문에, 인프라 관점에서는 하나의 서버 인스턴스로 간주해도 무방하기 때문이다. 즉, 해당 부하 테스트에서 Auto Scaling으로 Task를 1개에서 5개로 늘렸다는 것은, 트래픽 처리를 위한 서버 인스턴스를 5배로 증설했다는 의미이다.
ECS Fargate 환경에서 Task는 애플리케이션이 실행되는 독립적인 컴퓨팅 단위이다. 즉, ECS Fargate 환경에서는 1 Task = 1 서버(인스턴스)라고 간주된다.
이 개념에 대해서는 아래와 같이 정리할 수 있다.
- Cluster (아파트 단지): 서비스들이 모여 사는 논리적인 그룹.
- Service (관리 사무소): "항상 서버 5대를 유지해라", "이미지는 v1을 써라"라고 관리하는 주체
- Task (집/서버):
- 실제 IP 주소를 가지고, CPU/RAM 자원을 할당받아 돌아가는 실행 단위
- EC2(가상 서버)와 거의 동급이다. Fargate에서는 우리가 EC2를 직접 관리하지 않을 뿐, AWS가 내부적으로 어딘가에 격리된 공간(서버)을 띄워주는 것이다.
- 우리가 "서버 5대 떴다"라고 할 때 그게 바로 Task 5개이다
- Container (방):
- Task 안에서 실행되는 실제 프로세스(애플리케이션)이다.
- 보통 1 Task = 1 Container (스프링 부트 앱) 구성
- 하지만 사이드카 패턴이라고 해서, 1개의 Task 안에 [스프링 컨테이너 + 로그 수집 컨테이너] 처럼 2개 이상이 들어갈 수도 있음. (한 집에 방이 여러 개일 수 있듯이)
결론
- 대기열의 역할: Redis 기반의 대기열 시스템이 1차적으로 트래픽을 흡수하여 DB 장애를 방지함
- Auto Scaling의 효과: 예상치 못한 트래픽 폭주 시에도 3분 내에 서버가 자동으로 확장되어 서비스 중단을 막고 응답 속도를 2초 이내로 유지하였다
- 비용 효율성: 평소에는 최소한의 리소스(1개)만 유지하다가, 필요할 때만 확장하는 Auto Scailing 기능의 장점을 확인
느낀점
처음으로 AWS ECS Fargate 기반의 MSA를 구축하며 시스템에 Auto Scaling를 적용하고, 중규모(1000명) 동시 접속 트래픽을 테스트 해볼 수 있는 경험이었다. 특히 배포 초기에 뭣모르고 그 비싼 MSK Serverless를 먼저 생성하여 틀어두는 우를 범하게 되었는데(7개의 ECS Service가 정상적으로 다 올라가서 동작한 것을 확인한 후에 MSK를 붙였어야 했다..), 적정한 리소스를 현재 단계에 맞게끔 설정하는 것의 중요성을 카드값을 통해 깊이 깨달았다.. 물론 MSK는 중간에 꺼두고, 남는 임시 EC2 인스턴스에 Kafka를 설치해서 설정해서 비용 나가는 것을 막아두긴 했다.
MSA 서비스의 배포를 진행하면서 또 하나 번거로웠던 점으로는 다음과 같다. 로컬(Eureka)과 실서버(AWS Cloud Map/ALB) 환경 차이로 인해 총 7개 서비스에 대한 설정값들이 꽤나 달라졌다는 점이다. OpenFeign을 쓰는 부분들도 마찬가지다. 각 서비스의 DNS 네임스페이스를 설정해둔 URL을 별도의 환경변수로 지정해주었어야 했다. 뿐만 아니라, 누군가가 작업 브랜치에서 develop 브랜치를 최신화하지 않고 머지하는 바람에 깃이 꼬여서 로컬에서부터도 실행이 안되는 서비스들도 있었다. 그런 것들을 고치는 동안 ECS 서비스는 계속 가동되고 있었고(고치며 테스트 하는 동안..), 시간의 흐름에 따라 비용도 증가하게 되었다.
그러다가 sh 파일로 모든 ECS 서비스를 꺼둘 수 있다는 것을.. 맨 나중에 알게 되었는데..^^ 진작부터 이걸 찾아봤어야 했는데 후회가 막심하다. 이렇게 또 하나 새롭게 배울 수 있었다.
이번 프로젝트를 통하여 느낀 점으로는 기술을 도입하는 것 그 자체보다, 비용 효율성과 운영 환경을 고려하여 아키텍처를 설계하는 안목을 기를 수 있었던 것 같다.
'Project > RUSH DEAL' 카테고리의 다른 글
| [RUSH_DEAL] 6. 대기열 순번 조회 로직 (feat.UUID 함정) (0) | 2025.12.04 |
|---|---|
| [RUSH_DEAL] 5. 대기열 진입 로직 (0) | 2025.12.04 |
| [RUSH_DEAL] 4. 대기열 아키텍처&도메인 구성 (0) | 2025.12.04 |
| [RUSH_DEAL] 3. 대기열 Redis 데이터 구조 설계 (0) | 2025.12.03 |
| [RUSH_DEAL] 2. DDD에서의 VO(Value Object) (0) | 2025.12.02 |