월요일에 나는 Google Forward-Deployed-Engineer 시스템 설계 면접을 봤고, 통과하지 못했다. 캐시, 큐, 샤드, 로드 밸런서 같은 구성 요소들은 알고 있었다. 압박감 속에서 내가 하지 못했던 것은, 엔지니어와 아키텍트를 가르는 단 한 가지였다. 그래서 나는 그 주 내내 이 기술을 처음부터 다시 쌓아 올렸다. 이 글은 내가 발견한 것들을, 미리 알고 들어갔더라면 좋았을 방식으로 정리한 것이다.
요즘 화제가 되고 있는 인기 글이 하나 있다 — "These 15 articles will turn you from engineer to architect" (번역: 이 15개의 글이 당신을 엔지니어에서 아키텍트로 바꿔줄 것이다) — Instagram의 설계, Netflix의 아키텍처, 캐싱, Kafka 등을 다룬 링크들이 걸려 있다. 좋은 글들이다. 하지만 열다섯 개의 블루프린트를 읽는다고 아키텍트가 되는 건, 열다섯 개의 레시피를 읽는다고 셰프가 되지 않는 것과 마찬가지다. 블루프린트는 무엇을 다루는지 보여줄 뿐이다. 면접이 시험하는 건 어떻게 하느냐다 — 그리고 그 '어떻게'는 단 하나의 습관이다.
단 하나의 습관: 무엇을 포기할 수 있는지 먼저 말하라
"Instagram을 설계하라"는 문제를 받은 엔지니어는 익숙한 상자를 집어 들고 거꾸로 논리를 짜맞춘다. 아키텍트는 첫 질문 자체를 다르게 던진다: 내가 희생해도 되는 건 무엇인가? 모든 실제 시스템은 동시에 완전히 가질 수 없는 것들 사이의 협상이다 — 그리고 그 유명한 협상에는 이름이 있다.
CAP는 그 사고방식 전체를 세 글자로 압축한 것이다. 네트워크가 분할되면(대규모에서는 반드시 그렇게 된다), 해당 데이터에 대해 일관성이든 가용성이든 둘 중 하나만 지킬 수 있다 — 둘 다는 불가능하다. 은행 잔고는 일관성을 지킨다. 그래서 쓰기를 거부한다. '좋아요' 카운터는 가용성을 지킨다. 그래서 값은 나중에 맞춘다. 아키텍트는 자신이 어느 쪽을 희생하는지 소리 내어 말하고, 그 이유도 밝힌다. "이 데이터에 대해서는 일관성 대신 가용성을 택하겠다. 오래된 좋아요 수는 괜찮지만 거부된 결제는 안 되니까" — 이 한 문장이 바로 이 일의 본질을 드러낸다.
블루프린트 열다섯 개는 어휘일 뿐이고, 트레이드오프가 문법이다. 단어를 더 외운다고 그 언어를 구사할 수 있게 되는 건 아니다.
아키텍트의 루프 — 압박 속에서도 실행할 수 있는 방법론
내가 실제로 저지른 실패는 이것이다: 어떤 아키텍처가 옳고 다른 아키텍처가 낭비인지를 가르는 숫자를 세우기도 전에 박스와 화살표부터 그리기 시작했다. 그래서 나는 그 습관을 어떤 인터뷰에서든 소리 내어 실행할 수 있는 5단계 루프로 바꿨다. 이 루프는 모든 답변의 개요 역할도 겸한다.
아키텍트의 루프. 파란색 단계는 예전의 나였다면 곧장 Sketch로 건너뛰었을 부분이다. 인터뷰에서 채점하는 건 Constrain, Estimate, Stress, Evolve다 — 암기가 아니라 판단력을 보여주는 부분들이다.
핵심은 바로 2단계다. 어림값 계산이 이후의 모든 선택을 좌우한다. 일간 활성 사용자 × 행동 수 × 페이로드 = 쓰기 QPS. 읽기는 보통 쓰기의 10~1000배다. 저장 용량은 일별 데이터량 × 보관 기간이다. 이 수치들이 없으면 정당화할 수 없다 — 캐시든, 샤드 개수든, 큐든 말이다. 그저 추측할 수밖에 없고, 인터뷰어는 추측의 냄새를 맡아낸다. 수치가 있으면 한 아키텍처는 명백히 정답이 되고 다른 하나는 명백히 낭비가 되며, 그 이유를 말할 수 있게 된다.
어휘를, 그것이 해결하는 결정에 따라 재분류하기
이제 열다섯 개의 주제는 읽을거리 목록이 아니라 네 그룹의 결정으로 바뀐다. 각 그룹에서 외울 만한 가치가 있는 건 오직 당연해 보이지 않는 판단뿐이다 — 똑똑한 엔지니어가 틀리기 쉬운 그 지점 말이다.
그룹 1 — 데이터 플레인 (상태가 머무는 곳)
| 주제 | 당연해 보이지 않는 판단 |
|---|---|
| 데이터베이스 선택 | 선택 기준은 유행이 아니라 접근 패턴이다: 먼저 핫 쿼리를 모델링한 다음, 그것을 O(1)로 만들어주는 저장소를 고른다. ACID와 관계형 데이터에는 SQL; 대량 쓰기 피드에는 와이드-컬럼; 샤드 키가 존재하는 이유는 핫 쿼리를 하나의 샤드에 묶어두기 위해서다. |
| 캐싱 | 어려운 부분은 속도가 아니라 무효화(invalidation)다. TTL이 실용적인 기본값이고, 함정은 콜드/만료된 키에서 발생하는 thundering herd (콜드 키·만료 키에서 발생하는) 문제 — request coalescing와 stale-while-revalidate로 해결한다. cache-aside 이 상황에서는 TTL이 흔히 쓰는 쓰기 정책이다. |
| 자료구조 | 이들은 저장 매체에 맞춰 설계된다: B-트리는 디스크 탐색을 최소화하고(SQL 인덱스), LSM-트리는 쓰기에 유리하며(Cassandra), consistent-hashing 링은 노드가 추가·제거될 때 이동하는 데이터를 최소화한다; 블룸 필터는 실패할 조회를 저렴하게 걸러낸다. |
그룹 2 — 정문과 계약
| 주제 | 당연해 보이지 않는 판단 |
|---|---|
| 로드 밸런서 vs 리버스 프록시 | 서로 다른 질문에 답한다. LB는 "한 대의 서버로는 트래픽이 감당이 안 될 때" (동일한 백엔드들에 분산)라는 질문에 답한다. 리버스 프록시는 "똑똑한 정문이 필요할 때" (TLS, 라우팅, 느린 클라이언트를 애플리케이션 앞단에서 버퍼링)라는 질문에 답한다 — 단일 서비스 앞에도 둘 수 있다. 대부분의 게이트웨이는 둘 다이다. |
| REST / API | 무상태성이 핵심이다: 각 요청이 자신의 맥락을 담고 있어 어떤 서버든 처리할 수 있다 — 이것이 LB 뒤에서 수평 확장을 가능하게 하는 속성이다. 멱등성은 분산 시스템에서 재시도를 안전하게 만드는 요소다. REST의 획일성이 부담이 될 때는 gRPC(내부용, 저지연, 스트리밍)나 GraphQL(클라이언트 맞춤 페이로드)을 쓴다. |
그룹 3 — 비동기 백본(시간축의 분리)
| 주제 | 당연해 보이지 않는 판단 |
|---|---|
| Kafka / 큐 | 재생 가능한 로그는 프로듀서와 컨슈머를 시간과 속도 측면에서 분리한다. 순서 보장은 전역이 아니라 파티션 단위다 — 실제로 지켜야 할 순서를 유지하려면 파티션 키를 신중히 골라야 한다. 진짜 건강 지표는 처리량이 아니라 컨슈머 랙(consumer lag)이다. |
| 마이크로서비스 | 그 동인은 기술적이라기보다 조직적이다 (콘웨이의 법칙 — 팀들이 독립적으로 배포한다는 것). 프로세스 내 호출 하나를 실패할 수 있는 네트워크 호출로 맞바꾸는 셈이니, 그 대가로 서비스 디스커버리·재시도·서킷 브레이커·트레이싱, 그리고 분산 트랜잭션 대신 사가(sagas)를 얻는다. 반직관적이지만 진실은 이렇다: 대부분의 시스템은 잘 구조화된 모놀리스로 시작해야 한다. |
| 아키텍처 패턴 | 각각은 결합도를 유연성과, 혹은 일관성을 확장성과 맞바꾼다: 이벤트 기반(느슨한 결합, 최종 일관성), CQRS(읽기·쓰기 부하가 갈라질 때 모델을 분리), strangler-fig(모놀리스를 점진적으로 마이그레이션). 패턴의 이름을 아는 것은 실력이 아니다 — 중요한 건 그 패턴이 어떤 제약을 완화해주는지 아는 것이다. |
그룹 4 — 두 가지 사례 연구 (균일한 전략이 무너지는 지점)
Instagram과 Netflix가 유명한 이유는 각각 분포의 꼬리(tail)에 관한 날카로운 교훈을 하나씩 가르쳐주기 때문이다.
Instagram = 하이브리드 팬아웃. 균일한 전략은 꼬리 부분에서 무너진다. 푸시는 읽기가 O(1)이지만 셀럽에게는 폭발적으로 늘어나고, 풀은 쓰기는 저렴하지만 읽기가 느리다. 답은 둘 다이며, 계정별로 선택한다. 균일한 전략이 꼬리에서 실패하는 지점을 짚어내는 것이 테스트의 전부다.
Netflix는 정반대의 분리를 가르쳐준다. 거대하고 캐시 가능한 데이터 플레인 (여러 비트레이트로 미리 인코딩된 영상을 ISP 근처 CDN 엣지로 푸시)을 작고 동적인 컨트롤 플레인 (인증, 추천 시스템 — 클라우드 위의 마이크로서비스). 그리고 장애를 예외가 아니라 기본값으로 취급하는데, 카오스 엔지니어링으로 인스턴스를 일부러 죽여서 시스템이 살아남는지 증명하는 식이다. 아키텍트가 얻어가는 교훈은 이렇다 — 장애를 기본 시나리오로 상정하고 설계하며, 바이트를 사용자 가까이로 옮기라는 것이다.
내가 놓쳤던 마지막 한 수
월요일의 나는 못 했지만 지금은 하는 것이 있다. 해피 패스를 스케치한 다음, 나는 장애 모드를 소리 내어 하나씩 짚어본다: 이 노드가 죽으면 어떻게 되지? 이 큐가 밀리면? 이 캐시가 식으면? 이 샤드에 부하가 몰리면? 그러고 나서 병목과 다음에 당길 레버를 짚어낸다. 실패 시나리오가 빠진 설계는 엔지니어의 답변일 뿐이다. 병목을 짚어내고 그다음에 어떻게 진화시킬지 말하는 것 — 그게 아키텍트의 답이고, 바로 그 부분이 채점 대상이다.
한 호흡으로 정리한 전체 방법론
제약하고 문제를, 추정하고 숫자를, 스케치하고 그 숫자들이 요구하는 경로를, 스트레스 테스트하고 실패 상황에 맞서, 진화시킨다 — 병목과 다음 레버를 짚어내면서. 모든 트레이드오프를 소리 내어 말하라. 그게 전부다. 열다섯 개의 글을 아는 것과 열여섯 번째 글을 설계할 수 있는 사람이 되는 것의 차이는 바로 여기서 갈린다.
월요일에 나는 시스템 설계 라운드에서 떨어졌다. 그리고 금요일이 되자 그것은 하나의 프레임워크가 되어 있었다 — 그리고 솔직히, 합격했을 때보다 지금 이 내용을 더 깊이 이해하고 있다. 실패를 가르칠 수 있는 무언가로 바꾸는 과정이야말로 이걸 몸에 새긴 방법이었다. 여러분도 준비 중이라면: 열다섯 개의 글을 정답을 찾으려고 읽지 마라. 트레이드오프를 읽어내고, 매번 어느 쪽을 선택하는지 소리 내어 말하는 연습을 하라.
합격하지 못한 Google FDE 시스템 설계 라운드 이후, "15개 아티클, 엔지니어 → 아키텍트" 리딩 리스트를 그 밑에 깔린 단 하나의 습관으로 압축해 썼다. 이 글 덕분에 나보다 침착하게 들어갈 수 있다면, 그걸로 내 역할은 다한 셈이다. — Paul