Skip to content

0.1 추상화가 무너지는 순간, CS가 필요해진다

추상화는 내부를 몰라도 되는 영구 보증이 아니라, 정해진 조건에서 세부를 미뤄 둘 수 있게 하는 계약이다. 이 문서에서는 계약을 의미·자원·실패라는 세 차원으로 나누어 언제 API만 믿어도 되고 언제 내부 동작 모델로 내려가야 하는지 판단한다.

학습 목표

  • 추상화가 복잡성을 제거하는 대신 경계 뒤로 옮긴다는 사실을 설명한다.
  • 의미·자원·실패 계약을 구분해 API가 보장하는 것과 보장하지 않는 것을 판별한다.
  • 규모·동시성·분산·실패·적대적 입력에서 추상화의 경계가 드러나는 원인을 분석한다.
  • 현상에 필요한 내부 깊이를 정하고 불필요한 조기 최적화를 피한다.

배경: 동작하던 코드가 갑자기 낯설어지는 이유

다음 코드는 읽기 어렵지 않다.

ts
const uniqueUsers = [...new Set(events.map((event) => event.userId))];

입력에서 사용자 ID를 꺼내 중복을 제거한다. 이 한 줄을 사용하기 위해 Set의 내부 표현이나 V8의 객체 배치를 알 필요는 없다. map, Set, 전개 구문이 제공하는 추상화 덕분이다. 그러나 입력이 수백 건에서 수천만 건으로 늘고 프로세스가 메모리 제한에 가까워지면 질문이 달라진다.

  • events.map이 만드는 중간 배열과 전개 결과는 동시에 메모리에 존재하는가?
  • Set의 키 하나는 원래 ID 크기 외에 어떤 메타데이터를 요구하는가?
  • 대상 런타임의 Set은 어떤 자료구조를 사용하며, 용량 증가와 재배치가 처리 시간과 꼬리 지연에 어떤 영향을 주는가?
  • 전체 결과가 꼭 필요한가, 스트리밍하거나 데이터베이스로 연산을 옮길 수 있는가?

ECMAScript는 Set의 관찰 가능한 의미와 원소 수에 대해 평균적으로 선형보다 나은 접근 성능을 요구하지만, 해시 테이블 같은 특정 내부 구조를 강제하지 않는다. 따라서 해시 충돌이나 리사이징을 원인으로 조사하려면 대상 Node.js·브라우저와 엔진 버전을 고정하고 실제 구현이나 프로파일을 확인해야 한다.

코드의 의미는 변하지 않았지만 자원 조건이 변했다. 추상화는 틀리지 않았다. 단지 “결과가 무엇인가”를 잘 보장했을 뿐 “어떤 입력에서도 주어진 메모리와 시간 안에 끝나는가”까지 약속하지 않았다.

추상화(abstraction)는 복잡한 대상에서 현재 목적에 필요한 성질만 남기는 모델이다. 좋은 추상화는 사용자가 매번 내부를 추론하지 않게 한다. 파일 API를 호출할 때 디스크 블록 배치와 인터럽트를 직접 다루지 않고, TCP 소켓을 사용할 때 손실된 패킷의 재전송을 직접 구현하지 않는 이유다. 이 압축 덕분에 오늘날의 소프트웨어를 만들 수 있다.

그러나 압축된 정보가 사라진 것은 아니다. 성능 한계, 장애, 보안, 호환성처럼 생략한 성질이 결정을 좌우하는 순간에는 다시 펼쳐 보아야 한다. “모든 추상화는 샌다”는 말의 실용적 의미는 추상화가 쓸모없다는 것이 아니라 추상화가 생략한 정보가 언제 다시 중요해지는지 알아야 한다는 것이다.

추상화를 검토하는 세 가지 계약 차원

아래 분류는 표준화된 계약 체계라기보다 이 커리큘럼에서 실무 판단에 사용하는 분석 틀이다. API 문서를 읽을 때 반환 타입과 예외 목록만 보면 계약의 일부만 본 것이다. 의미, 자원, 실패라는 세 차원을 함께 보면 명시된 보장뿐 아니라 비보장과 애플리케이션이 채워야 할 책임까지 드러난다.

의미 계약: 무엇이 참이어야 하는가

의미 계약(semantic contract)은 입력과 출력, 상태 변화, 순서, 원자성처럼 관찰 가능한 동작을 정한다. 예를 들어 언어의 Map은 같은 키로 조회하면 대응하는 값을 돌려준다는 의미를 제공한다. 데이터베이스 트랜잭션은 선택한 격리 수준과 원자성에 관한 약속을 제공한다. 파일 시스템의 rename이 어떤 조건에서 원자적인지도 의미 계약이다.

의미 계약을 오해하면 코드는 빠르게 동작하면서 틀린 결과를 낸다. 흔한 사례는 다음과 같다.

  • 순회 순서를 보장하지 않는 컬렉션에서 현재 구현의 순서에 의존한다.
  • 메시지 브로커의 “적어도 한 번(at-least-once) 전달”을 “정확히 한 번 처리”로 읽는다.
  • 트랜잭션이 성공했다는 사실을 모든 복제본에서 즉시 같은 값을 읽을 수 있다는 뜻으로 해석한다.
  • TCP가 바이트 스트림의 순서를 보장한다는 사실을 애플리케이션 메시지 경계도 보존한다는 뜻으로 해석한다.

이 오류를 막으려면 “지금 관찰되는가?”와 “계약이 보장하는가?”를 분리해야 한다. 테스트에서 계속 같은 순서가 나와도 문서나 표준이 순서를 보장하지 않으면 구현 교체와 버전 변경에서 깨질 수 있다.

자원 계약: 얼마의 비용으로 가능한가

자원 계약(resource contract)은 시간, 공간, 데이터 이동, 동시 실행 용량에 관한 성질이다. 많은 API는 자원 비용을 엄격히 보장하지 않는다. 표준 라이브러리 문서가 평균 복잡도를 알려 주더라도 상수 인자, 메모리 배치, 런타임 최적화, 최악 입력까지 모두 고정하지는 않는다.

다음 코드는 모두 의미상 파일 내용을 돌려줄 수 있다.

ts
const contents = await readFile(path, 'utf8');

하지만 파일이 10KiB인지 100GiB인지에 따라 같은 추상화의 적합성이 달라진다. 전체 파일을 메모리에 올리는 API의 의미 계약은 만족해도 프로세스 메모리 한도라는 시스템 계약은 만족하지 못할 수 있다. 이때 스트림은 “더 현대적인 API”라서가 아니라 데이터 전체 대신 제한된 청크만 메모리에 두는 자원 모델 때문에 선택한다.

성능 문제에서 자주 생기는 실수는 API 이름을 비용 모델로 사용하는 것이다. “비동기라서 빠르다”, “해시 맵이라 O(1)이다”, “캐시라 싸다”는 문장은 불완전하다. 비동기는 대기 중 스레드를 점유하지 않는 실행 구조일 수 있지만 단일 작업의 지연을 줄인다는 보장은 없다. 해시 조회는 기대 O(1)이지만 키 해싱, 충돌, 리사이징, 캐시 미스의 비용이 남는다. 원격 캐시는 원본 저장소보다 빠를 수 있지만 네트워크 왕복과 직렬화는 지역 메모리 접근보다 비싸다.

실패 계약: 무엇이 깨질 수 있으며 무엇이 남는가

실패 계약(failure contract)은 연산이 중간에 멈추거나 일부 구성 요소가 응답하지 않을 때 관찰 가능한 상태를 정한다. 단일 프로세스에서는 예외 하나로 보이던 실패가 분산 시스템에서는 여러 상태로 갈라진다.

클라이언트가 결제 요청을 전송한 뒤 타임아웃을 받았다고 하자. 가능한 상태는 적어도 세 가지다.

  1. 서버가 요청을 받지 못했다.
  2. 서버가 받았지만 처리 전에 실패했다.
  3. 서버가 처리를 완료했지만 응답이 유실되었다.

클라이언트가 관찰한 “타임아웃”만으로 어느 상태인지 알 수 없다. 무조건 재시도하면 3번에서 중복 결제가 생길 수 있고, 재시도하지 않으면 1번과 2번에서 요청이 사라진다. 여기서 필요한 것은 HTTP 클라이언트 옵션 지식만이 아니다. 네트워크의 불확실성, 멱등성, 트랜잭션, 중복 제거 키가 조립되어야 한다.

하위 계층의 신뢰성이 상위 계층의 정확성을 자동으로 완성하지도 않는다. Saltzer, Reed, Clark의 종단 간 논증(end-to-end argument)은 통신 하위 계층의 오류 검사와 재전송이 유용하더라도, 애플리케이션이 요구하는 완전한 정확성은 결국 종단에서 확인해야 하는 경우를 설명한다. TCP 체크섬과 재전송이 있어도 “수신한 파일이 사용자가 보낸 바로 그 파일인가”, “결제가 정확히 한 번 반영됐는가”는 애플리케이션의 상태와 의미를 아는 종단이 검증해야 한다.

경계가 드러나는 다섯 가지 변화

추상화의 내부를 항상 살필 필요는 없다. 다음 변화가 관찰되면 생략된 성질이 결정에 영향을 주기 시작했는지 확인한다.

변화감춰져 있던 질문연결되는 CS 모델
규모 증가입력·상태·사용자 수에 따라 비용은 어떻게 증가하는가?복잡도, 자료구조, 캐시, 저장 구조
동시성 증가어떤 상태를 공유하며 순서와 가시성을 어떻게 보장하는가?스케줄링, 동기화, 메모리 모델, 트랜잭션
분산 경계 추가지연·손실·중복·부분 실패에서 무엇을 확정할 수 있는가?네트워크, 합의, 복제, 일관성
실패 발생연산이 중간에 멈췄을 때 어떤 상태가 남는가?원자성, 내구성, 복구, 멱등성
적대적·병적 입력평균 가정이 무너질 때 최악 비용은 무엇인가?최악 복잡도, 파싱, 보안, 자원 제한

사례 1: 정규식 한 줄이 전 세계 CPU를 소진하다

2019년 7월 Cloudflare는 웹 애플리케이션 방화벽(WAF)의 관리형 규칙 하나를 배포한 뒤 전 세계 네트워크에서 HTTP/HTTPS 트래픽을 처리하는 CPU 코어가 소진되는 장애를 겪었다. 공식 사고 보고서에 따르면 문제의 정규식은 특정 입력에서 과도한 백트래킹을 일으켰고, 장애는 27분 동안 이어졌다.

표면에서 정규식은 “문자열이 패턴과 일치하는가”를 묻는 선언적 추상화다. 의미 계약만 보면 규칙은 맞거나 틀리다. 그러나 사용한 엔진이 후보 경로를 어떤 순서로 탐색하는지, 중첩된 반복과 겹치는 패턴이 탐색 공간을 어떻게 키우는지라는 계산 비용은 패턴 뒤에 남아 있다.

이 사건의 교훈을 “정규식을 쓰지 말자”로 줄이면 재사용할 수 없다. 실제 판단 모델은 다음과 같다.

  • 외부 입력이 계산량을 통제할 수 있는가?
  • 엔진의 실행 전략이 최악 입력에서 비선형·지수적 경로를 만들 수 있는가?
  • 배포 전 성능 테스트가 매칭 성공 사례뿐 아니라 거의 일치하다 끝에서 실패하는 입력을 포함하는가?
  • 한 규칙의 CPU 소비를 격리하거나 즉시 비활성화할 수 있는가?

형식 언어와 오토마타는 정규식 문법을 시험에 쓰기 위한 지식이 아니다. 어떤 엔진이 선형 시간에 처리할 수 있는 표현과 백트래킹 탐색이 폭발할 수 있는 구조를 구분하는 운영 지식이 된다. 이 주제는 챕터 2 계산 이론에서 모델을 세우고, 챕터 5 컴파일러와 인터프리터에서 렉싱·파싱 구현으로 연결한다.

사례 2: 객체를 지웠는데 RSS가 줄지 않는다

애플리케이션에서 큰 객체 그래프의 참조를 제거하고 GC가 실행되었는데 운영 대시보드의 RSS(resident set size)가 그대로일 수 있다. “GC가 메모리를 회수하지 못했다”는 결론은 가능하지만 유일한 설명은 아니다. GC와 수동 해제는 시작점이 다르므로 먼저 두 경로를 분리한다.

text
GC 경로
도달 가능한 객체 → 마지막 참조 제거 → 도달 불가능 → GC 회수

                                  런타임 힙의 재사용 가능 공간

네이티브 경로
할당된 블록 → free → 네이티브 할당자의 재사용 가능 공간

공통 하위 경계
런타임/할당자의 재사용 가능 영역
      ├─ 매핑과 resident page를 유지
      └─ OS별 페이지 반환 또는 매핑 해제

가상 주소 매핑·커밋·resident page·RSS는 서로 다른 상태와 계측값

언어 런타임은 회수한 공간을 다음 할당에 재사용하려고 힙 영역을 보유할 수 있다. free도 보통 블록을 먼저 네이티브 할당자에 돌려주며, 단편화와 반환 정책 때문에 운영체제에 즉시 페이지가 반환된다는 뜻은 아니다. 가상 주소 매핑의 제거와 물리 페이지의 resident 상태 변화도 같은 사건이 아니다. 따라서 힙 프로파일, 할당자 통계, 메모리 매핑, RSS는 같은 “메모리”라는 단어를 쓰지만 서로 다른 상태를 관찰한다.

여기서 CS 지식은 용어를 늘리는 대신 검사를 분리한다. 살아 있는 객체가 계속 늘면 보유 경로를 찾고, 객체는 줄지만 런타임 힙이 유지되면 GC와 힙 정책을 보고, 힙의 빈 공간은 충분한데 RSS가 높으면 할당자와 페이지 반환을 살핀다. 같은 그래프를 보고 GC 옵션부터 무작정 바꾸는 대신 각 계층의 가설을 다른 증거로 검증한다. 상세한 경계는 챕터 6 런타임과 메모리 및 8.2 가상 메모리에서 다룬다.

사례 3: 재시도가 장애를 회복시키지 않고 증폭한다

원격 호출이 간헐적으로 실패하면 타임아웃과 재시도는 합리적인 복원 전략이다. 그러나 여러 계층이 각각 세 번씩 재시도하면 한 사용자 요청이 하위 서비스에 보내는 호출 수가 곱으로 늘 수 있다. 과부하 때문에 응답이 느려진 상황에서 재시도 트래픽이 더해지면 큐가 길어지고 타임아웃이 늘며 다시 더 많은 재시도가 발생한다.

추상화 수준에서는 retry: 3이라는 옵션 하나다. 내부 모델에서는 다음 요소가 결정을 바꾼다.

  • 실패가 일시적인가, 과부하처럼 재시도로 악화되는가?
  • 타임아웃은 평상시 평균이 아니라 꼬리 지연 분포를 반영하는가?
  • 호출 스택 중 어느 한 계층만 재시도를 소유하는가?
  • 지수 백오프와 지터가 동시 재시도를 분산하는가?
  • 연산이 멱등적인가, 중복 방지 키나 조건부 쓰기가 있는가?
  • 전체 요청의 시간 예산 안에서 각 시도에 얼마를 배분하는가?

재시도는 네트워크 실패를 “없애는” 추상화가 아니다. 실패의 일부를 추가 부하와 지연으로 교환하는 정책이다. 정상 상태에서는 성공률을 높이지만 포화 구간에서는 시스템을 무너뜨릴 수 있다. 챕터 9 네트워크는 지연·손실과 연결 수명을, 챕터 10 분산 시스템은 부분 실패와 중복·일관성을 다룬다.

실무 관점: 어디까지 내려가야 하는가

“내부를 알아야 한다”는 말은 끝이 없다. JavaScript 성능 문제를 이해하려면 V8을 보고, V8을 이해하려면 컴파일러와 CPU를 보고, CPU를 이해하려면 마이크로아키텍처와 회로까지 내려갈 수 있다. 모든 층을 같은 깊이로 공부하는 것은 불가능하고 필요하지도 않다.

필요한 깊이는 현재 결정이 요구하는 예측 해상도로 정한다.

1. 먼저 기대와 관찰의 차이를 고정한다

“느리다”, “메모리가 샌다”는 아직 문제가 아니다. 어떤 입력과 환경에서 어떤 지표가 기준에서 얼마나 벗어났는지 적는다.

  • 배포 전 p99 180ms가 배포 후 1.2s가 되었다.
  • 요청량은 일정한데 컨테이너 RSS가 시간당 300MiB씩 증가한다.
  • 네트워크 오류율이 1%일 때 하위 서비스 요청량이 평소의 네 배로 뛴다.

현상을 좁혀야 어떤 계약을 조사할지도 좁혀진다.

2. 깨졌다고 의심하는 계약을 분류한다

  • 결과나 순서가 틀렸다면 의미 계약을 본다.
  • 지연·CPU·메모리·처리량이 문제라면 자원 계약을 본다.
  • 타임아웃·부분 완료·중복·복구가 문제라면 실패 계약을 본다.

실제 사고는 둘 이상을 함께 포함할 수 있다. 예를 들어 메모리 포화라는 자원 문제가 프로세스 종료를 만들고, 요청의 부분 완료라는 실패 문제로 이어진다. 분류의 목적은 상자를 하나 고르는 것이 아니라 빠진 질문을 찾는 것이다.

3. 한 단계 아래의 실행 단위를 그린다

필요한 것은 전체 소스 코드가 아니라 현상을 설명할 다음 실행 단위다.

표면 현상한 단계 아래에서 그릴 것
해시 조회가 느림해시 계산 → 버킷 탐색 → 키 비교 → 리사이징
비동기 API가 멈춤이벤트 루프 → 작업 큐 → 스레드 풀/커널 I/O → 완료 콜백
DB 커밋 지연락 대기 → 로그 기록 → 플러시/그룹 커밋 → 복제 확인
원격 호출 타임아웃DNS → 연결 → TLS → 서버 큐 → 처리 → 응답 전송

이 흐름에서 관찰 가능한 경계를 표시한다. CPU 프로파일, off-CPU 시간, GC 로그, 패킷 캡처, 실행 계획처럼 각 단계에 맞는 증거를 고른다.

4. 결정을 바꾸지 않는 세부에서는 멈춘다

내부를 더 아는 것이 흥미롭더라도 현재 선택을 바꾸지 않으면 조사 범위에서 제외한다. 예를 들어 느린 쿼리의 실행 계획이 전체 테이블 스캔을 보여 주고 적절한 인덱스로 문제가 해결되었다면, 당장 B-tree 페이지 분할 구현까지 내려갈 필요는 없다. 반대로 쓰기 증폭과 인덱스 크기가 다음 병목이라면 저장 구조가 결정에 영향을 주므로 한 단계 더 내려간다.

멈춤 조건은 “원인을 하나 찾았다”가 아니다. 다음 세 질문에 답할 수 있을 때다.

  1. 관찰한 현상을 어떤 메커니즘이 만들었는가?
  2. 제안한 변경이 그 메커니즘을 어떻게 바꾸는가?
  3. 변경이 새로 만드는 비용과 실패 모드는 무엇인가?

추상화를 대하는 두 극단

극단 1: 문서에 없는 것은 신경 쓰지 않는다

명시적 계약만 믿는 태도는 코드의 이식성과 단순성을 지키는 데 유용하다. 그러나 운영 시스템에는 문서에 없는 자원 한도와 사실상의 호환성 계약이 생긴다. Hyrum의 법칙은 API 사용자가 충분히 많아지면 누군가가 모든 관찰 가능한 동작에 의존하게 된다는 경험칙이다. 반환 순서, 오류 문자열, 지연 특성처럼 공식 계약이 아니었던 성질도 실제 소비자에게는 계약이 될 수 있다.

따라서 제공자라면 구현 변경 전 관찰 가능한 행동을 조사하고, 소비자라면 명시되지 않은 행동에 의존했는지 테스트해야 한다. 그렇다고 모든 구현 세부를 영구 보존할 수는 없다. 명시적 계약으로 승격할 행동, 마이그레이션할 의존, 깨뜨릴 수 있는 내부 세부를 구분하는 설계 판단이 필요하다.

극단 2: 추상화를 믿지 않고 항상 밑바닥부터 본다

모든 변경에서 커널과 CPU까지 내려가면 개발 속도와 설명 가능성이 오히려 나빠진다. 추상화의 목적은 검증된 결정을 재사용하는 것이다. 표준 컬렉션, 데이터베이스 트랜잭션, TCP를 매번 재구현하지 않는 이유다.

내부 지식은 추상화를 우회하는 자격증이 아니다. 추상화의 적용 조건을 더 정확히 지키는 도구다. 데이터베이스의 격리 모델을 안다고 애플리케이션에서 임의의 락을 추가하는 것이 아니라, 필요한 불변식을 가장 단순하게 보장하는 격리 수준과 제약 조건을 선택한다. CPU 캐시를 안다고 모든 코드를 비트 연산으로 바꾸는 것이 아니라, 프로파일에서 메모리 접근이 지배적인 경로에만 데이터 배치를 검토한다.

경계 지도 작성하기

새 기술이나 라이브러리를 도입할 때 다음 표를 한 장으로 작성하면 내부를 공부할 우선순위가 드러난다.

항목기록할 질문
의미입력·출력·순서·원자성에 무엇을 보장하는가?
자원시간·메모리·연결·스레드는 무엇에 비례해 증가하는가?
실패타임아웃·취소·부분 실패 뒤에 어떤 상태가 남는가?
경계최대 크기, 포화점, 최악 입력, 지원하지 않는 경우는 무엇인가?
관찰어떤 메트릭·로그·프로파일로 내부 상태를 확인할 수 있는가?
탈출구기능 비활성화, 우회, 롤백, 대체 구현은 가능한가?

이 표는 문서를 베껴 쓰는 체크리스트가 아니다. 우리 시스템의 사용 조건을 추상화의 계약과 대조하는 도구다. “연결 풀 최대 100”이라는 값만 적지 말고, 동시 요청 수와 쿼리 지연이 변할 때 대기열이 어떻게 자랄지 예측한다. “자동 재시도 지원”만 적지 말고, 어느 오류를 몇 번 재시도하며 호출 전체의 시간 예산과 멱등성을 어떻게 지킬지 적는다.

더 깊이: 추상화 누수는 설계 실패인가

추상화 누수라는 표현은 종종 구현 세부가 노출된 나쁜 API를 뜻한다. 그런 경우도 있지만 모든 누수가 제거 가능한 것은 아니다. 계산에는 시간과 공간이 들고, 통신에는 지연과 실패가 있으며, 저장 장치에는 유한한 내구성과 대역폭이 있다. 상위 계층이 이 물리적 제약을 영원히 감출 수는 없다.

좋은 추상화의 기준은 내부가 절대 보이지 않는가가 아니다.

  • 정상 사용 구간에서 소비자가 적은 개념으로 올바르게 사용할 수 있는가?
  • 의미·자원·실패의 보장과 비보장이 구분되어 있는가?
  • 한계에 가까워질 때 관찰할 신호가 있는가?
  • 더 세밀한 제어가 필요할 때 하위 계층으로 내려갈 통로가 있는가?
  • 구현을 바꿔도 지켜야 할 계약이 테스트 가능하게 정의되어 있는가?

파일을 바이트 스트림으로 보는 추상화는 디스크가 블록으로 동작한다는 사실을 감추지만, fsync와 오류를 통해 내구성 경계를 다룰 수 있게 한다. 데이터베이스는 트랜잭션으로 여러 저장 연산을 묶지만 격리 수준과 충돌 오류로 동시성 비용을 노출한다. 누수 자체보다 위험한 것은 경계가 존재하지 않는다고 믿는 일이다.

정리

  • 추상화는 복잡성을 제거하지 않고 현재 목적에 불필요한 정보를 경계 뒤로 옮긴다.
  • 의미 계약은 결과와 상태를, 자원 계약은 시간·공간·용량을, 실패 계약은 중간 실패 뒤의 상태를 다룬다.
  • 규모, 동시성, 분산, 실패, 적대적 입력이 변하면 평소 감춰진 내부 성질이 설계 결정을 좌우하기 시작한다.
  • CS 지식은 모든 내부를 외우기 위한 것이 아니라 현상을 설명할 만큼 정확한 실행 모델과 검증 방법을 고르는 데 쓰인다.
  • 필요한 깊이는 다음 결정을 바꿀 수 있는가로 정하고, 메커니즘·변경 효과·새 비용을 설명할 수 있을 때 조사를 멈춘다.

확인 문제

1. 메시지 브로커가 “적어도 한 번 전달”을 보장하고 소비자 라이브러리가 자동 재시도를 제공한다. 팀은 이를 근거로 주문 처리가 유실도 중복도 없다고 주장한다. 의미·자원·실패 계약으로 나누어 빠진 질문을 제시하라.

정답과 해설

의미 계약에서는 브로커의 전달과 애플리케이션의 처리 완료가 같은 사건인지, 주문 ID에 유일성 제약이나 멱등 처리 키가 있는지 물어야 한다. 적어도 한 번 전달은 중복 가능성을 명시적으로 허용한다. 자원 계약에서는 실패 시 재시도와 대기 메시지가 처리량·저장 공간·하위 DB 연결을 얼마나 소비하는지 본다. 실패 계약에서는 DB 반영 후 ACK 전에 소비자가 죽는 경우, 처리 도중 타임아웃, 영구 실패 메시지의 격리와 재처리 정책을 확인한다. 브로커의 전달 보장만으로 종단의 정확히 한 번 상태 변경은 완성되지 않는다.

2. 새 버전 배포 후 프로세스 RSS가 증가했지만 힙 프로파일의 살아 있는 객체 크기는 이전과 같다. “메모리 누수가 아니다”와 “GC 문제다” 중 어느 결론이 옳은가? 다음 관찰을 설계하라.

정답과 해설

현재 증거만으로 둘 중 하나를 확정할 수 없다. 힙 프로파일은 런타임이 추적하는 살아 있는 객체를, RSS는 현재 물리 메모리에 상주한 프로세스 페이지를 센다. 네이티브 버퍼, 스레드 스택, JIT 코드, 할당자 단편화, 런타임이 보유한 빈 힙이 차이를 만들 수 있다. 런타임 힙의 committed/used 변화, GC 전후 사용량, 네이티브 할당 통계, 메모리 매핑과 페이지 종류를 시간축으로 비교한다. 부하를 제거한 뒤 재사용되는지, 안정된 상한에 도달하는지도 본다. 각 계층의 관찰이 있어야 누수, 보유 정책, 단편화를 구분할 수 있다.

3. 한 팀이 모든 원격 호출에 재시도 3회를 공통 미들웨어로 적용하려 한다. 이 결정을 승인하기 전에 경계 지도에 기록할 내용을 작성하라.

정답과 해설

의미 면에서는 각 연산의 멱등성과 중복 방지 방법, 자원 면에서는 한 사용자 요청이 만드는 최대 하위 호출 수와 추가 지연·연결 점유를 적는다. 실패 면에서는 재시도 가능한 일시 오류와 즉시 실패해야 할 영구 오류, 응답만 유실된 경우의 상태를 구분한다. 경계로 전체 요청 시간 예산, 시도별 타임아웃, 최대 동시 재시도와 포화 시 중단 조건을 둔다. 관찰 항목은 원 시도와 재시도 요청량, 성공률, 백오프 대기, 하위 서비스 포화도다. 탈출구로 기능별 비활성화, 재시도 예산, 회로 차단, 트래픽 감축을 준비한다. 공통 미들웨어가 정책을 소유하더라도 호출 의미를 모르면 안전한 기본값을 정할 수 없다.

참고 자료

  • Jerome H. Saltzer, David P. Reed, David D. Clark, End-to-End Arguments in System Design (1984) — 하위 계층의 기능만으로 상위 애플리케이션의 정확성을 완성할 수 없는 경우와 기능 배치의 판단 원칙을 확인한다.
  • Ecma International, ECMAScript Language Specification — Keyed CollectionsSet의 관찰 가능한 의미와 평균 접근 성능 요구가 특정 내부 자료구조를 강제하지 않는다는 경계를 확인한다.
  • Hyrum Wright, Hyrum's Law — 명시적 API 계약 밖의 관찰 가능한 구현 행동이 실제 호환성 의존으로 굳어지는 현장을 이해하는 보조 자료다.
  • Cloudflare, Details of the Cloudflare outage on July 2, 2019 — 백트래킹 정규식의 계산 비용과 배포·격리 체계가 결합해 전역 장애로 이어진 실제 사례를 확인한다.
  • Jeffrey Dean, Luiz André Barroso, The Tail at Scale (2013) — 규모와 사용률 증가가 개별 구성 요소의 일시적 지연을 전체 서비스의 꼬리 지연으로 증폭하는 메커니즘을 참고한다.