시니어 백엔드 면접 질문 2편 - 운영/안정성 (5~10년차)
시니어 개발자에게 운영 경험은 필수입니다. 장애가 났을 때 어떻게 대응하는지, 시스템을 얼마나 안정적으로 운영할 수 있는지가 핵심입니다.
시리즈 구성:
- 1편: 인프라/스케일링 - 트래픽, DB, 분산시스템
- 2편 (현재): 운영/안정성 - 장애대응, 이벤트 아키텍처, 배포
- 3편: 설계/리더십 - 의사결정, 시스템디자인, 코드리뷰
1. 장애 대응과 복원력
Q1. 외부 API 호출하는 서비스가 느려지면 전체 시스템에 어떤 영향이 있나요?
기대 답변: 스레드 풀이 고갈되어 다른 요청도 처리 못하게 됩니다. 연쇄적으로 전체 시스템이 멈출 수 있습니다 (Cascading Failure).
💡 실제 사례 보기
시나리오: 제휴 결제사 응답이 8초로 늘어나면서 우리 결제 API의 톰캣 스레드 풀(200개)이 전부 점유. 무관한 상품 조회 API까지 5xx 발생.
진단:
- APM의 thread dump → 197개 스레드가 외부 결제사 응답 대기
- 결제사 평소 p99 250ms → 갑자기 8초
- 우리 timeout이 30초로 너무 길게 잡혀 있어 풀이 회복 못 함
대응 (긴급 → 영구):
- 긴급 (10분): 결제사 timeout을 30s → 3s로 축소 + 배포
- 단기 (당일): Resilience4j 서킷 브레이커 도입
- 임계치: 에러율 30%, 슬라이딩 윈도우 100건
- Open 시 즉시 “결제사 점검 중” Fallback 응답
- 영구 (1주): 외부 호출별 Bulkhead 도입
- 결제사 호출은 별도 스레드 풀 50개로 격리
- 우리 핵심 API 풀(150개)은 외부 장애와 무관하게 동작
결과:
- 같은 결제사 장애가 한 달 뒤 또 발생했으나 우리 시스템은 정상 응답
- “결제만 잠시 불가” 안내로 사용자 이탈 최소화
교훈:
- 외부 의존성은 격리(Bulkhead) + 빠른 실패(짧은 timeout) + 자동 회피(서킷 브레이커) 3단 방어
- timeout은 외부 p99 × 2~3 정도가 안전 (8초짜리 외부에 30초 timeout은 자살)
🔄 꼬리질문 1: 서킷 브레이커(Circuit Breaker)가 뭔가요?
기대 답변: 외부 호출 실패가 임계치를 넘으면 회로를 열어 빠르게 실패 처리합니다.
- Closed: 정상 호출
- Open: 호출 차단, 즉시 fallback 반환
- Half-Open: 일정 시간 후 테스트 호출
Resilience4j, Hystrix(deprecated) 등으로 구현합니다.
📋 실제 사례
상황: 상품 상세 페이지가 추천 시스템을 동기 호출. 추천 서버가 ML 모델을 갱신하는 매일 새벽 시간대마다 응답이 80ms → 5초로 튀고, 그동안 상품 페이지 전체가 같이 느려져 이탈률이 3%p 상승.
조치 — Resilience4j 서킷 브레이커 도입:
- 임계: 에러율 50% 또는 슬로우 콜 비율 50%(p99 > 1초), 윈도우 100건
- Open 30초 유지 → Half-Open 5건 테스트 → 3건 이상 성공 시 Closed
- Open 동안 즉시 이전 추천 캐시 fallback
결과:
- ML 모델 갱신 시간대에도 상품 페이지 p99 100ms 유지
- 이탈률 영향 0.3%로 축소
- 추천 서버 부하도 감소 (Open 동안 호출 차단)
교훈: 서킷 브레이커는 에러율만이 아니라 슬로우 콜 비율도 트리거 조건에 넣어야 한다. “에러는 안 나는데 느린” 의존성이 사실 더 위험하다.
🔄 꼬리질문 2: Fallback은 어떻게 설계하시겠어요?
기대 답변:
- 캐시된 이전 데이터 반환
- 기본값 반환
- 기능 제한 (추천 기능 비활성화 등)
- 에러 메시지와 함께 graceful degradation
📋 실제 사례
상황: 추천 서버 장애 시 단순히 빈 배열을 반환했더니 상품 상세 페이지의 “추천 상품” 영역이 통째로 비어 보기 흉하고 전환율도 떨어짐.
조치 — 계층적 Fallback 설계:
- 1순위: Redis에 캐시된 해당 사용자의 이전 추천 결과 (TTL 1시간)
- 2순위: 캐시도 없으면 해당 카테고리 인기 상품 (사용자 컨텍스트 무관, 매일 배치로 갱신)
- 화면에는 fallback 표시 없이 정상 추천처럼 노출
결과:
- 추천 서버 완전 장애 시에도 추천 영역이 항상 채워짐
- 전환율 하락 거의 없음 (개인화 품질만 살짝 저하)
교훈: Fallback은 “기능을 끄는 것”이 아니라 “그럴듯한 대체를 채우는 것” 이다. 빈 응답보다 stale 개인화 → 비개인화 인기 순의 계층적 degradation이 사용자 체감을 지킨다.
🔄 꼬리질문 3: Timeout은 어떻게 설정하시나요?
기대 답변:
- 외부 API의 P99 응답시간 기준으로 설정 (보통 2~3초)
- Connection Timeout과 Read Timeout 분리
- 너무 길면 스레드 점유, 너무 짧으면 정상 응답도 실패 처리
- 실제 트래픽 기반으로 지속 튜닝
📋 실제 사례
상황: 결제사 연동 timeout이 “혹시 모르니” 30초로 잡혀 있었음. 결제사가 8초 지연되자 스레드들이 30초까지 붙잡혀 풀이 고갈, 전체 결제 API 마비.
진단:
- 결제사 평소 p99 = 250ms인데 timeout이 30초 → 비정상 응답을 120배까지 기다림
- Connection timeout과 Read timeout이 분리되어 있지 않아 연결 자체 실패도 30초 대기
조치:
- Read timeout = p99 × 3 + 마진 = 1초, Connection timeout = 500ms 로 분리 설정
- 부하 테스트(k6)로 정상 응답이 timeout에 안 걸리는지 검증 (실패율 0% 확인)
결과:
- 같은 결제사 지연 재발 시 1초 만에 실패 → 서킷 브레이커가 즉시 Open
- 스레드 고갈·전체 마비 소멸
교훈: timeout은 외부 p99 × 2~3이 마지노선이다. “모르겠으면 길게”는 장애를 증폭시킨다. Connection/Read를 반드시 분리하고, 부하 테스트로 정상 응답이 안 걸리는지 검증한다.
2. 이벤트 기반 아키텍처
Q2. 동기 API 대신 이벤트 기반 아키텍처를 선택하는 기준은?
기대 답변:
- 즉각적인 응답이 필요 없는 경우
- 서비스 간 결합도를 낮추고 싶을 때
- 데이터 일관성보다 가용성이 중요할 때 (Eventual Consistency 허용)
- 처리량이 급증해도 버퍼링이 필요할 때
💡 실제 사례 보기
시나리오: 회원가입 API가 1.2초 — 가입 직후 이탈률 18%. 내부적으로 7개 후속 처리를 동기 호출 중.
병목 분석:
| 호출 | 평균 시간 | 본질적 필요성 |
|---|---|---|
| 회원 INSERT | 30ms | 필수 동기 |
| 환영 이메일 발송 | 350ms | 비동기 가능 |
| SMS 인증코드 | 280ms | 필수 동기 (즉시 노출) |
| 추천 시스템 등록 | 150ms | 비동기 가능 |
| 마케팅 큐 등록 | 200ms | 비동기 가능 |
| Slack 알림 | 100ms | 비동기 가능 |
| 통계 적재 | 90ms | 비동기 가능 |
리아키텍처:
- 회원 INSERT + SMS만 동기로 유지 (310ms)
- 나머지 5개는 Kafka 토픽
user.signed_up발행 - 각 컨슈머가 비동기 처리, 멱등성 보장 (
event_id유니크 제약)
결과:
- 가입 응답: 1.2s → 320ms (73% 감소)
- 가입 직후 이탈률: 18% → 9%
- 이메일/추천이 일시 장애여도 가입 자체는 정상 동작
교훈:
- 동기로 묶여있는 것 같지만 사실 비동기여도 되는 호출이 대부분
- “사용자가 다음 화면에서 즉시 필요한가?”가 판단 기준
- 비동기 전환 시 멱등성 + DLQ + 모니터링 3종 세트가 필수
🔄 꼬리질문 1: Kafka를 쓴다면 Consumer 장애 시 메시지 유실은 어떻게 방지하나요?
기대 답변:
- Auto Commit 비활성화: 처리 완료 후 수동 커밋
- At-least-once: 중복 허용, 멱등 처리로 보완
- Dead Letter Queue: 재처리 불가 메시지 별도 보관
- Consumer Group 리밸런싱 전략 설정
📋 실제 사례
상황: 알림 컨슈머가 트래픽 폭주로 OOM kill 당했는데, 재시작 후 그 시간대 알림 일부가 영영 안 감. 사용자가 “주문했는데 알림을 못 받았다” 클레임.
진단:
- 컨슈머가
enable.auto.commit=true(기본값) - Kafka는 처리 여부와 무관하게 주기적으로 오프셋을 commit
- OOM 직전 poll한 메시지들이 처리 전에 이미 commit되어 → 재시작 시 그 메시지를 다시 안 읽음 → 유실
조치:
enable.auto.commit=false로 변경- 메시지 처리 완료 후 수동 commit (
acknowledgment.acknowledge()) - at-least-once 보장 + 컨슈머 멱등 처리(이벤트 ID 유니크)로 중복 무해화
결과:
- OOM·재밸런싱 상황에서도 알림 유실 0건 (6개월)
- 가끔 중복 처리는 발생하나 멱등 처리로 사용자에겐 영향 없음
교훈: Kafka auto.commit은 “처리”가 아니라 “수신”을 기준으로 커밋하는 유실 함정이다. 시니어 코드에서 이걸 발견하면 즉시 수동 commit + 컨슈머 멱등성으로 교체해야 한다.
🔄 꼬리질문 2: 메시지 순서 보장은 어떻게 하나요?
기대 답변: Kafka에서 같은 파티션 내에서만 순서 보장됩니다.
- 순서가 중요한 데이터는 같은 키(예: 사용자 ID)로 같은 파티션에 보냄
- 파티션 수 늘리면 순서 보장 범위도 고려해야 함
📋 실제 사례
상황: 주문 상태 변화 이벤트(CREATED → PAID → SHIPPED → DELIVERED)를 Kafka로 발행했는데, 컨슈머가 가끔 PAID보다 SHIPPED를 먼저 처리해서 “결제 전에 배송됨” 같은 불가능한 상태가 DB에 기록됨.
진단:
- 처음엔 라운드로빈으로 메시지를 여러 파티션에 분산
- 같은 주문의 이벤트들이 서로 다른 파티션에 흩어져 → 컨슈머가 병렬 처리하며 순서 뒤바뀜
- Kafka는 파티션 내부에서만 순서를 보장
조치:
- 발행 시 파티션 키 =
order_id지정 → 같은 주문의 모든 이벤트가 같은 파티션 - 같은 파티션은 단일 컨슈머 스레드가 순차 처리 → 순서 보장
- 컨슈머 수는 늘려도 OK (다른 주문은 다른 파티션에서 병렬)
결과:
- 상태 역전 사고 0건
- 컨슈머 병렬성은 유지 (주문별 독립)
교훈: Kafka 순서 보장은 글로벌이 아니라 파티션 단위다. 순서가 중요한 데이터는 순서를 지켜야 하는 단위(aggregate id)를 파티션 키로 잡아야 한다. 글로벌 순서를 요구하면 파티션 1개로 묶여 처리량을 잃는다.
🔄 꼬리질문 3: 이벤트 소싱(Event Sourcing)과 CQRS를 설명해주세요.
기대 답변:
- Event Sourcing: 상태 대신 이벤트(변경 이력)를 저장. 현재 상태는 이벤트 리플레이로 복원.
- CQRS: Command(쓰기)와 Query(읽기) 모델 분리. 각각 최적화 가능.
- 복잡도가 높아 모든 상황에 적합하지 않음. 감사 로그 필수, 복잡한 도메인에서 고려.
📋 실제 사례
상황: 핀테크 거래 내역 도메인 — 규제상 “특정 과거 시점의 잔액을 정확히 재현” 할 수 있어야 했음. 기존엔 잔액을 단일 컬럼으로 덮어쓰기만 해서 과거 잔액 추적이 불가능했음.
조치 — 거래 도메인에만 Event Sourcing 도입:
- 잔액을 직접 저장하지 않고 모든 입출금 이벤트를 append-only로 저장
- 현재 잔액 = 이벤트 리플레이 (성능 위해 일별 스냅샷 + 이후 이벤트만 재생)
- 감사·분쟁 시 임의 과거 시점 잔액을 정확히 재현 가능
결과:
- 규제 감사 요구사항 충족 (과거 시점 잔액 재현)
- 분쟁 처리 시 “왜 이 잔액인지” 전체 이력 제시 가능
- 단, 운영 부담(프로젝션·스냅샷·리플레이)이 커서 거래 도메인에만 한정 적용
교훈: Event Sourcing은 멋있어서가 아니라 감사·이력 재현이 본질적 요구사항인 도메인에만 도입한다. 운영 복잡도가 크므로 전체 시스템이 아니라 그게 꼭 필요한 도메인만 골라야 한다.
3. 배포와 운영
Q3. 무중단 배포는 어떻게 하시나요?
기대 답변:
- Rolling Update: 점진적 교체. K8s 기본 전략.
- Blue-Green: 두 환경 준비 후 트래픽 스위칭.
- Canary: 일부 트래픽만 새 버전으로. 문제 발견 시 빠른 롤백.
💡 실제 사례 보기
시나리오: 결제 모듈의 컬럼 타입 변경(VARCHAR → JSONB)을 무중단으로 진행.
Expand-Contract 패턴 적용 (총 3주):
Week 1 — Expand:
- 새 컬럼
payment_data_v2 JSONB추가 (nullable) - 신규 코드는 둘 다 read, 옛 컬럼에만 write
- 기존 트래픽 영향 0
Week 2 — Migrate:
- 백필 배치: 옛 컬럼 → 새 컬럼 JSON 변환
- 매 100건마다 sleep 50ms로 부하 제어
- 검증 SQL:
WHERE payment_data_v2 IS NULL0건 확인
Week 3 — Contract:
- 코드 배포 1: write를 새 컬럼으로 전환 (read는 여전히 fallback 유지)
- 1주일 모니터링 → 회귀 없음 확인
- 코드 배포 2: read도 새 컬럼만 사용
- 코드 배포 3: 옛 컬럼 drop
무중단을 가능하게 한 요소:
- 모든 단계가 이전 버전과 양립 (롤백 가능)
- Flyway로 마이그레이션 버전 관리, PR로 코드 리뷰
- 단계별 카나리 배포 (5% → 50% → 100%)
결과:
- 다운타임 0초, 데이터 유실 0건
- 1주차 중간에 옛 컬럼 read 의존 외부 시스템을 발견해 추가 어댑터 작업 (Expand가 길었던 덕에 발견)
교훈:
- DB 스키마 변경은 한 번의 마이그레이션이 아니라 여러 배포로 쪼개진 여정
- Expand-Contract는 안전을 위해 일부러 느리게 가는 패턴
🔄 꼬리질문 1: DB 스키마 변경이 있을 때는 어떻게 하나요?
기대 답변:
- Backward Compatible 변경 유지: 컬럼 추가는 OK, 삭제/이름변경은 위험
- Expand-Contract 패턴:
- 새 컬럼 추가 (기존 코드 영향 없음)
- 새 코드 배포 (새 컬럼 사용)
- 기존 컬럼 삭제 (완전히 전환 후)
- Flyway, Liquibase로 마이그레이션 버전 관리
📋 실제 사례
상황: user.phone 컬럼을 user.phone_number로 이름 변경하는 마이그레이션을 한 번의 배포로 시도. 마이그레이션은 먼저 적용됐는데 구 버전 인스턴스(롤링 중) 가 아직 phone을 참조 → 30분간 일부 인스턴스에서 5xx.
진단:
- 롤링 배포 중에는 신·구 버전이 동시에 떠 있음
- 컬럼을 한 번에 rename하면 구 버전과 호환 불가 → 그 시간 동안 장애
조치 — Expand-Contract로 재발 방지:
- Expand:
phone_number컬럼 추가, 트리거로phone과 양방향 동기화 - 코드 배포: 신규 코드는
phone_number사용 (구 버전은phone계속 사용 → 양립) - 1주 모니터링 후 Contract:
phone컬럼·트리거 제거
- 모든 rename·drop을 최소 3 배포로 분리하도록 팀 규칙화
결과:
- 이후 스키마 변경 중 호환성 장애 0건
- 각 단계가 롤백 가능 (안전망)
교훈: rename·drop은 롤링 배포의 신·구 공존 때문에 한 번에 하면 반드시 깨진다. Expand → Migrate → Contract 3단계로 분리하고, 각 단계가 이전 버전과 양립하는지 확인한다.
🔄 꼬리질문 2: Feature Flag는 왜 쓰나요?
기대 답변:
- 배포와 릴리스 분리: 코드는 배포하되 기능은 비활성화
- A/B 테스트, 점진적 롤아웃
- 장애 시 빠른 기능 비활성화
- LaunchDarkly, Unleash 같은 도구 활용
📋 실제 사례
상황: 새 결제 흐름을 전면 배포했다가 특정 카드사에서만 발생하는 버그로 전체 결제 롤백 + 재배포에 40분 소요. 그동안 결제 전환율 급락.
조치 — Unleash Feature Flag 도입:
- 새 결제 흐름을
new-checkout-flow플래그 뒤에 배치 - 코드는 미리 배포하되 기능은 0%로 비활성
- 점진 롤아웃: 내부 직원 → 1% → 5% → 30% → 100%
- 문제 발생 시 재배포 없이 플래그만 off (수 초 내 적용)
결과:
- 5% 단계에서 카드사 버그 발견 → 플래그 off로 1초 만에 차단, 영향 사용자 최소화
- 수정 후 다시 점진 롤아웃, 전환율 하락 없이 안착
교훈: Feature Flag는 배포(코드 전달) ≠ 릴리스(기능 노출) 를 분리한다. 장애 시 재배포(수십 분) 가 아니라 플래그 토글(수 초) 로 대응할 수 있는 게 핵심 가치.
🔄 꼬리질문 3: 롤백 기준은 어떻게 정하시나요?
기대 답변:
- Error Rate: 특정 임계치 초과 시 (예: 1% 이상)
- Latency: P99 응답시간 급증
- Business Metrics: 전환율, 결제 성공률 등 핵심 지표 이상
- 자동화된 롤백(Automated Rollback)과 알림 설정
📋 실제 사례
상황: 카나리 배포에서 기술 메트릭(에러율·latency)은 모두 정상인데, 새 버전이 배포된 뒤 결제 전환율이 조용히 떨어지고 있었음. 에러를 안 내면서 결제 버튼이 일부 기기에서 안 눌리는 UX 버그였고, 6시간 뒤에야 매출 리포트로 발견.
조치 — 비즈니스 메트릭 자동 게이트:
- 카나리 배포에 기술 메트릭 + 비즈니스 메트릭 둘 다 게이트로 설정
- 5분간 결제 성공률 < 98% 또는 에러율 > 1% 또는 p99 > 1초 중 하나라도 위반 시 자동 롤백
- 결제 성공률을 카나리 대시보드 1순위 지표로 노출
결과:
- 다음 유사 UX 버그는 5분 만에 자동 롤백 (매출 영향 최소화)
- “기술은 정상인데 비즈니스가 망가지는” 사각지대 제거
교훈: 롤백 기준에 에러율·latency만 넣으면 “에러 없이 비즈니스가 망가지는” 버그를 놓친다. 결제 성공률·전환율 같은 핵심 비즈니스 KPI를 자동 게이트에 반드시 포함한다.
4. 모니터링과 옵저버빌리티
Q4. 모니터링과 옵저버빌리티의 차이는 뭔가요?
기대 답변:
- 모니터링: 미리 정의한 지표를 수집하고 대시보드로 확인. “알고 있는 문제” 감지.
- 옵저버빌리티: 로그, 메트릭, 트레이스를 통해 “모르는 문제”도 추적 가능. 시스템 내부 상태를 외부에서 파악.
💡 실제 사례 보기
시나리오: 주문 API p99 응답이 가끔 4초로 튀는 현상. CPU·메모리·DB 다 정상. 원인 불명.
옵저버빌리티 도입 전:
- 로그는 서비스별로 흩어져 있어 한 요청의 전체 흐름 추적 불가
- “느린 요청은 있는데 어디서 느려졌는지 모름”
- 운영팀은 일주일째 추정만 반복
OpenTelemetry 도입:
- Trace ID 자동 전파: 게이트웨이에서 부여 → 모든 마이크로서비스 헤더에 propagate
- Span 자동 수집: HTTP, DB, Redis, Kafka 호출마다 span 생성
- Tail-based sampling: 4초 이상 요청 100% 수집
발견:
- 주문 API → 사용자 서비스 → 외부 휴면 회원 체크 API 호출에서 99%가 30ms, 1%가 3.5초
- Span 분석으로 원인: 외부 API의 특정 사용자 유형에서 timeout 직전까지 갔다가 응답
조치:
- 휴면 체크를 비동기 처리 + 로컬 캐시 30분
- p99: 4s → 180ms
교훈:
- “모니터링 대시보드에는 안 보이는” 문제가 실제론 많음
- Trace ID 전파 + Span 수집만 있으면 90%의 미스터리 장애가 풀림
- 도입 비용은 OpenTelemetry SDK 한 번 적용 + 헤더 propagate 정도
🔄 꼬리질문 1: 옵저버빌리티의 세 가지 축(Pillar)은 뭔가요?
기대 답변:
- Logs: 개별 이벤트 기록. 디버깅에 필수.
- Metrics: 집계된 수치. 트렌드 파악, 알림 설정.
- Traces: 요청의 전체 흐름 추적. 분산 시스템 디버깅.
📋 실제 사례
상황: 새벽 결제 에러율이 0.1% → 5%로 급등한 장애. 세 축을 연결해서 5분 만에 근본 원인에 도달한 사례.
세 축의 협업:
- Metrics: Grafana 알람이 결제 에러율 5% 를 먼저 감지 → “문제가 있다”
- Traces: 에러 trace를 따라가니 결제 → 쿠폰 검증 서비스 구간에서 실패 집중 → “어디서 시작됐다”
- Logs: 해당 span의 로그를 trace_id로 검색 →
쿠폰 DB connection pool exhaustedstack trace → “왜 실패했다”
조치:
- 쿠폰 서비스 커넥션 풀 고갈(슬로우 쿼리 누적)이 원인 → 슬로우 쿼리 인덱스 추가 + 풀 사이즈 조정
결과:
- 5분 내 근본 원인 도달 (이전 같으면 추정으로 수 시간)
- 에러율 5% → 0.1% 복구
교훈: 세 축은 각각 따로가 아니라 Metrics(이상 감지) → Traces(위치 특정) → Logs(원인 확인) 로 연결될 때 가치가 폭발한다. 핵심 연결고리는 모든 로그·span에 박힌 trace_id다.
🔄 꼬리질문 2: 분산 트레이싱을 어떻게 구현하나요?
기대 답변:
- Trace ID: 요청마다 고유 ID 부여, 모든 서비스에 전파
- Span: 각 서비스에서의 작업 단위
- 도구: Jaeger, Zipkin, OpenTelemetry
- 로그에 Trace ID 포함하여 연관 검색 가능
📋 실제 사례
상황: 15개 마이크로서비스 환경에서 한 요청이 여러 서비스를 거치는데, 장애 시 각 서비스 로그를 따로 뒤져야 해서 한 요청의 전체 흐름 재구성에만 30분 이상.
조치 — OpenTelemetry + Jaeger 도입:
- 게이트웨이가 진입 시
trace_id발급 - 모든 서비스가 W3C
traceparent헤더를 받아서 전파 (HTTP·gRPC·Kafka 헤더) - 로그 포맷에
trace_id필드 자동 주입 (MDC) - Kibana/Jaeger에서
trace_id하나로 15개 서비스 로그·span 통합 조회
결과:
- 한 요청 전체 흐름 재구성: 30분 → 10초
- 도입 비용 약 1주 (SDK 적용 + 헤더 전파 표준화)
교훈: 분산 트레이싱의 핵심은 trace_id를 모든 홉(HTTP/gRPC/Kafka)에 빠짐없이 전파하고, 모든 로그에 trace_id를 박는 것이다. 도입 비용은 1주지만 첫 미스터리 장애에서 즉시 회수된다.
🔄 꼬리질문 3: 알림 피로(Alert Fatigue)는 어떻게 방지하나요?
기대 답변:
- 알림 우선순위: P1(즉시 대응), P2(업무시간), P3(모니터링)
- 그룹핑: 동일 이슈 알림 묶기
- Runbook: 알림별 대응 절차 문서화
- 주기적 리뷰: 의미 없는 알림 제거
📋 실제 사례
상황: 야간 온콜에게 알람이 일 평균 12회 울려 1차 응답자가 번아웃, 결국 진짜 P1 알람도 무시하기 시작하는 위험 단계 (the boy who cried wolf).
진단 — 분기 알람 리뷰:
- 30일치 알람을 분석하니 실제 대응 액션이 있었던 알람은 25% 뿐
- 나머지 75%는 “잠깐 튀었다가 자가 회복”하는 노이즈 (예: 일시적 CPU 80% spike)
조치:
- 실제 액션 없는 7종 알람 임계·지속시간 조정 (예: “CPU 80% 5분 지속”으로 변경) 또는 P3(대시보드)로 강등
- 동일 원인 알람 5분 그룹핑 (한 장애가 10개 알람 안 됨)
- 모든 P1 알람에 Runbook 링크 첨부
결과:
- 야간 알람: 일 12회 → 3회
- 1차 응답자가 모든 알람을 진지하게 받는 상태로 회복
교훈: 알람은 많이 만드는 것이 아니라 대응 가능한 것만 남기는 운영 작업이다. “액션 없는 알람”은 신뢰를 갉아먹어 진짜 알람마저 무시되게 만든다. 분기 리뷰로 노이즈를 주기적으로 쳐낸다.
5. 장애 복구와 포스트모템
Q5. 장애가 발생하면 어떤 순서로 대응하시나요?
기대 답변:
- 탐지: 알림 또는 모니터링으로 인지
- 완화: 롤백, 스케일업, 트래픽 차단 등 즉각 조치
- 원인 분석: 로그, 메트릭, 트레이스 분석
- 해결: 근본 원인 수정
- 포스트모템: 재발 방지책 수립
💡 실제 사례 보기
시나리오: 새벽 3시 P1 알림. 결제 API 5xx 비율 80%, 전 사용자 결제 실패.
타임라인:
- 03:02 PagerDuty 알림, 1차 응답자 wake-up
- 03:08 슬랙 워룸 개설, 인시던트 채널 자동 생성
- 03:12 5분 전 배포된 결제 모듈이 원인으로 1차 가설 → 즉시 롤백 (mitigation 우선)
- 03:18 롤백 완료, 결제 성공률 99%로 회복
- 03:30 워룸 해산, 다음 날 RCA 미팅 예약
다음 날 포스트모템:
| 항목 | 내용 |
|---|---|
| 타임라인 | 발생 02:55, 인지 03:02, 회복 03:18, 총 23분 |
| 영향 범위 | 결제 시도 12,400건 중 8,920건 실패 (72%), 매출 손실 4,300만 원 |
| 근본 원인 | 결제 옵션 추가 PR이 enum 값 누락으로 NPE, 통합 테스트가 그 케이스를 안 다룸 |
| 잘된 점 | 자동 알림이 5분 내 작동, 1차 응답자가 즉시 롤백 결정 |
| 개선점 | (1) enum 변경 시 컨트랙트 테스트 강제, (2) 카나리 5% 5분 자동 모니터링 게이트 |
| Blame-free | 개인 책임 추궁 없음, 시스템 개선 4건 액션 아이템화 |
액션 아이템:
- 결제 모듈 카나리 게이트 도입 (1주)
- enum 변경 시 contract test 강제 (2주)
- 야간 1차 응답자 룸북 업데이트 (즉일)
교훈:
- 장애 대응 속도가 근본 해결보다 우선 (이미 일어난 일은 빨리 복구)
- 포스트모템은 Blame-free가 핵심, 안 그러면 솔직한 RCA가 안 나옴
- 같은 장애 재발 시 액션 아이템이 안 끝났다는 신호 → 추적 도구 필수
🔄 꼬리질문 1: 포스트모템에는 어떤 내용이 들어가나요?
기대 답변:
- 타임라인: 언제 발생, 언제 인지, 언제 해결
- 영향 범위: 영향받은 사용자 수, 시간
- 근본 원인: 왜 발생했는지
- 재발 방지: 어떻게 막을 것인지 (액션 아이템)
- Blame-free: 개인 탓 아닌 시스템 개선 관점
📋 실제 사례
상황: 포스트모템 문화는 있었지만 *액션 아이템 완료율이 40%*에 그쳐, 비슷한 장애가 반복됨. “회고는 하는데 개선은 안 되는” 상태.
진단:
- 액션 아이템에 담당자·마감일이 없어 흐지부지
- 회고 문서가 작성 후 아무도 다시 안 봄
조치 — 액션 아이템 추적 강제:
- 포스트모템 템플릿에 owner + 마감일 + Jira 티켓 필수화
- 분기마다 미완료 액션 아이템 리뷰 회의
- 같은 카테고리 장애 재발 시 “이전 액션 아이템이 안 끝났다는 신호” 로 우선순위 격상
결과:
- 액션 아이템 완료율: 40% → 90%
- 동일 패턴 장애 재발 60% 감소
교훈: 포스트모템의 가치는 잘 쓴 문서가 아니라 액션 아이템 실행률이다. owner·마감일·티켓 없는 액션 아이템은 실행되지 않는다. 추적 가능하게 만드는 게 회고 문화의 본질.
🔄 꼬리질문 2: RTO와 RPO가 뭔가요?
기대 답변:
- RTO (Recovery Time Objective): 복구 목표 시간. 장애 후 얼마 안에 복구해야 하는가.
- RPO (Recovery Point Objective): 복구 목표 시점. 얼마만큼의 데이터 손실을 허용하는가.
- 비즈니스 요구사항에 따라 백업 주기, 복제 전략 결정
📋 실제 사례
상황: 결제 DB의 백업 전략을 정할 때, 처음엔 비용 절감을 위해 일 1회 백업(RPO 24시간)을 검토했으나, “마스터가 죽으면 최대 24시간치 결제 데이터가 사라진다”는 의미임을 비즈니스에 환산하니 받아들일 수 없었음.
조치 — 도메인별 RTO/RPO 차등:
- 결제 DB: RTO 15분, RPO 0초 → 동기 복제(Multi-AZ) + 자동 페일오버. 데이터 손실 불가
- 통계/로그 DB: RTO 4시간, RPO 1시간 → 일반 스냅샷 백업 (비용 절감)
- 분기마다 DR 훈련으로 실제 RTO 측정 (페일오버 스크립트가 진짜 15분 안에 도는지)
결과:
- 결제 마스터 장애 실제 발생 시 데이터 손실 0, 4분 만에 페일오버 복구
- 비핵심 도메인은 과투자 없이 비용 최적화
교훈: RTO/RPO는 기술 스펙이 아니라 “데이터를 얼마나 잃으면 / 얼마나 멈추면 비즈니스가 얼마를 잃는가” 의 환산이다. 도메인마다 다르게 정하고, DR 훈련으로 실제 값을 검증해야 한다 (설정값과 실측이 다를 수 있음).
🔄 꼬리질문 3: Chaos Engineering 해보신 적 있나요?
기대 답변: 의도적으로 장애를 주입하여 시스템 복원력을 테스트하는 방법입니다.
- Netflix Chaos Monkey: 랜덤 인스턴스 종료
- Gremlin, LitmusChaos 등 도구 활용
- 프로덕션 적용 전 스테이징에서 충분히 테스트
📋 실제 사례
상황: “DB 마스터가 죽어도 자동 페일오버 30초면 복구된다”고 문서상으로만 믿고 있었음. 실제로 검증한 적은 없었음.
조치 — 스테이징에서 Chaos 실험:
- LitmusChaos로 스테이징 DB 마스터를 강제 종료
- 가설: 30초 내 replica 승격 + 애플리케이션 재연결
- 실제 측정: 페일오버에 2분 소요 (DNS TTL + 커넥션 풀 재연결 지연이 누적)
조치 (실험 결과 반영):
- 커넥션 풀 health check 주기 단축, DB endpoint를 DNS가 아닌 라우팅 프록시로 변경
- 재실험에서 페일오버 35초로 단축
결과:
- 운영 환경에서 진짜 마스터 장애가 났을 때 실제로 35초 만에 복구
- “문서상 30초”가 실제로는 2분이었던 위험을 사전에 제거
교훈: Chaos Engineering은 훈련이 아니라 “우리가 믿고 있는 가정의 검증” 이다. “자동 복구된다”는 문서상 믿음과 실측은 거의 항상 다르다. 스테이징에서 먼저, 충분히 검증한 뒤 운영으로 확장한다.
마무리: 2편 핵심 정리
- 장애 대응: 서킷 브레이커, Fallback, Timeout 전략
- 이벤트 아키텍처: Kafka 유실 방지, 순서 보장, CQRS
- 배포: 무중단 배포, DB 스키마 변경, Feature Flag
- 옵저버빌리티: Logs/Metrics/Traces, 분산 트레이싱
- 복구: 포스트모템 문화, RTO/RPO, Chaos Engineering
다음 편에서는 설계 의사결정, 시스템 디자인, 코드 리뷰를 다룹니다.