타임딜 백엔드 — 면접 질문 & 답변 (팀원용)
이 문서 읽는 법
면접관이 날카롭게 물어볼 법한 질문들을 미리 뽑아서 답변까지 정리해놨어요. 기술적으로 어려운 부분은 비유를 들어서 설명했으니, 팀원들이랑 같이 읽고 대화하면서 이해해보세요!
📑 목차
1. 우리 서비스가 뭐 하는 건지
한 줄 요약
“수백 명이 동시에 달려들어도 재고를 정확하게 팔고, 돈을 받고, 결과를 알려주는 타임딜 시스템”
주문 한 건이 처리되는 과정
사용자가 "구매하기" 클릭
│
▼
1단계: Redis에서 재고 1개 선점 (~1ms, 매우 빠름)
├── 재고 있음 → 다음 단계
└── 재고 없음 → 즉시 "품절" 응답 (DB도 안 건드림)
│
▼
2단계: DB에 "주문 접수됨(PENDING)" 저장
│
▼
3단계: 결제 준비 - "결제 중(PAYING)" 으로 변경
│
▼
4단계: 외부 결제 서버(Mock PG, Cloud Run)에 결제 요청
300~800ms 걸림, 8%는 실패
※ 이 구간에는 DB 연결을 안 잡고 있음 (중요!)
│
├── 결제 성공 → "결제 완료(PAID)" 저장
└── 결제 실패 → Redis 재고 복구 + "결제 실패(FAILED)" 저장
│
▼
5단계: 사용자가 결과 조회 → PAID or FAILED 반환
인프라 구조 (건물로 비유)
[인터넷 사용자들]
│
▼
[ALB - 입구 안내원] ← K6 부하테스트 도구도 여기로 들어옴
│
▼
[Spring Boot EC2 - 주문 처리 직원] t3.medium 1대
│ │
│ └──► [Cloud Run - 외부 카드결제 단말기]
│ (우리가 만든 가짜 결제서버, Google 서버 사용)
│
├──► [Redis - 재고 칠판] 가장 빠름, 1차 방어
└──► [RDS PostgreSQL - 장부] 최종 기록, 2차 방어
2. 재고 처리 관련 질문
Q1. “Redis랑 DB가 따로 있는데, 둘이 재고가 안 맞으면 어떡해요?”
쉽게 말하면: 칠판(Redis)에 적힌 재고랑 장부(DB)에 적힌 재고가 다를 수 있다는 질문이에요.
답변:
두 줄 방어선을 만들어놨어요.
1차 방어: Redis에서 재고 1 빼기 (칠판에서 지우기)
→ 0이 되면 더 이상 주문 못 받음 (빠름!)
2차 방어: DB에서 "재고 > 0인 경우에만 1 빼기"
→ Redis가 이상해도 DB에서 최종 차단
만약 Redis는 성공했는데 DB 저장이 실패하면? → Redis에서 뺀 재고는 복구가 안 됩니다. 이건 솔직히 MVP에서 구현 못 한 빈 곳이에요. 발생 확률은 낮지만 프로덕션에선 보완이 필요합니다.
Q2. “수백 명이 동시에 주문하면 재고가 음수로 내려가는 거 아닌가요? (Overselling)”
쉽게 말하면: 100개 남은데 200명이 동시에 달려들면, 100명 넘게 “주문 완료” 뜨는 거 아니냐는 질문이에요.
답변:
Redis의 DECR(빼기) 연산이 원자적(Atomic)이라서 괜찮아요.
원자적이란?
“딱 한 명씩만 처리된다”는 뜻이에요. 200명이 동시에 달려들어도 Redis는 한 명씩 순서대로 처리하고, 100명 째부터는 전부 “0 이하라 안 됨”을 반환해요.
비유하자면: 공중전화 부스에 줄 세우기예요. 동시에 문을 잡아당겨도 한 명만 들어갈 수 있어요.
실제로 부하테스트에서 900명이 동시에 달려들었을 때:
- 142명: Redis에서 “품절”로 즉시 차단 (DB도 안 건드림)
- 14명: DB 2차 방어에서 추가 차단
- 132명: 실제 결제 성공
Q3. “테스트 결과에서 PAID가 143건인데 재고가 100개였잖아요. 초과판매 아닌가요?”
쉽게 말하면: 100개 팔기로 했는데 143건 결제됐으면 43개 손해 본 거 아니냐는 질문이에요.
이 질문은 사실 우리한테 유리한 질문이에요!
“아, 그게 왜 그런지 설명드릴게요” 하면서 분산 테스트 인프라 구축 얘기를 자연스럽게 꺼낼 수 있거든요. K6 EC2 3대를 AWS SSM으로 동시에 제어해서 하나처럼 움직이게 만든 것 자체가 꽤 복잡한 인프라 작업이에요.
답변:
초과판매가 아니에요! 테스트 도구(K6) 설정 문제였어요.
우리 분산 테스트 구조:
K6-0 (EC2 t3.micro) ─┐
K6-1 (EC2 t3.micro) ─┼─ AWS SSM으로 동시 명령 전송 → 3대가 동시에 부하 생성
K6-2 (EC2 t3.micro) ─┘
이 3대를 SSM으로 한 번에 제어하는 것 자체도 꽤 복잡한 작업이에요.
(SSH 없이, IAM + SSM Agent로만 운영)
문제가 생긴 지점:
K6-0이 "재고 100개로 초기화" 실행 → 주문 받기 시작
그 사이에 K6-1도 "재고 100개로 초기화" 실행 → 칠판 덮어씀
그 사이에 K6-2도 "재고 100개로 초기화" 실행 → 또 덮어씀
결과: 재고 슬롯이 사실상 300개처럼 동작
DB는 정상이었어요. 직접 SQL로 검증했어요:
마지막 재고 초기화(100) - 3차 테스트 순수 판매(36) = DB 남은 재고 64 ✅
(DB의 "재고>0 조건" 이중 방어가 정상 동작했다는 증거)
해결 방법: K6-0만 초기화 담당(마스터), K6-1/K6-2는 부하만 생성(워커)으로 역할 분리
# K6-1, K6-2 실행 시 초기화 건너뜀
k6 run --no-setup --no-teardown timedeal-test.js극한 부하 테스트(1,200 VU)에서 이 방식으로 실제 적용했어요.
Q4. “SELECT FOR UPDATE가 뭔가요? 왜 쓰나요?”
쉽게 말하면: “이 주문, 내가 지금 처리할게요~ 다른 사람 손 대지 마세요!” 라고 잠금 거는 것이에요.
이게 왜 중요하냐면:
결제 버튼을 빠르게 두 번 클릭하거나, 네트워크 오류로 같은 요청이 두 번 날아오면 이중 결제가 날 수 있어요.
SELECT FOR UPDATE 없을 때 문제:
요청A: "주문 123번 결제할게요" → DB 읽기: PENDING 확인 → 결제 진행 중...
요청B: "주문 123번 결제할게요" → DB 읽기: 아직 PENDING 확인 → 결제 진행 중...
(A가 PAYING으로 바꾸기 전에 B도 읽어버림!)
→ 결과: 같은 주문이 두 번 결제됨 💸
SELECT FOR UPDATE 있을 때:
요청A: "주문 123번 결제할게요" → DB 잠금 획득 → PAYING으로 변경 → 결제 진행
요청B: "주문 123번 결제할게요" → DB 잠금 대기...
A가 끝나면 → "이미 PAYING 상태" 확인 → 거절
→ 결과: 딱 한 번만 결제됨 ✅
트랜잭션 경계로 보면:
TX2 시작
└─ SELECT * FROM orders WHERE id=123 FOR UPDATE ← 여기서 잠금
└─ 상태 확인: PENDING이 아니면 예외 발생
└─ UPDATE orders SET status='PAYING'
TX2 종료 (커밋과 동시에 잠금 해제)
→ TX2 동안은 아무도 이 주문 행을 건드릴 수 없음
데드락(서로 잠금 걸어서 아무것도 못 하는 상태) 걱정은 없어요. 주문 1건씩 잠그는 구조라 A→B, B→A 교차 잠금이 생길 일 자체가 없거든요.
3. 성능 관련 질문
Q5. “EC2 t3.medium 한 대로 900명 동시 처리가 가능한 건가요?”
쉽게 말하면: 작은 서버 한 대로 버틸 수 있냐는 질문이에요.
답변:
900 VU(동시 사용자) 기준으로 97% 성공률 달성했어요. 버텼어요!
97%가 뭘 의미하는 건가요?
K6가 검사하는 모든 체크 항목(주문 접수됐는지, 결제 상태 바뀌었는지, 조회 되는지 등) 중 97.03%가 기대한 결과였다는 뜻이에요.
남은 ~3%는 PG(결제 서버) 자체의 실패율 8% 때문이에요. Mock PG는 실제 카드사처럼 일부러 결제를 실패시키도록 설계했고(8% 실패, 1% 타임아웃), 그 실패 케이스에 대한 체크 항목이 일부 포함된 거예요.
즉, 우리 시스템(인프라, API) 오류로 인한 실패는 거의 없었어요. 재고 처리 정확성은 100% 달성했고요.
하지만 한계도 명확히 있어요:
| 구성 | 결과 |
|---|---|
| 900 VU, Pool=20 | ✅ 97% 통과 |
| 1,200 VU, Pool=20 | ❌ 서버 포화 |
| 1,200 VU, Pool=50 (차가운 상태) | ❌ 부분 실패 |
“Pool”이 뭔지 모를 분들을 위해 👇
Q6. “HikariCP 커넥션 풀이 뭔가요? 왜 문제가 되나요?”
쉽게 말하면: DB(장부)에 접근할 수 있는 창구가 20개밖에 없다는 이야기예요.
답변:
비유: 은행 창구가 20개뿐인데 180명이 줄을 선 상황
손님(요청) : 180명
창구(DB연결) : 20개
→ 160명은 창구가 빌 때까지 대기
→ 5초 넘게 기다리면 → 오류 반환
우리 서버에서 실측한 내용:
1,200 VU 테스트 결과:
"Connection is not available, request timed out after 5000ms"
(total=20, active=20, idle=0, waiting=379)
→ 창구 20개 다 차고, 379명이 기다리다 포기
이게 “Breaking Point(한계점)” 이에요.
Q7. “Pool을 50으로 늘리면 해결 아닌가요?”
쉽게 말하면: 창구를 20개에서 50개로 늘리면 되는 거 아니냐는 질문이에요.
답변:
늘려봤는데, “Cold Pool(차가운 풀)” 문제가 있었어요.
Cold Pool 비유
영업 시작하자마자 손님이 500명 몰려왔는데, 창구 직원이 아직 출근 중이에요 (20명 출근 → 30명 → 50명 서서히 늘어나는 중) 손님이 몰려오는 속도가 직원 출근 속도보다 빠르면? → 또 병목!
Flash Sale은 정확히 이런 상황이에요. 0명에서 갑자기 1,200명으로 터지는 거라서요.
해결책: 미리 창구 직원을 다 출근시켜 놓기
hikari:
maximum-pool-size: 50
minimum-idle: 50 # ← 이게 핵심! 항상 50개 유지 (줄이지 말 것)이렇게 하면 Pool=50 warm 기준 이론상 약 2,500건/초 처리 가능해요.
Q8. “왜 결제 요청하는 동안 DB 연결을 안 잡나요? 그게 가능한가요?”
쉽게 말하면: 결제 기다리는 시간(300~800ms)에 DB 연결을 놓아주는 게 맞냐는 질문이에요.
답변:
이게 사실 이 서비스에서 가장 중요한 설계 포인트예요!
비유: 카페에서 주문 받고 커피 나올 때까지 계산대 막아두면 안 되잖아요. 주문 받고 → 진동벨 주고 → 다음 손님 받는 거처럼요.
나쁜 방법 (안 한 것):
주문 받기(DB 연결 시작) → 결제 기다리기 800ms → 결제 완료(DB 연결 종료)
→ 800ms 동안 DB 연결 1개 낭비
좋은 방법 (우리가 한 것):
주문 받기(TX1 종료, DB 반환) → 결제 기다리기 800ms → 결제 완료(TX3 시작, DB 다시 받기)
→ 800ms 동안 DB 연결을 다른 요청이 쓸 수 있음
덕분에 창구 20개로도 훨씬 더 많은 손님을 처리할 수 있어요.
4. 장애 관련 질문
Q9. “PAYING 상태가 88건이나 고착됐다는 게 무슨 뜻이에요?”
쉽게 말하면: “결제 중” 상태로 멈춰서 영원히 완료도 실패도 안 되는 주문이 88건 있다는 거예요.
답변:
테스트 도중에 K6 프로그램이 강제 종료되면서 발생했어요.
정상 흐름:
결제 중(PAYING) → 결제 완료 응답 도착 → PAID 또는 FAILED로 변경
문제 발생 흐름:
결제 중(PAYING) → K6가 응답 기다리다 프로그램 강제 종료
→ Spring Boot가 응답 받을 곳이 없어짐
→ PAYING 상태로 영구 고착
→ Redis 재고도 복구 안 됨!
실제 DB 조회 결과:
| 시간 | 결제 완료 | 결제 실패 | 결제 중(고착) | 접수만 됨 |
|---|---|---|---|---|
| 1차 테스트 | 99건 | 7건 | 70건 | 35건 |
| 2차 테스트 | 100건 | 13건 | 18건 | 58건 |
| 3차 테스트 | 143건 | 14건 | 0건 | 1건 |
| 누적 | 343건 | 279건 | 88건 고착 | 94건 |
해결 방법은 사실 간단해요 — 타임아웃 처리:
// "10분 이상 결제 중인 주문은 자동으로 실패 처리" 스케줄러
@Scheduled(fixedDelay = 60_000) // 1분마다 실행
public void cleanupStuckPayingOrders() {
// 10분 이상 PAYING 상태인 주문 찾기
// → Redis 재고 복구 (INCR)
// → 주문 상태를 FAILED로 변경
}코드 몇 줄이면 돼요. 지금 MVP에 추가할 수 있는 가장 임팩트 있는 개선이에요!
팀원 분들께
이 스케줄러 하나 추가하면 “고착 주문 처리”라는 실제 결함이 해결돼요. 면접에서도 “발견했고 해결책도 알고 있어요”가 “못 했어요”보다 훨씬 강한 답변이 되고요.
Q10. “Cloud Run(결제 서버)이 다운되면요?”
쉽게 말하면: 외부 결제 서비스가 먹통이 되면 어떻게 되냐는 질문이에요.
답변:
결제 시도 → 오류 → FAILED + Redis 재고 복구로 처리돼요.
근데 재시도(Retry) 로직이 없어요. 카드사 서버가 잠깐 버벅여도 바로 실패 처리됩니다.
프로덕션에서는 “CircuitBreaker + Retry” 패턴을 쓰면 되는데, MVP에서는 미구현이에요.
Q11. “Redis가 다운되면요?”
쉽게 말하면: 1차 방어선이 날아가면 어떻게 되냐는 질문이에요.
답변:
솔직히 말하면 주문 자체가 안 돼요. Redis DECR가 실패하면 예외가 발생하거든요.
폴백(대안) 설계를 한다면 Redis 오류 감지 시 DB 직접 잠금으로 처리할 수 있는데, DB 부하가 훨씬 커지기 때문에 동시 허용 수를 제한해야 해요.
MVP에서는 Redis 고가용성(AWS ElastiCache)을 인프라 레벨에서 보장하고, 애플리케이션 레벨 폴백은 구현하지 않았어요.
5. 설계 선택 관련 질문
Q12. “왜 서버가 1대예요? 1대 죽으면 서비스 전체 다운 아닌가요?”
쉽게 말하면: “서버 2대 이상 써서 하나 죽어도 괜찮게 하면 되는 거 아닌가요?”라는 질문이에요.
답변:
의도적인 선택이에요. 단순 시간/비용 절약이 아니라 근거가 있어요.
근거 1 — 무중단 배포가 생각보다 복잡해요:
EC2 2대로 무중단 배포 하려면...
ALB 타겟 그룹 전환 설계 + 배포 스크립트 수정 + 세션 공유 처리 + ...
→ 핵심 기능(재고 처리 정확성) 검증 기간에 이걸 다 하기엔 범위가 너무 넓었어요
근거 2 — 부하테스트 결과로 확인한 것:
서버 2대 늘리기 전에, 지금 설정의 병목이 서버 수가 아니라 DB 커넥션 풀 설정이라는 걸 테스트로 알아냈어요.
서버 2대로 늘려도 → HikariCP Cold Pool 문제 그대로 있음
→ 그냥 병목이 2배 생기는 것
진짜 해결책: minimum-idle = maximum-pool-size 설정
나중에 EKS로 전환하면 그때 수평 확장 + HPA(자동 스케일링) 적용하면 가장 쉽게 해결되겠죠?
Q13. “userId 검증이 왜 이 서비스에 없어요? 보안 문제 아닌가요?”
쉽게 말하면: “남의 아이디로 주문할 수 있는 거 아니에요?”라는 질문이에요.
답변:
userId 검증은 GitHub 코드와 아키텍처에 구현되어 있어요. 다만 내가 부하테스트를 돌린 시점과 팀원들의 개발 완료 시점에 약간 차이가 있었어요.
MSA(마이크로서비스) 구조에서는 역할 분리가 원칙이에요:
- 유저 서비스 → 로그인, 토큰 발급, userId 검증 담당
- 타임딜 서비스 → 유저 서비스가 검증한 토큰에서 추출한 userId를 신뢰
비유: 식당에서 웨이터가 손님 신분증을 보는 게 아니라, 입구 경비원(유저 서비스)이 확인하고 들어온 손님이면 신뢰하는 것과 같아요.
면접에서 이 질문 나오면
“GitHub 코드에 구현되어 있고, 아키텍처 설계에도 포함되어 있어요. 부하테스트 시점에 팀 개발 속도 차이로 테스트 환경에는 반영이 안 됐지만, 유저 서비스에서 토큰 검증 후 userId를 전달하는 구조예요” 라고 답하면 돼요.
Q14. “왜 Redis랑 DB 두 곳에 재고를 관리해요? 하나로 통일하면 안 돼요?”
쉽게 말하면: 두 군데 관리하면 복잡하고 불일치 날 수 있는데 왜 그렇게 했냐는 질문이에요.
답변:
각자 역할이 달라서 같이 쓰는 거예요.
| Redis (칠판) | DB (장부) | |
|---|---|---|
| 속도 | 1ms (초고속) | 20~30ms |
| 역할 | 순간 차단 (품절 처리) | 최종 기록 |
| 정확도 | 근사값 (약간 느슨해도 됨) | 정확해야 함 |
Flash Sale은 1초에 수백 명이 동시에 달려들기 때문에 DB만 쓰면 DB가 버티질 못해요. Redis로 대부분을 걸러내고, DB에는 실제 주문만 보내는 구조예요.
비유: 대형 콘서트 입장할 때 외부에 직원이 이미 마감됐으면 줄을 세우지 않고 자르고, 내부에서 좌석 확인하는 것과 같아요.
6. 트레이드오프 & 약점 정리
면접에서 "완벽합니다"라고 말하지 마세요!
단, 모든 약점이 다 같은 무게는 아니에요. 트레이드오프로 설명할 수 있는 것과, 공격받으면 진짜 아픈 것을 구분해서 대응하세요.
유연하게 설명 가능한 트레이드오프 (자신 있게 말해도 돼요)
| 항목 | 왜 이렇게 했나요? | 개선 방향 |
|---|---|---|
| EC2 1대 (HA 없음) | 병목이 서버 수가 아니라 Pool 설정임을 부하테스트로 확인. 무중단 배포 복잡성 대비 효과 미미 | EKS 전환 시 HPA + Rolling Update |
| Redis + DB 이중 저장소 | 속도(Redis) vs 정확성(DB) 역할 분리가 명확한 의도 | 현 설계 유지가 적합 |
| HikariCP Pool=20 | MVP 검증 범위에서 Breaking Point를 실측으로 찾아냄 | minimum-idle = maximum-pool-size = 50으로 해결 가능, 근거 있음 |
| K6 setup() 경합 발생 | 발견했고, 마스터/워커 분리로 극한 테스트에서 이미 해결 | 해결 완료 ✅ |
| userId 검증이 이 서비스에 없음 | MSA 구조에서 유저 서비스 담당. GitHub 코드와 아키텍처에는 구현됨 | 테스트 환경 반영 필요 |
공격받으면 뼈아픈 진짜 약점들 (솔직하게 인정 + 개선책 세트로 답변)
| 약점 | 얼마나 아픈가요? | 개선 방법 |
|---|---|---|
| PAYING 고착 (88건 실측) | 🔴 실제 데이터로 증명된 결함. 사용자가 돈 물릴 수 있음 | @Scheduled 타임아웃 스케줄러 추가 (코드 몇 줄) |
| Redis DECR 후 DB 실패 시 재고 복구 없음 | 🟠 발생 빈도 낮지만 재고 1개씩 누수 가능. 로직 공백 | DB 실패 catch에서 Redis INCR 보상 추가 |
| 결제 서버 재시도 없음 | 🟠 PG 서버 순간 버벅임에도 바로 결제 실패 처리됨 | Resilience4j Retry 1~2회 추가 |
| Redis 다운 시 주문 불가 | 🟠 1차 방어선 전체가 날아가는 상황 | ElastiCache Multi-AZ + 앱 레벨 폴백 설계 |
7. 부하테스트 결과 숫자 정리
기본 테스트 (900 VU, Pool=20)
VU가 뭔가요?
Virtual User, 가상 사용자 수예요. 900 VU = 900명이 동시에 클릭하는 상황
| 측정 항목 | 결과 | 좋은 건가요? |
|---|---|---|
| 전체 테스트 통과율 | 97.03% | ✅ 좋음 |
| 주문 시도 건수 | 156건 | — |
| Redis에서 품절 차단 | 142건 | ✅ 1차 방어 정상 |
| 결제 성공 (PAID) | 132건 | ✅ |
| PG 실패 (8% 실패율) | 12건 | ✅ 의도된 수치 |
| 주문 API 평균 응답 | 877ms | 보통 |
| 주문 API 상위 5% 응답 | 3,075ms | ⚠️ 경계선 |
| 결제 API 평균 응답 | 2,946ms | PG 포함이라 느림이 정상 |
| 결제 API 상위 5% 응답 | 5,676ms | ✅ 목표(8,000ms) 이내 |
한계 테스트 (1,200 VU)
| 구성 | 결과 | 왜요? |
|---|---|---|
| Pool=20 | ❌ 서버 터짐 | DB 창구 20개로 379명 대기 → 5초 뒤 오류 |
| Pool=50 (처음 시작) | ❌ 부분 실패 | 창구 늘리는 중(30개)에 손님이 몰려옴 |
| Pool=50 (미리 예열) | ✅ 예상 통과 | minimum-idle=50으로 처음부터 50개 유지 |
핵심 한계선 (Breaking Point)
Pool=20 기준:
동시 DB 요청 800건 = 한계선
800건 초과 → 커넥션 대기 → timeout → 서버 에러 폭발
테스트하면서 고친 버그들
| 버그 | 증상 | 원인 | 수정 방법 |
|---|---|---|---|
| Redis 연결 실패 | 앱 시작하자마자 오류, 235번 재시작 | Spring Boot 3.x에서 설정 경로 변경됨 (spring.redis. → spring.data.redis.) | 설정 경로 수정 |
| GET 주문조회 500 에러 | 주문 조회가 전부 실패 | LocalDateTime 타입을 JSON으로 못 변환함 | Entity 직접 반환 → DTO(OrderResponse) 반환으로 변경 |
| 재고 초기화 불일치 | Redis만 리셋, DB는 안 됨 | 관리자 API가 Redis만 초기화했음 | Redis + DB 동시 초기화로 수정 |
| K6 결제 실패 체크 오류 | PG 실패 케이스 체크가 전부 실패 | 에러 응답 형식을 잘못 가정함 | 502 응답 후 GET으로 DB 상태 직접 확인으로 변경 |
| Mock PG → Cloud Run | Terraform SG 변경 시 10분 이상 멈춤 | EC2 인라인 SG 변경 시 destroy+create 발생 | Cloud Run으로 이관 (월 ~$15 절약 보너스) |
작성일: 2026-03-03 기준 아키텍처: timedeal-architecture-mvp.drawio 부하테스트 날짜: 2026-02-27 AWS ap-northeast-2 실측