0.2 현장의 문제는 CS 과목의 경계를 지키지 않는다
운영 시스템의 요청 하나는 알고리즘, 런타임, 운영체제, 네트워크, 저장 엔진을 차례로 통과한다. 증상을 제품이나 도구의 이름으로 분류하지 않고 비용 모델·불변식·계약·증거로 바꾸면 여러 CS 영역의 지식을 하나의 설계와 디버깅 과정으로 연결할 수 있다.
학습 목표
- 하나의 현상이 여러 시스템 계층의 상호작용에서 발생하는 과정을 설명한다.
- 증상을 비용·불변식·계약 관점의 가설로 변환하고 계층별 증거를 선택한다.
- 설계, 디버깅, 기술 선택에 공통으로 쓰이는 판단 절차를 적용한다.
- 국소 최적화가 전체 시스템의 지연·처리량·정합성을 악화하는 경우를 판별한다.
배경: 장애 티켓에는 과목명이 적혀 있지 않다
사용자가 주문 확정 버튼을 눌렀는데 화면은 5초 뒤 타임아웃을 표시했다. 다시 누르자 주문이 두 건 생성됐다. 같은 시간대에 애플리케이션의 CPU 사용률은 55%, 데이터베이스 CPU는 40%였고 오류율은 2%였다. 평균 응답 시간은 평소보다 20%만 증가했지만 서버에서 기록한 p99는 400ms에서 6.5초로 치솟았다.
이 문제는 어느 팀과 과목의 문제인가?
- 프런트엔드가 버튼을 다시 누를 수 있게 둔 UX 문제인가?
- HTTP 클라이언트의 타임아웃과 재시도 문제인가?
- 서버의 이벤트 루프나 스레드 풀이 막힌 런타임 문제인가?
- 주문 ID 중복을 막지 못한 데이터 모델 문제인가?
- 트랜잭션과 잠금 때문에 생긴 데이터베이스 문제인가?
- 메시지가 중복 전달된 분산 시스템 문제인가?
- 평균만 보고 꼬리 지연을 놓친 관찰 가능성 문제인가?
정답은 하나가 아닐 수 있다. 사용자의 요청은 브라우저, 로드 밸런서, 애플리케이션, 데이터베이스, 메시지 브로커를 지나며 각 계층의 큐와 상태를 통과한다. 한 계층의 작은 지연이 다른 계층의 타임아웃을 넘기고, 재시도가 부하를 늘리며, 멱등성 없는 쓰기가 중복 상태를 만들 수 있다. 결과는 여러 계층이 조립한 하나의 실패다.
대학의 과목 구분과 이 커리큘럼의 챕터 구분은 학습을 가능하게 하는 경계다. 운영 시스템의 실행 경계는 아니다. 자료구조만 알아서는 데이터베이스 잠금을 설명할 수 없고, 네트워크만 알아서는 중복 주문을 막을 수 없다. 그렇다고 모든 문제에서 모든 지식을 동원할 수도 없다. 필요한 능력은 현상을 구성 요소와 계약으로 분해하고, 가능성이 높은 계층부터 증거로 좁히는 것이다.
요청 하나를 시스템 전체로 펼쳐 보기
주문 확정 API의 정상 경로를 단순화하면 다음과 같다.
사용자 클릭
│
▼
HTTP 클라이언트 ── DNS/TCP/TLS ──▶ 로드 밸런서
│
▼
애플리케이션 큐
│
검증 → 재고 확인 → 주문 저장
│
┌──────────────┴──────────────┐
▼ ▼
데이터베이스 메시지 브로커
락·로그·플러시 결제/알림 이벤트코드에서는 confirmOrder()라는 함수 하나로 보일 수 있지만, 실제 완료 시간은 각 구간의 실행 시간과 대기 시간의 합이다. 각 구간은 다른 모델을 요구한다.
| 구간 | 핵심 질문 | 필요한 모델 |
|---|---|---|
| 입력 검증과 계산 | 입력 크기에 따라 연산량이 어떻게 증가하는가? | 알고리즘, 파싱, 복잡도 |
| 객체 생성과 함수 실행 | 할당·GC·JIT가 지연에 어떤 패턴을 만드는가? | 런타임, 메모리 관리, 컴파일 |
| 태스크 실행 | CPU를 실행한 시간과 큐·락에서 기다린 시간은 얼마인가? | 스케줄링, 동시성, I/O |
| 원격 호출 | 연결·손실·타임아웃·재시도가 어떻게 상호작용하는가? | 네트워크, 분산 시스템 |
| 주문 저장 | 인덱스·락·로그·격리 수준이 어떤 비용과 보장을 만드는가? | 데이터 구조, DB 내부, 트랜잭션 |
| 이벤트 발행 | DB 상태 변경과 메시지 전달 사이의 원자성을 어떻게 다루는가? | 분산 트랜잭션, 로그, 멱등성 |
| 전체 변경 | 실패를 어떻게 감지·완화·복구하고 재발을 막는가? | 신뢰성, 배포, 관찰 가능성 |
표의 목적은 모든 행을 동시에 깊게 조사하는 것이 아니다. 요청 경로를 펼치면 “CPU가 55%니 애플리케이션 계산은 문제가 아니다”처럼 근거가 약한 결론을 피할 수 있다. 전체 평균 CPU가 낮아도 특정 코어의 이벤트 루프가 막히거나, 스레드 풀이 포화되거나, 대부분의 요청이 락에서 기다릴 수 있다. 데이터베이스 CPU가 낮아도 연결 풀과 행 잠금 큐는 길 수 있다.
여러 영역을 잇는 네 가지 공통 언어
CS 과목마다 용어는 다르지만 이 문서에서는 현장 판단에서 반복되는 질문을 네 종류로 압축한다.
비용 모델: 무엇이 증가하면 무엇이 비싸지는가
비용 모델(cost model)은 입력이나 상태가 변할 때 시간·공간·데이터 이동·운영 복잡성이 어떻게 변할지 설명하는 가설이다.
- 배열의 선형 탐색은 원소 수에 비례해 비교 횟수가 늘어난다.
- 스레드 풀이 모두 사용 중이면 추가 요청은 실행되지 않고 큐에서 기다린다.
- 병렬 분기(fan-out) 요청이 여러 하위 서비스의 응답을 모두 기다리면 가장 느린 응답 경로와 집계 오버헤드가 전체 지연을 정한다.
- 인덱스를 추가하면 일부 조회는 빨라지지만 쓰기마다 갱신할 구조와 저장 공간이 늘어난다.
- 복제본을 늘리면 읽기 용량과 장애 내성은 얻지만 복제 지연과 일관성 정책이 추가된다.
비용 모델의 핵심은 “A가 빠르다”가 아니라 “어떤 축이 어느 범위에서 지배적인가”이다. 메모리 캐시는 작업 집합이 용량 안에 있고 접근 지역성이 충분할 때 효과적이다. 작업 집합이 메모리를 넘으면 축출과 원본 조회가 늘고 이득이 줄어든다. 이벤트 기반 네트워크 I/O는 요청마다 대기 전용 스레드를 둘 필요를 줄일 수 있지만, 모든 비동기 API가 스레드를 사용하지 않는다는 뜻은 아니다. 예를 들어 이 저장소의 기준 런타임인 Node.js 24.14 이상에서 비동기 파일 API는 호출자를 이벤트 루프에서 차단하지 않는 대신 libuv 스레드 풀에서 작업한다. 어느 방식도 CPU 바운드 작업을 자동으로 병렬화하지 않는다.
비용 모델은 설계와 디버깅을 잇는다. 설계 시에는 예상 워크로드로 후보를 비교하고, 장애 시에는 실제 지표가 어느 가정을 위반했는지 찾는다.
불변식: 실패와 동시성 속에서도 무엇이 참이어야 하는가
불변식(invariant)은 시스템 상태가 바뀌는 동안에도 지켜야 할 조건이다. 주문 시스템에서는 다음과 같은 조건이 있을 수 있다.
- 하나의 결제 의도에는 성공한 결제가 최대 하나다.
- 각 상품의 가용 재고 수량은 0 미만이 되지 않는다.
- 주문이
PAID상태라면 대응하는 결제 승인 기록이 존재한다. - 같은 멱등성 키로 재시도한 요청은 새 주문을 만들지 않는다.
불변식이 없으면 기술 선택을 평가할 기준도 없다. “Kafka를 쓰자”, “분산 락을 걸자”, “격리 수준을 높이자”는 해결책처럼 들리지만 무엇을 지키려는지 빠져 있다. 불변식을 먼저 적으면 선택지가 달라진다.
예를 들어 주문 중복 방지는 전역 분산 락만으로 해결할 필요가 없다. 하나의 주문 의도에서 안정적으로 재사용하는 멱등성 키와 데이터베이스의 유일성 제약을 사용하면 경쟁 요청 중 하나만 상태를 만들도록 할 수 있다. 이 선택은 저장소가 제공하는 원자적 조건부 쓰기라는 의미 계약을 이용한다. 반면 서로 다른 저장소의 상태를 동시에 바꿔야 한다면 로컬 유일성 제약만으로는 불변식이 완성되지 않아 아웃박스, 보상, 합의 같은 다른 모델이 필요하다.
정확성 불변식과 운영 목표는 구분해야 한다. 불변식은 모든 허용된 상태 전이에서 지켜야 하지만, 백분위 지연은 측정 구간과 모집단을 가진 통계적 목표이며 자원 상한은 과부하를 제어하는 안전 경계다.
- 서비스 수준 목표(service level objective, SLO): 정의한 정상 부하와 측정 구간에서 주문 확정 p99는 800ms 이하여야 한다.
- 자원 안전 한계: 한 요청은 하위 서비스에 최대 한 번의 동시 시도만 만들고, 큐 적체량은 복구 가능한 저장 한도를 넘기기 전에 유입을 제한한다.
이 조건들은 설계가 무엇을 희생할 수 있는지 결정한다. 정확성 불변식은 지키되 알림은 지연시킬 수 있고, 부하가 높을 때 추천 기능을 끄되 주문 확정은 유지할 수 있다.
계약: 계층과 팀 사이에서 누가 무엇을 책임지는가
0.1에서 의미·자원·실패 계약을 구분했다. 여러 계층이 만나는 시스템에서는 계약의 조립이 중요하다.
브로커가 적어도 한 번 전달하고, 소비자가 트랜잭션으로 DB를 갱신하며, 네트워크가 응답을 유실할 수 있다고 하자. 각 구성 요소의 계약은 개별적으로 맞아도 “이벤트 하나가 비즈니스 상태를 정확히 한 번 바꾼다”는 종단 계약은 자동으로 생기지 않는다. 소비자는 중복을 식별하고 이미 적용한 이벤트를 다시 적용하지 않거나, 상태 전이를 조건부로 수행해야 한다.
계약을 연결할 때 다음 질문을 한다.
- 한 계층의 성공이 상위 계층에서 무엇을 의미하는가?
- 성공 응답을 받지 못했을 때 실제 상태는 몇 가지인가?
- 중간 결과를 다시 실행하거나 되돌릴 수 있는가?
- 시간·용량 한도를 누가 소유하며 초과 시 어떻게 신호를 보내는가?
- 버전 변경과 구현 교체에서 유지해야 할 관찰 가능한 행동은 무엇인가?
이 질문은 기술 문제이면서 조직 문제다. API 제공 팀이 실패 의미를 정의하지 않으면 소비 팀이 재시도 정책을 안전하게 설계할 수 없다. 데이터 플랫폼이 복제 지연을 노출하지 않으면 애플리케이션은 read-after-write가 필요한 경로를 구분할 수 없다.
증거: 어떤 관찰이 가설을 구분하는가
로그 한 줄이나 대시보드 하나는 원인이 아니다. 증거(evidence)는 경쟁하는 가설 중 일부를 지지하거나 기각할 수 있어야 한다.
“주문 API가 느리다”에서 가능한 가설을 나누어 보자.
| 가설 | 필요한 관찰 | 가설을 약화하는 증거 |
|---|---|---|
| CPU 계산이 길다 | 요청 구간별 CPU 프로파일, 코어별 사용률 | wall time은 긴데 on-CPU 표본이 적다 |
| GC 정지가 길다 | GC 이벤트와 요청 지연의 시간 상관, 힙 변화 | 지연 구간에 정지가 없고 힙도 안정적이다 |
| 스레드/연결 풀이 포화됐다 | active·idle·queue·wait time | 대기열이 없고 즉시 자원을 얻는다 |
| DB 락을 기다린다 | 세션 wait event, 잠금 그래프, 트랜잭션 시간 | DB 구간이 짧고 잠금 대기가 없다 |
| 하위 호출 꼬리 지연이다 | 분산 트레이스, 하위 p50/p95/p99 | 하위 span은 빠르고 상위 로컬 구간이 길다 |
| 재시도가 부하를 증폭한다 | 원 시도 대비 총 시도 수, 오류별 재시도율 | 실패 때도 시도 배율이 변하지 않는다 |
증거는 모델 없이 해석되지 않는다. CPU 사용률이 낮다는 사실은 “CPU 문제가 아니다”가 아니라 샘플링 범위, 코어 수, 대기 시간 모델과 결합되어야 의미가 있다. 분산 트레이스에 긴 빈 구간이 있다면 트레이싱되지 않은 로컬 큐, 런타임 정지, 계측 누락 중 무엇인지 추가 가설이 필요하다.
통합 사례: 느리고 중복되는 주문 확정
이제 출발점의 사건을 가상의 조사 기록으로 따라가 보자. 목적은 실제 장애의 정답을 맞히는 것이 아니라 여러 계층을 어떻게 좁히는지 보는 것이다.
1단계: 사용자 영향과 변경 시점을 고정한다
팀은 먼저 다음을 확인했다.
- 14:05부터 서버에서 기록한 주문 확정 p99가 400ms에서 6.5초로 증가했다.
- p50은 220ms에서 260ms로 소폭 증가했다.
- 같은 시간부터 중복 주문 비율이 0에서 0.3%로 증가했다.
- 13:55에 재고 서비스 SDK가 업데이트되었고 기본 재시도 횟수가 0회에서 2회로 바뀌었다.
- 전체 요청량과 사용자 입력 크기에는 큰 변화가 없었다.
평균이 거의 그대로인데 p99만 증가했다는 사실은 모든 요청의 알고리즘이 균일하게 느려졌다는 가설보다 일부 요청이 대기나 재시도를 겪는 가설을 우선하게 한다. 배포 시점의 상관관계는 강한 단서지만 아직 인과는 아니다.
2단계: 요청 시간 예산을 펼친다
분산 트레이스에서 정상 요청은 재고 확인 80ms, DB 저장 40ms, 나머지 100ms였다. 느린 요청은 재고 확인 span이 세 번 연속 나타났고 각각 약 2초 뒤 타임아웃됐다. 주문 API의 전체 타임아웃은 7초였지만 프록시는 8초, 클라이언트는 5초였다.
여기서 세 가지 계약 충돌이 드러난다.
- 하위 호출의 시도별 시간과 재시도 횟수가 상위 요청의 7초 예산을 거의 모두 소비한다.
- 클라이언트는 서버보다 먼저 5초에 포기하지만 서버는 이후에도 주문을 저장할 수 있다.
- 사용자는 실패 화면을 보고 다시 누르지만 두 요청을 같은 의도로 연결할 멱등성 키가 없다.
두 번째 클릭은 중복 주문을 촉발한 직접 계기다. 그러나 같은 사용자 의도를 별개의 주문으로 받아들인 시스템적 원인은 완료 여부를 확정할 수 없는 실패 계약과, 같은 의도를 식별하지 못하는 데이터 계약이다. 계기와 재발 가능한 구조를 구분해야 사용자 행동만 제한하는 처방에 머물지 않는다.
3단계: 재고 서비스가 느려진 이유를 분리한다
재고 서비스의 CPU는 낮았지만 DB 연결 풀의 대기 시간이 급증했다. 조사 결과 SDK 배포 뒤 재시도 트래픽이 원래 호출량의 2.4배까지 늘었고, 각 재시도가 새 DB 연결 요청을 만들었다. 연결 풀은 100개로 고정되어 있었고 대기열에는 상한이 없었다.
일부 DB 지연
→ 재고 호출 타임아웃
→ 동시 재시도 증가
→ 연결 풀 대기 증가
→ 더 많은 호출 타임아웃
→ 더 많은 재시도초기 지연의 원인은 느린 인덱스 갱신 작업이었지만, 6.5초의 꼬리 지연을 만든 지배 요인은 재시도와 무제한 대기열의 양의 피드백이었다. 인덱스 작업만 중단하면 당장 회복할 수 있으나 같은 조건은 다시 발생한다. 재시도만 제거하면 일시 실패 복원 능력을 잃는다. 두 층의 원인과 해결을 분리해야 한다.
4단계: 불변식과 완화책을 우선순위로 정한다
장애 중에는 원인 규명보다 사용자 피해를 멈추는 일이 먼저다. 팀은 변경 범위를 최소화한 긴급 완화부터 수행한다.
- 새 SDK의 재시도를 기능 플래그로 비활성화해 부하 증폭을 멈춘다.
- 초기 지연을 만든 인덱스 작업을 중단하거나 운영 트래픽과 자원을 격리한다.
- 연결 풀 앞에서 유입을 제한하고 포화된 요청을 빠르게 실패시켜 대기열 증가를 멈춘다.
서비스가 안정된 뒤에는 재발 가능한 구조를 영구적으로 바꾼다.
- 하나의 주문 의도에 대해 클라이언트가 키를 한 번 생성하고 타임아웃·재시도·더블 클릭에서 같은 키를 재사용하도록 한다. 서버는 멱등성 키, 요청 지문(request fingerprint), 주문 결과를 주문 생성과 원자적으로 저장하고 같은 요청에는 이전 결과를 반환한다. 같은 키로 다른 주문 내용이 오면 충돌로 거부하고 보존 기간과 정리 정책을 정한다.
- 클라이언트 전체 예산에서 프록시·서버·하위 호출로 내려갈수록 남은 시간 예산이 감소하도록 마감 시각(deadline)을 전파한다. 각 계층은 상위 요청이 취소된 뒤 불필요한 작업을 계속할 수 있는 조건도 명시한다.
- 연결 풀 대기열에 상한을 두고 포화 시 빠르게 실패시켜 시스템 전체를 보호한다.
- 재시도 가능한 오류만 제한된 시도·시간 예산에서 지수 백오프·지터와 함께 재시도한다.
- 인덱스 작업이 운영 트래픽과 자원을 경쟁하지 않도록 실행 계획, 실행 시간, 배포 정책을 바꾼다.
각 변경은 비용이 있다. 빠른 실패는 일부 요청의 즉시 오류를 늘리고, 멱등성 레코드는 저장 공간과 정리 정책을 요구한다. 짧은 타임아웃은 느리지만 성공할 요청을 포기할 수 있다. 대기열 상한은 과부하를 숨기지 않고 외부에 드러낸다. 그러나 “주문 중복 금지”라는 정확성 불변식과 “전체 시스템의 연쇄 포화 방지”라는 자원 안전 목표에 비추면 받아들일 비용을 설명할 수 있다.
5단계: 국소 지표가 아니라 종단 결과를 검증한다
완료 조건을 “재고 서비스 CPU 정상화”로 두면 부족하다. 다음을 함께 검증한다.
- 같은 멱등성 키의 동시 요청이 주문 하나만 만드는가?
- 응답 유실 후 재시도해도 이전 결과를 안전하게 돌려주는가?
- 하위 서비스가 느려질 때 총 시도 수와 연결 대기열이 설정한 상한 안에 있는가?
- p50뿐 아니라 p95·p99와 오류율이 부하 단계별로 어떻게 변하는가?
- 빠른 실패가 시작되는 포화점과 회복 후 대기열이 줄어드는 시간이 예상과 맞는가?
- 기능 플래그와 롤백이 실제 장애 경로에서도 동작하는가?
이 검증에는 알고리즘 한 과목이나 특정 APM 제품만으로 충분하지 않다. 시간 복잡도, 큐잉, 네트워크 타임아웃, 데이터베이스 원자성, 분산 실패, 신뢰성 설계가 하나의 사용자 결과로 조립된다.
설계·디버깅·기술 선택은 같은 활동이다
세 업무는 별개처럼 보이지만 모두 모델과 증거를 이용해 선택의 결과를 예측하는 일이다.
설계: 트레이드오프를 실패 전에 드러낸다
설계 단계에서는 아직 운영 지표가 없으므로 워크로드와 실패 가정을 명시한다.
- 최대 입력과 정상·피크 요청량은 얼마인가?
- 읽기·쓰기·갱신·순회의 비율은 어떠한가?
- p99와 처리량 중 어느 제약을 먼저 지켜야 하는가?
- 일부 구성 요소의 지연·중단·응답 유실을 어떻게 다룰 것인가?
- 강하게 지켜야 할 불변식과 지연시킬 수 있는 결과는 무엇인가?
이 답으로 자료구조, 동시성 모델, 데이터 저장소, 전달 보장을 비교한다. 설계 문서는 선택한 제품의 장점 목록이 아니라 가정과 비용 모델을 기록해야 한다. 그래야 운영 결과가 다를 때 어느 가정을 수정할지 알 수 있다.
디버깅: 관찰에서 경쟁 가설을 줄인다
Google SRE의 문제 해결 모델은 관찰과 시스템 지식을 바탕으로 가능한 원인을 세우고, 확인·반증할 증거를 반복해서 찾는 가설-연역적(hypothetico-deductive) 과정으로 설명된다. 핵심은 첫 설명에 애착을 갖지 않는 것이다.
효과적인 디버깅 기록에는 다음이 남는다.
- 기대와 실제의 차이
- 영향 범위와 시작 시각
- 가능한 원인과 각 원인이 예측하는 관찰
- 수행한 검사와 기각된 가설
- 즉시 완화와 근본 변경의 구분
- 변경이 만드는 새 비용과 회귀 검증
이 구조가 있으면 “재시작하니 해결됨”을 완료로 착각하지 않는다. 재시작은 큐와 캐시·힙·연결 상태를 동시에 초기화하므로 여러 가설을 한꺼번에 바꾸는 처치다. 서비스는 회복시킬 수 있지만 어느 상태가 문제였는지는 별도 증거가 필요하다.
기술 선택: 제품명이 아니라 메커니즘을 비교한다
기술 선택에서 “대규모 회사가 쓴다”, “벤치마크가 빠르다”, “관리형이라 운영이 없다”는 정보는 후보를 찾는 데 도움을 주지만 결정 근거로 부족하다. 다음 순서로 비교한다.
| 단계 | 질문 |
|---|---|
| 문제 | 현재 병목이나 불변식은 무엇인가? |
| 메커니즘 | 후보는 어떤 자료구조·실행·복제 모델로 문제를 푸는가? |
| 적합성 | 우리 데이터 분포·연산 비율·실패 가정과 맞는가? |
| 비용 | 지연·공간·정합성·운영 복잡성 중 무엇을 더 지불하는가? |
| 검증 | 대표 워크로드와 실패 주입으로 어떤 예측을 확인할 것인가? |
| 탈출 | 부적합할 때 되돌리거나 병행할 수 있는가? |
예를 들어 캐시 도입은 “DB가 느리다”에 대한 자동 답이 아니다. 반복 조회가 많고 약간 오래된 값을 허용하며 적중률이 충분할 때 유리하다. 무효화가 어렵고 키 공간이 크며 대부분 한 번만 읽는다면 캐시는 데이터 중복과 정합성 문제만 늘릴 수 있다. 캐시 제품보다 워크로드의 지역성(locality)과 허용할 일관성이 먼저다.
실무에서 자주 빠지는 함정
도구부터 순회한다
CPU가 의심되면 프로파일러, 네트워크가 의심되면 패킷 캡처처럼 도구는 가설에 따라 고른다. 가설 없이 익숙한 대시보드를 넘기면 우연한 상관관계를 원인으로 오해하기 쉽다. 먼저 “이 가설이 맞다면 어떤 지표가 어떤 방향으로 변해야 하는가”를 적는다.
마지막 변경만 원인으로 확정한다
변경 시점과 장애 시점의 일치는 강한 단서다. 그러나 새 배포가 트래픽 증가나 백그라운드 작업과 동시에 일어났을 수 있다. 롤백으로 회복되면 인과 증거는 강해지지만, 구성·캐시·부하까지 함께 바뀌었는지 살핀다. 변경을 의심하되 그것만 볼 이유는 없다.
한 계층의 정상 지표로 전체 계층을 제외한다
평균 CPU 40%, 평균 DB 지연 20ms 같은 집계는 국소 포화와 꼬리 분포를 숨긴다. 코어별, 샤드별, 엔드포인트별, 백분위별로 나누고 대기와 실행을 구분한다. 평균은 용량 계획에 유용할 수 있지만 사용자 한 명이 겪은 긴 요청 경로를 대표하지 않는다.
국소 최적화로 병목을 다음 계층에 밀어낸다
애플리케이션 워커를 두 배로 늘리면 요청 큐는 짧아질 수 있지만 DB 연결 경쟁과 락 대기는 길어질 수 있다. 압축률을 높이면 네트워크 바이트는 줄지만 CPU 지연이 늘 수 있다. 배치 크기를 키우면 처리량은 오르지만 개별 이벤트의 지연과 실패 시 재처리량이 커진다. 변경 전후에 종단 지표와 다음 제한 자원을 함께 본다.
근본 원인을 하나의 사람이나 한 줄의 코드로 축소한다
직접 계기는 한 줄의 정규식이나 잘못된 설정일 수 있다. 그러나 전역 배포, 자원 격리 부재, 성능 테스트 누락, 롤백 지연이 없었다면 같은 코드의 영향은 작았을 수 있다. 재발 방지는 “누가 실수했는가”보다 “어떤 방어 계층이 없거나 함께 실패했는가”를 묻는다.
커리큘럼을 하나의 판단 지도처럼 사용하기
이후 Part들은 서로 다른 질문을 담당한다.
| Part | 핵심 판단 | 현장에서 나타나는 질문 |
|---|---|---|
| Part 1 이론 컴퓨터 과학 | 무엇을 표현·계산할 수 있고 비용은 어떻게 증가하는가? | 이 입력에서 알고리즘이 감당 가능한가? 도구가 원리적으로 놓치는 것은 무엇인가? |
| Part 2 언어와 컴파일러 | 코드가 어떤 표현과 실행 전략으로 바뀌는가? | 타입·정적 분석은 무엇을 보장하는가? JIT·GC는 지연을 어떻게 만드는가? |
| Part 3 컴퓨터 시스템 | 계산이 CPU·메모리·I/O·네트워크·저장소에서 어떻게 실행되는가? | 시간은 실행과 대기 중 어디에 쓰였는가? 실패 후 어떤 상태가 남는가? |
| Part 4 소프트웨어 공학 | 판단을 팀이 검증하고 안전하게 변경하는 구조는 무엇인가? | 요구사항과 품질 목표를 어떻게 계약·테스트·배포 정책으로 만들 것인가? |
새 문제를 만나면 과목명을 먼저 고르지 않는다. 비용 모델, 불변식, 계약, 증거 중 비어 있는 칸을 찾고 해당 질문을 담당하는 챕터로 이동한다. 한 챕터에서 답이 끝나지 않으면 요청 경로의 앞뒤 계층을 연결한다.
더 깊이: 꼬리 지연은 왜 계층을 연결하게 만드는가
평균이 아닌 꼬리 지연(tail latency)은 시스템을 부분의 합으로만 보지 못하게 하는 대표 사례다. 요청이 20개 샤드의 응답을 모두 기다리고 각 샤드가 드물게 느려진다면, 개별 샤드의 느린 사건은 드물어도 요청 전체가 하나 이상의 느린 샤드를 만날 확률은 커진다. 규모가 커질수록 일시적 GC, 스케줄링 지연, 네트워크 재전송, 저장 장치 큐가 사용자 요청을 지배할 수 있다.
따라서 p99 문제를 한 계층의 “평균 최적화”로 풀 수 없다.
- 알고리즘은 최악 입력과 작업량 분산을 본다.
- 런타임은 긴 정지와 할당 폭주를 본다.
- 운영체제는 런 큐·I/O 큐와 자원 포화를 본다.
- 네트워크는 재전송·혼잡·연결 수립 지연을 본다.
- 분산 시스템은 fan-out, 백업 요청, 시간 예산 전파를 본다.
- 신뢰성 설계는 사용자 관점의 SLI와 부하 차단을 본다.
Dean과 Barroso의 The Tail at Scale은 대규모 온라인 서비스가 예측하기 어려운 개별 구성 요소로부터 예측 가능한 전체 응답성을 만들어야 하는 문제를 설명한다. 이 관점은 “어느 서버가 느린가?”를 넘어 “느린 구성 요소를 만날 확률과 영향이 요청 구조에서 어떻게 증폭되는가?”를 묻게 한다.
정리
- 운영 요청은 여러 시스템 계층을 통과하므로 장애와 성능 문제도 과목 하나의 경계에 머물지 않는다.
- 비용 모델은 증가하는 축과 지배 비용을, 불변식은 반드시 지킬 상태를, 계약은 계층별 책임을, 증거는 경쟁 가설을 구분한다.
- 설계·디버깅·기술 선택은 모두 모델로 결과를 예측하고 관찰로 수정하는 같은 판단 활동이다.
- 평균이나 한 계층의 정상 지표는 국소 포화, 대기, 꼬리 지연을 숨길 수 있으므로 요청 경로와 분포를 함께 본다.
- 커리큘럼은 과목 목록이 아니라 현재 문제에서 비어 있는 질문을 찾아 이동하는 판단 지도로 사용한다.
확인 문제
1. 이미지 처리 API의 애플리케이션 CPU가 35%인데 p99가 급증했다. 팀원은 CPU가 남으므로 서버를 늘릴 필요도 프로파일링할 필요도 없다고 말한다. 가능한 가설과 구분할 증거를 세 가지 이상 제시하라.
정답과 해설
전체 평균 35%는 코어별 포화, 일부 인스턴스의 핫스폿, 실행이 아닌 대기를 숨길 수 있다. 첫째, 단일 이벤트 루프나 특정 코어가 포화됐는지 코어·인스턴스별 CPU와 on-CPU 프로파일을 본다. 둘째, 작업 큐나 스레드 풀이 포화됐는지 active, queue length, wait time을 본다. 셋째, 원격 객체 저장소나 파일 I/O 대기인지 요청 span과 off-CPU 시간을 본다. 넷째, 큰 이미지에서 알고리즘 비용이나 메모리 대역폭이 급증하는지 입력 크기별 지연과 CPU 하드웨어/프로파일 지표를 비교한다. 다섯째, GC나 메모리 압력이 일부 요청을 멈추는지 정지 이벤트와 p99 시각을 맞춘다. 서버 증설은 원인이 용량일 때 완화할 수 있지만 공유 저장소 포화나 무제한 큐는 악화시킬 수도 있다.
2. 캐시 도입으로 DB 조회량은 70% 감소했지만 사용자에게 오래된 권한이 노출되는 사고가 발생했다. 이 변경을 비용 모델과 불변식으로 재평가하라.
정답과 해설
DB 조회량 감소는 자원 비용의 한 축에서 성공한 결과다. 그러나 권한 변경이 즉시 반영되어야 한다는 보안·정확성 불변식이 있다면 오래된 값 허용은 받아들일 수 없는 비용이다. 권한 데이터와 일반 콘텐츠에 같은 캐시 정책을 적용한 것이 문제다. 권한 검사의 원본 조회를 유지하거나, 버전·짧은 TTL·명시적 무효화·세션 철회 목록처럼 요구되는 일관성을 만족하는 메커니즘을 비교한다. 각 대안은 DB 부하, 무효화 실패, 추가 저장 공간, 가용성 비용을 가진다. 캐시 적중률만이 아니라 권한 변경 후 반영 지연과 실패 시 안전한 기본값을 검증해야 한다.
3. 주문 사례에서 SDK 재시도를 끄자 지연이 정상화되었다. 이것으로 근본 원인이 증명되었다고 볼 수 있는가? 즉시 완화와 재발 방지를 구분하라.
정답과 해설
재시도 비활성화와 회복은 재시도 증폭 가설을 강하게 지지하지만 초기 지연의 원인과 중복 주문의 구조까지 모두 설명하지는 않는다. 즉시 완화는 재시도를 끄고 과부하를 낮추며 필요하면 트래픽을 제한하는 것이다. 재발 방지는 초기 DB 지연의 원인, 호출별 시간 예산, 재시도 가능한 오류와 예산, 연결 풀 대기열 상한을 정리해야 한다. 별도로 클라이언트 타임아웃 뒤 서버가 성공할 수 있는 실패 계약과 멱등성 부재를 수정해 중복 주문 불변식을 지킨다. 부하·응답 유실·동시 재시도를 주입한 종단 테스트로 이 방어들이 함께 동작하는지 검증한다.
4. 팀이 메시지 큐를 도입하면 주문 저장과 알림 발송이 완전히 분리된다고 주장한다. 어떤 계약과 실패 구간을 확인해야 하는가?
정답과 해설
큐는 실행 시간과 가용성의 결합을 낮출 수 있지만 상태의 의미까지 자동으로 분리하지 않는다. 주문 DB 커밋 뒤 메시지 발행 전에 프로세스가 죽는 구간에서는 주문은 있으나 알림 이벤트가 없을 수 있고, 발행 뒤 ACK 유실에서는 중복 이벤트가 생길 수 있다. DB 변경과 이벤트 기록을 같은 로컬 트랜잭션에 넣는 아웃박스 같은 방법, 소비자의 멱등 처리, 재시도·dead-letter·순서 계약을 검토한다. 큐 적체 시 저장 용량과 복구 처리량, 오래된 알림의 가치도 자원·제품 계약으로 정해야 한다.
참고 자료
- Chris Jones, Effective Troubleshooting — 관찰과 시스템 지식에서 경쟁 가설을 만들고 검사로 좁히는 장애 진단 절차, 흔한 추론 오류를 참고한다.
- Jeffrey Dean, Luiz André Barroso, The Tail at Scale (2013) — fan-out과 규모가 개별 구성 요소의 지연 변동을 종단 꼬리 지연으로 증폭하는 구조와 완화 전략을 확인한다.
- Rob Ewaschuk, Monitoring Distributed Systems — 사용자 관점의 black-box 관찰과 내부 상태를 보는 white-box 관찰, 지연·트래픽·오류·포화 신호의 역할을 참고한다.
- Brendan Gregg, The USE Method — 각 자원에서 사용률(utilization), 포화(saturation), 오류(errors)를 체계적으로 확인해 도구 중심 조사에서 빠져나오는 방법을 참고한다.
- Jerome H. Saltzer, David P. Reed, David D. Clark, End-to-End Arguments in System Design (1984) — 구성 요소의 개별 보장과 종단 애플리케이션 불변식이 자동으로 같아지지 않는 이유를 확인한다.
- Node.js 24, File system — 비동기 파일 API가 이벤트 루프 밖의 libuv 스레드 풀을 사용한다는 구현 경계를 확인한다.
- Stripe, Idempotent requests — 같은 키의 재요청에 저장된 결과를 돌려주는 실전 멱등성 계약의 한 예를 확인한다.