시니어 백엔드 면접에서 운영 역량을 묻는 질문은 대개 이렇게 시작한다. - “장애를 어떻게 탐지하고 대응하나요?” - “알람이 너무 많이 울리면 어떻게 줄이나요?” - “p99 latency가 튀는데 CPU는 정상입니다. 어디를 보겠습니까?” - “신규 기능 출시 속도와 안정성은 어떻게 조율하나요?” 이 질문의 핵심은 도구 이름이 아니다. Prometheu...
시니어 백엔드 면접에서 운영 역량을 묻는 질문은 대개 이렇게 시작한다.
이 질문의 핵심은 도구 이름이 아니다. Prometheus, Grafana, Datadog, OpenTelemetry를 안다는 것보다 중요한 것은 사용자 영향 기준으로 신뢰성을 정의하고, 그 신뢰성 예산 안에서 출시와 장애 대응을 의사결정하는 능력이다.
SLO(Service Level Objective)와 Error Budget은 이 의사결정의 언어다. “장애가 났다/안 났다”가 아니라 “이번 달 사용자에게 허용한 실패 예산을 얼마나 태웠는가”로 대화하게 만든다.
이 문서는 시니어 백엔드 관점에서 다음을 정리한다.
관련 문서인 Observability 입문, Resilience 패턴, 커머스/F&B 채널 장애 첫 5분은 “무엇을 볼 것인가”와 “어떻게 격리할 것인가”에 가깝다. 이 문서는 한 단계 위에서 어떤 기준으로 알리고, 멈추고, 재개할 것인가를 다룬다.
SLI는 서비스 수준을 나타내는 측정 지표다. “사용자 입장에서 좋은 요청인가?”를 수치로 표현한다.
대표 SLI:
커머스 주문 API 예시:
SLI = 정상 주문 생성 요청 수 / 전체 주문 생성 요청 수
정상 주문 생성 = HTTP 2xx 이면서 body.status = SUCCESS 이고 주문번호가 생성됨주의할 점은 HTTP 상태 코드만 보면 안 된다는 것이다. 결제 PG는 200 OK로 응답하면서 body 안에 status=FAIL을 담는 경우가 많다. 사용자는 결제 실패를 경험했는데 서버 메트릭상 2xx로 집계되면 SLI가 거짓말을 한다.
SLO는 SLI에 대한 목표치다.
주문 생성 API의 월간 availability SLO는 99.9%다.
결제 승인 API의 99% 요청은 1초 이내에 완료되어야 한다.
상품 상세 API의 95% 요청은 300ms 이내에 응답해야 한다.SLO는 “100%”가 아니다. 100%는 현실적으로 불가능하고, 그 목표를 세우면 팀은 아무것도 출시하지 못한다. 시니어 엔지니어는 “어느 정도의 실패가 비즈니스적으로 허용 가능한가”를 제품·운영·인프라 팀과 합의해야 한다.
SLA는 외부 고객과 맺은 계약이다. 보통 위반 시 보상이나 패널티가 있다.
일반적으로 SLA는 SLO보다 느슨하게 잡는다. 예를 들어 내부 SLO는 99.95%, 외부 SLA는 99.9%로 둔다. 내부 목표를 더 엄격하게 잡아야 외부 계약 위반 전에 대응할 여유가 생긴다.
좋은 SLO는 다음 조건을 만족한다.
커머스 백엔드 예시:
| 도메인 | 권장 SLO | 이유 |
|---|---|---|
| 주문 생성 | availability 99.9% | 매출과 직접 연결 |
| 결제 승인 | success rate 99.5% + p99 2s | 외부 PG 영향 포함 |
| 상품 조회 | p95 300ms | UX 영향 큼, 실패는 캐시 fallback 가능 |
| 추천 영역 | availability 99% | 실패 시 숨김/대체 가능 |
| 포인트 적립 | eventual completion 99.9% within 10m | 동기 성공보다 최종 정합성이 중요 |
시니어 레벨 답변에서는 “모든 API에 99.99%를 붙입니다”가 아니라, 핵심 경로와 보조 경로의 SLO를 다르게 둔다고 말해야 한다.
Error Budget은 SLO가 허용하는 실패 예산이다.
월간 availability SLO가 99.9%라면 허용 실패율은 0.1%다.
월 요청 수 = 100,000,000
SLO = 99.9%
허용 실패율 = 0.1%
Error budget = 100,000 실패 요청시간 기준으로 보면 30일 월 기준 허용 다운타임은 약 43.2분이다.
30일 = 43,200분
0.1% = 43.2분99.99%라면 월 4.32분밖에 없다. 이 차이는 운영 난이도를 완전히 바꾼다.
Error Budget은 출시와 안정성의 균형 장치다.
이 방식의 장점은 “안정성이 중요하니 배포하지 말자” 같은 추상적인 논쟁을 줄인다는 것이다. 예산이라는 숫자가 있으므로 제품팀과 엔지니어링팀이 같은 언어로 이야기할 수 있다.
Burn rate는 error budget을 얼마나 빠르게 태우고 있는지를 나타낸다.
burn rate = 현재 오류율 / 허용 오류율SLO 99.9%의 허용 오류율은 0.1%다. 현재 오류율이 1%라면:
burn rate = 1% / 0.1% = 10x즉 지금 속도가 계속되면 예산을 정상 속도의 10배로 태운다는 뜻이다.
단순히 “5xx > 1% for 5m”로 알리면 서비스마다 기준이 달라진다. 99.99% SLO 서비스에서 0.2% 오류는 심각하지만, 99% SLO 서비스에서는 상대적으로 덜 심각할 수 있다.
Burn rate는 SLO 기준으로 정규화하므로 서비스별 중요도를 반영한다.
Google SRE에서 널리 쓰는 방식이다. 하나의 긴 창과 하나의 짧은 창을 같이 본다.
목표:
예시: 30일 SLO 기준
| 종류 | 짧은 창 | 긴 창 | burn rate | 의미 |
|---|---|---|---|---|
| Fast burn page | 5m | 1h | 14.4x | 2% budget을 1시간에 태움 |
| Slow burn ticket | 30m | 6h | 6x | 5% budget을 6시간에 태움 |
| Very slow burn | 2h | 1d | 3x | 장기 품질 저하 |
Prometheus 예시:
groups:
- name: order-slo-alerts
rules:
- alert: OrderApiFastBurn
expr: |
(
sum(rate(http_server_requests_seconds_count{service="order",outcome="SERVER_ERROR"}[5m]))
/
sum(rate(http_server_requests_seconds_count{service="order"}[5m]))
) > (14.4 * 0.001)
and
(
sum(rate(http_server_requests_seconds_count{service="order",outcome="SERVER_ERROR"}[1h]))
/
sum(rate(http_server_requests_seconds_count{service="order"}[1h]))
) > (14.4 * 0.001)
for: 2m
labels:
severity: page
service: order
annotations:
summary: "order-service is burning error budget fast"
runbook_url: "https://wiki/runbooks/order-slo"
dashboard: "https://grafana/d/order-slo"여기서 0.001은 99.9% SLO의 허용 오류율이다.
핵심은 5분과 1시간을 동시에 만족해야 한다는 점이다. 1~2분짜리 단발 스파이크는 무시하고, 실제 지속 장애만 page한다.
Availability만으로는 부족하다. 사용자가 10초 기다린 뒤 성공해도 성공 요청으로 집계되면 SLO는 좋아 보인다.
Latency SLO는 보통 “좋은 요청”의 조건에 latency를 포함한다.
Good event = status success AND duration <= 1s
Total event = all requests
SLI = good event / total eventMicrometer histogram bucket으로 계산할 수 있다.
sum(rate(http_server_requests_seconds_bucket{service="order",le="1.0",status!~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_count{service="order"}[5m]))주의할 점:
Spring Boot Actuator와 Micrometer를 쓰면 기본 HTTP metric이 나온다.
management:
endpoints:
web:
exposure:
include: health,prometheus,info
metrics:
tags:
service: order-service
env: prod
distribution:
percentiles-histogram:
http.server.requests: true
slo:
http.server.requests: 100ms,300ms,500ms,1s,2s,5s도메인 성공/실패는 별도 counter로 노출한다.
@Service
@RequiredArgsConstructor
public class OrderMetrics {
private final MeterRegistry registry;
public void orderAttempt(String channel) {
registry.counter("order_attempt_total", "channel", channel).increment();
}
public void orderSuccess(String channel) {
registry.counter("order_success_total", "channel", channel).increment();
}
public void orderFailure(String channel, String reason) {
registry.counter("order_failure_total",
"channel", channel,
"reason", normalize(reason)
).increment();
}
private String normalize(String reason) {
return switch (reason) {
case "PG_TIMEOUT", "PG_DECLINED", "OUT_OF_STOCK", "IDEMPOTENCY_CONFLICT" -> reason;
default -> "OTHER";
};
}
}reason에 원본 exception message를 넣으면 cardinality가 폭발한다. 반드시 작은 enum으로 정규화한다.
알림이 많다는 것은 운영이 성숙하다는 뜻이 아니다. 오히려 위험 신호다. 사람이 무시하기 시작하면 진짜 장애도 묻힌다.
좋은 알림의 조건:
나쁜 알림:
좋은 알림:
원칙은 cause보다 symptom을 먼저 page한다는 것이다. CPU, GC, DB connection은 대시보드와 ticket으로 충분한 경우가 많다. 사용자가 실패를 경험할 때 page한다.
시니어 백엔드는 장애 상황에서 “내가 코드를 고치는 사람”을 넘어 상황을 구조화하는 사람이어야 한다.
이 단계의 목표는 원인 확정이 아니다. 영향 범위와 심각도 확정이다.
완화는 근본 원인 분석보다 우선한다. 사용자가 계속 실패하고 있다면 먼저 출혈을 멈춘다.
장애가 커질수록 가장 위험한 것은 모두가 각자 디버깅하는 상황이다. Incident Commander(IC)는 직접 코드를 고치는 사람이 아니라 흐름을 잡는 사람이다.
IC의 역할:
시니어 면접에서 강한 답변은 “제가 로그를 봤습니다”가 아니라 “저는 IC 역할로 범위를 나누고, 한 명은 PG 상태를, 한 명은 직전 배포를, 한 명은 DB saturation을 보게 했습니다”에 가깝다.
Postmortem은 책임자 찾기가 아니다. 시스템이 왜 그 실패를 허용했는지 찾는 문서다.
최소 템플릿:
# Incident: 주문 생성 실패율 상승
## Summary
- 2026-05-15 12:03~12:28 KST 동안 주문 생성 실패율이 0.2%에서 3.8%로 상승.
- 약 1,240건 주문 생성 실패.
- PG A사의 응답 지연과 우리 retry 정책이 결합되어 order-service thread pool saturation 발생.
## Impact
- 영향 사용자 수:
- 실패 주문 수:
- 매출 영향 추정:
- SLA/SLO 영향:
## Timeline
- 12:03 SLO fast burn alert fired
- 12:05 IC 지정, incident channel 생성
- 12:08 PG A latency p99 상승 확인
- 12:12 PG A circuit breaker threshold 수동 조정
- 12:18 retry maxAttempts 3 -> 1 임시 변경
- 12:28 주문 실패율 정상화
## Root Cause
- PG A 응답 지연
- order-service의 PG retry가 짧은 시간에 집중되어 thread pool saturation 유발
## What went well
- SLO fast burn alert가 2분 내 발화
- PG별 dependency dashboard로 범위 빠르게 축소
## What went poorly
- retry storm 여부를 바로 볼 수 있는 대시보드가 없었음
- runbook에 circuit 수동 open 절차가 누락됨
## Action Items
- [ ] PG별 retry rate dashboard 추가
- [ ] circuit breaker 수동 open runbook 작성
- [ ] 결제 retry budget 도입
- [ ] 부하 테스트에 PG 2s 지연 시나리오 추가좋은 postmortem은 action item이 코드만이 아니라 알림 / 플레이북 / 테스트 / 설계로 나뉜다.
SLI: 성공적으로 주문번호가 생성된 요청 비율
Good: HTTP 2xx AND body.status=SUCCESS AND orderId exists
SLO: 99.9% monthly
Page: 14.4x burn over 5m + 1h
Ticket: 6x burn over 30m + 6hSLI: 결제 승인 요청 중 최종 성공 또는 명확한 사용자 실패로 분류된 비율
Good: 승인 성공 OR 사용자의 잔액 부족/카드 거절처럼 시스템 문제가 아닌 실패
Bad: PG_TIMEOUT, PG_5XX, UNKNOWN, 내부 처리 실패
SLO: 99.5% monthly결제는 “실패”의 의미가 까다롭다. 카드 한도 초과는 시스템 장애가 아니다. PG timeout은 시스템 장애다. 이 구분을 metric label에 반영해야 SLO가 정확해진다.
SLI: 300ms 이하로 정상 응답한 상품 상세 요청 비율
SLO: 99% over 7d
Fallback: cache stale response 허용상품 조회는 캐시 fallback이 가능하므로 주문/결제와 같은 SLO를 요구하지 않아도 된다.
질문: “장애를 어떻게 탐지하고 대응하시나요?”
답변 구조:
좋은 한 문장:
“저는 장애 대응을 개별 로그 분석 문제가 아니라 SLO와 error budget을 기준으로 한 의사결정 문제로 봅니다. 사용자가 실패를 경험하는 지표로 알리고, burn rate로 긴급도를 정하고, 장애 중에는 원인 규명보다 완화를 먼저 선택합니다.”