한 줄 정의
서비스형 컴퓨트(CaaS)란 ‘내 코드를 실행할 하드웨어를 달라’는 단순한 요구를, 조직이 커져도 사람 손을 더 타지 않고 자동으로 확장되는 공통 인프라로 길들이는 일입니다.
쉽게 말하면
CaaS는 머신을 반려동물처럼 키우다가 가축처럼 치는 쪽으로 옮겨가는 이야기 입니다.
집에서 강아지 한 마리를 키우면 이름을 지어주고, 아프면 병원에 데려가 끝까지 낫게 합니다. 한두 마리는 가능하지만 수천 마리는 불가능합니다. 목장의 가축은 다릅니다. 이름 대신 번호(‘복제본001’)를 붙이고, 한 마리가 병들면 치료하느라 매달리는 대신 무리에서 빼내고 새 마리로 채웁니다.
서버도 똑같습니다. SSH로 직접 접속해 패키지를 깔고 문제가 생기면 달려가 보살피던 머신은 반려동물입니다. 반면 자동화 시스템이 죽은 머신을 알아서 제거하고 똑같은 새 인스턴스를 찍어내 작업을 이어가게 만들면, 그 머신은 가축이 됩니다.
핵심은 머신 하나하나에 정 붙이지 않고, 죽으면 똑같은 새것으로 자동 교체되도록 짓는 것 입니다. 그래야 1대를 굴리든 1만 대를 굴리든 관리 비용이 거의 늘지 않습니다. 이 장의 모든 기법(컨테이너·무상태 설계·서비스 디스커버리·멱등성)은 결국 “어떻게 하면 서버를 진짜 가축처럼 다룰 수 있는가”에 대한 답입니다.
왜 중요한가?
코드의 가치는 실행될 때 실현되고, 실행하려면 하드웨어가 필요합니다. 처음엔 SFTP로 코드를 올리고 SSH로 접속해 돌리는 것만으로 충분합니다. 하지만 머신이 수백~수천 대로 늘면 이 방식은 무너집니다.
제프 딘, 2002년 (Global WorkQueue 설계 문서)
50대 이상의 머신 각각에 프로세스를 띄우고 경과를 모니터링해야 합니다. 머신 중 하나가 고장나면 작업을 다른 머신으로 자동 이전할 방법이 없고, 사람이 작성한 복잡한 ‘sign up’ 파일로 사용량을 제한하다 보니 스케줄링이 비효율적이고 자원을 두고 치열하게 경합하게 됩니다.
이게 자동화되지 않으면 조직이 커질수록 다음 세 요인이 동시에 증가 하며 관리 비용이 성장 속도보다 가파르게 폭증합니다.
- 관리해야 할 애플리케이션의 종류
- 운영해야 할 애플리케이션 복제본의 수
- 가장 큰 애플리케이션의 크기
세 가지를 사람이 손으로 따라잡는 건 불가능하므로, 결국 모두 자동으로 관리하는 공통 컴퓨트 인프라가 필요해집니다.
핵심 내용
손으로 하던 일을 서비스로 — Borg의 탄생
길들이기는 단계적으로 진행됩니다. 구글의 Borg 는 쿠버네티스·Mesos 같은 현대 CaaS 아키텍처의 선구자였고, 이 진화 과정을 그대로 보여줍니다.
- 간단한 자동화 — 배포·모니터링·재시작을 셸 스크립트로 감쌉니다. 생존 여부(alive) 같은 지표를 모니터링 서비스(Grafana·Prometheus)로 모으고, 이상 감지 시 프로세스를 자동으로 종료·재시작합니다. 클라우드의 헬스 체크 실패 시 VM/컨테이너 자동 재생성과 같은 발상입니다.
- 스케줄링 자동화 — 여기서 진짜 ‘서비스’가 탄생합니다. 사용 가능한 머신 전체 목록을 알고, 요청이 오면 미사용 머신을 골라 자동 배포하는 중앙 서비스 입니다. 손으로 쓰던 ‘sign up’ 파일이 사라집니다.
- 장애 흡수 — 스케줄러가 장애 머신에는 작업을 배분하지 않고, 끝내지 못한 작업을 새 머신에 할당해 재시작합니다. 사람의 개입 없이 자가치유가 가능해집니다.
이렇게 만들어진 초기 형태의 CaaS가 Global WorkQueue 이고, 이후 Borg로 발전합니다.
컨테이너화와 멀티테넌시
머신과 프로그램을 1:1로 묶으면 자원(CPU·RAM·디스크)이 크게 낭비됩니다. 작업 종류는 머신 종류보다 훨씬 많고, 자원 요구량은 시간이 갈수록 늘지만 머신 구비엔 시간이 걸려 늘 넉넉하게(=낭비하며) 운용해야 하기 때문입니다. 그래서 한 머신에 여러 작업을 올리고, 프로그램별 자원 요구사항을 명기하면 스케줄러가 여유 있는 머신에 채워 넣게 합니다.
문제는 한 머신을 나눠 쓰는 작업들이 서로를 침범한다는 점입니다. 한 프로그램이 RAM을 먹어치우면 옆 프로그램이 OOM으로 죽고, 의존성·/tmp 같은 전역 자원이 충돌하며, 민감 데이터가 새기도 합니다. 그래서 멀티테넌트(multitenant) 컴퓨트 서비스는 테넌트들을 서로 격리해 보호해야 합니다.
| 격리 수단 | 특징 |
|---|---|
| VM (virtual machine) | OS까지 통째로 돌려 부하·구동 시간이 큼. 짧게 처리하고 끝내는 배치엔 부적합 |
| 컨테이너 | cgroups·chroot jail·네임스페이스·overlay 파일시스템으로 가볍게 격리. Docker·LMCTFY |
Borg가 2003년부터 컨테이너로 진화한 이유가 이것입니다. 다만 격리는 끝나지 않은 싸움입니다 — 2011년엔 프로세스 ID 공간이 고갈돼 격리가 깨지는 일이 있었습니다(아래 ‘컨테이너와 암묵적 의존성’).
적정 규모화는 왜 어려운가
자원 요구사항을 사람이 설정값으로 정하면, 그 값은 날이 갈수록 효율을 떨어뜨립니다. 프로그램은 업그레이드되며 덩치가 커지는데 설정값은 잘 따라 바뀌지 않아 예비 자원을 잠식하고, 결국 사용량이 급증하면 감당하지 못합니다. 그래서 컨테이너 크기를 자동으로 맞추는 적정 규모화(rightsizing) 가 필요하지만, 구글조차 자원 사용량 기준으로 자동화 비중이 최근에야 절반을 넘었을 만큼 까다로운 문제입니다.
가축처럼 다루기 — 장애를 감안한 설계
스케줄러가 죽은 워커를 새 인스턴스로 자동 교체할 수 있다는 게 ‘가축’의 핵심입니다. 하지만 가축화만으로는 부족 합니다. 200대 중 한 대가 종료되는 데 50분이 걸린다면, 그 워커가 처리하던 작업을 통째로 잃기 때문입니다.
해법은 작업을 잘게 쪼개 동적으로 할당 하는 것입니다. 문서 백만 개를 1,000개씩 1,000 뭉치로 나누고, 각 워커가 한 뭉치를 끝낼 때마다 결과를 보고하고 다음 뭉치를 가져가게 합니다. 그러면 워커 하나가 죽어도 손해는 최대 한 뭉치 처리 시간뿐입니다. 시작 시 동일한 분량을 정적으로 나눠주는 게 아니라, 진행 상황을 보며 동적으로 할당하는 것이 핵심입니다.
서빙 작업이라면 장애가 사용자에게 닿기 전에 재스케줄링해야 합니다. Borg 스케줄러는 컨테이너를 옮기기 전에 미리 의도를 알리고, 신호를 받은 컨테이너는 새 요청을 거부하면서 받아둔 요청만 마무리합니다. 이때 트래픽을 다른 컨테이너로 돌려줄 로드밸런서 가 함께 필요합니다.
배치 vs 서빙
| 배치(batch) | 서빙(serving) | |
|---|---|---|
| 성격 | 정해진 태스크를 완료하는, 끝이 있는 작업 | 들어오는 요청을 끝없이 처리 |
| 예 | 로그 분석, ML 모델 학습 | 웹 검색 |
| 중요 지표 | 처리량(throughput) | 지연시간(latency) |
| 수명 | 짧음(분~시간) | 긺(새 릴리스 때만 재시작) |
| 장애 강건성 | 뭉치를 동적 할당해 확보 (MapReduce→Flume) | 요청 자체가 작은 단위라 자연히 강함 |
서빙이라도 상태를 메모리/로컬에 쥔 ‘리더’ 서버나, 데이터를 머신에 정적으로 분배한 경우, 호스트명으로 접근하는 경우는 이 모델에 들어맞지 않아 별도의 대비가 필요합니다.
상태와 연결 — 무상태·디스커버리·멱등성
가축처럼 다룰 때 발목을 잡는 공통 주제는 상태(state) 입니다. 서버를 갈아치우는 순간 처리 중이던 상태와 로컬 스토리지 데이터를 잃기 때문입니다.
- 로컬은 휘발성으로 가정합니다. 한 요청·한 데이터 뭉치 처리 범위를 넘어 존재해야 하는 데이터는 견고한 외부 영구 스토리지에 둡니다. ‘영구 스토리지도 가축인가?’ → 네. 상태를 3~5벌 복제해 동기화하면(RAID 어레이처럼) 상태도 가축처럼 관리됩니다. 구글은 이를 위해 GFS·Bigtable·Spanner 같은 스토리지 제품을 만들었습니다.
- 캐시의 교훈 — 다시 만들 수 있는 데이터는 로컬에 캐시해 지연시간을 줄여도 됩니다. 단, 핵심 애플리케이션은 캐시가 통째로 사라져도 전체 부하를 감당 할 수 있게 준비해야 합니다(지연시간은 늘겠지만 서비스 중단은 막습니다).
연결도 가축화의 일부입니다. 호스트명을 하드코딩하면 복제본은 더 이상 가축이 아닙니다(다른 머신에서 재시작되면 끊기므로). 그래서 서비스 디스커버리(service discovery) 라는 간접 계층을 둡니다.
flowchart LR C["클라이언트"] -->|"① 식별자로 주소 조회"| L[("룩업 시스템<br/>서비스 디스커버리")] L -->|"② 현재 주소"| C C -->|"③ 연결"| B["백엔드 복제본"] S["스케줄러"] -->|"재배포 시 주소 갱신"| L
재시도에는 멱등성이 필요하다
응답 전에 장애가 날 수 있으므로 요청을 재시도해야 하는데, 상태를 바꾸는 요청이라면 두 번 보내도 한 번과 같은 결과여야 합니다 — 멱등성(idempotency) 입니다. 그래서 생성 요청 시 클라이언트가 식별자를 미리 할당(client-assigned identifier)해, 같은 식별자가 다시 오면 서버가 재요청으로 판단하고 곧장 성공으로 응답하게 합니다. 네트워크 단절로 ‘복제본072’가 두 대가 되는 상황(스플릿브레인)에서도, 주소 결정 시스템이 가리키는 쪽만 유효한 인스턴스로 두어 멱등성으로 안전을 보장합니다.
컨테이너는 추상화 계층이다
컨테이너는 격리 수단일 뿐 아니라 애플리케이션과 머신 사이의 추상화 계층 입니다. 덕분에 머신을 바꿔도 애플리케이션은 그대로 두고 컨테이너만 손보면 됩니다.
- 파일시스템 추상화 — 특정 경로(
/bin/foo/bar)를 가정하는 서드파티 바이너리도, 필요한 의존성을 미리 선언·패키징해 통합할 수 있습니다. 추상화가 없으면 모든 머신의 레이아웃을 고치거나 소프트웨어 자체를 수정(라이선스상 불가능할 수도)해야 합니다. - 포트 추상화 — 초기 구글 컨테이너는 포트를 추상화하지 않아 가용 포트를 찾는
PickUnusedPortOrDie()가 코드베이스에 2만 번이나 등장했습니다. Docker는 네임스페이스로 가상 NIC를 제공하고, 쿠버네티스는 컨테이너(포드)에 진짜 IP까지 줘서 모든 앱이 충돌 걱정 없이 원하는 포트를 씁니다.
컨테이너와 암묵적 의존성 — 하이럼의 법칙
하이럼의 법칙은 컨테이너 추상화에도 어김없이 작용합니다. PID는 보통 0~32,000 범위인데, 이 한계가 API가 보장하는 암묵적 약속 이 되어(로그 서비스가 ‘PID는 다섯 자리’라고 가정하는 식) 사람들이 의존하기 시작했습니다. 그래서 PID 이름공간을 도입해 격리하는 작업이 단계적으로 진행됐고, {호스트명, 타임스탬프, PID} 로 프로세스를 고유 식별한다는 가정이 무너지면서 관련 코드를 고치는 일이 8년이 지나도록 이어지고 있습니다. 지속 가능한 컨테이너 설계가 그만큼 어렵다는 방증입니다.
하나의 풀로 통합 + 표준 설정 언어
원래 배치는 WorkQueue, 서빙은 전용 머신 풀로 따로 굴렀습니다. 2003년 Borg는 이를 하나의 거대한 풀로 통합 했고, 두 가지 효율을 얻었습니다.
- 관리 비용이 규모와 무관해집니다. ‘머신엔 이름이 없고, 프로그램은 요구조건만 맞으면 어느 머신인지 개의치 않는다’는 가축화의 정점입니다.
- 배치와 서빙의 요구가 상호 보완적입니다. 서빙은 트래픽 급증에 대비해 자원을 여유 있게 잡아 이용률이 30%에 그치는데, 남는 70%를 처리량 중심의 배치가 채웁니다. 그래서 구글은 대부분의 배치를 사실상 무료로 돌립니다.
복잡해진 구성(다중 데이터센터, 스테이징/데브 환경, memcached 같은 부가 컨테이너)을 다루려면 표준 설정 언어 가 필요합니다. ‘신버전으로 교체하되 한 번에 중단되는 머신이 전체 5%를 넘지 않게’ 같은 관리 의도를 코드로 표현하고 팀 간에 공유할 수 있어, 시간·규모 측면에서 매우 효과적입니다.
한 단계 위: 서버리스
추상화를 더 높이면 서버리스(serverless) 가 됩니다. 프레임워크가 제어 반전(IoC)으로 요청을 받아 사용자가 작성한 함수만 호출하고, 트래픽에 맞춰 자동으로 확장·축소합니다.
서버리스의 결정적 특징은 코드가 완전히 무상태(stateless) 여야 한다는 점입니다. 다음 요청까지 지속되는 로컬 상태 개념이 없으므로 모든 것을 하나의 요청 범위 안에서 끝내야 합니다. 대신 트래픽이 없을 때 복제본을 0개까지 줄여 소유 비용을 없앨 수 있습니다(컨테이너 모델은 존재만으로 비용이 듭니다).
참고로 대부분의 서버리스는 다른 컴퓨트 계층 위에 얹혀 있습니다 — AppEngine은 Borg, Knative는 쿠버네티스, Lambda는 EC2 위에서 돕니다.
비교 / 트레이드오프
추상화 사다리 — 반려동물에서 서버리스까지
| 모델 | 추상화 | 관리 부담 | 핵심 |
|---|---|---|---|
| 베어메탈 (반려동물) | 낮음 | 전부 내 몫 | 직접 소유·임대, 머신마다 손수 관리. 대수에 비례해 비용 폭증 |
| VM | 중 | 자동 확장·적정 규모화 포함 | 자체 OS를 들고 올 수 있어 유연하나 컨테이너보다 무거움 |
| 컨테이너 (가축) | 높음 | 스케줄러가 자가치유 | 자원 효율·자동 교체. 무상태 설계 요구. 복제본 0개로는 못 줄임 |
| 서버리스 | 가장 높음 | 인프라 업체가 대부분 | 완전 무상태 강제. 트래픽 0이면 비용 0. 상태 필요한 작업은 불가 |
위로 갈수록 관리가 편하지만 제약과 종속이 커집니다. ‘추상화가 높을수록 무조건 좋다’가 아니라, 조직 규모와 작업 성격에 맞는 지점을 골라야 합니다. 구글은 Borg가 서버리스의 이점 대부분(자동 확장·프레임워크·통합 로깅)을 제공하므로 서버리스에 과투자하지 않는 길을 택했지만, 코드가 한 팀 규모인 작은 조직엔 Lambda·Cloud Run 같은 서버리스가 더 간단하고 저렴합니다.
공용 vs 사설 — lock-in이 진짜 변수다
컴퓨트 인프라는 강력한 종속(lock-in) 요인입니다. 코드가 그 인프라의 이점을 최대한 쓰도록 작성되기 때문에(여기에도 하이럼의 법칙), 한 번 정착하면 마이그레이션이 매우 어렵습니다.
| 선택 | 이점 | 위험 |
|---|---|---|
| 사설(직접 구축) | 완전한 통제, 베어메탈은 저렴 | 관리 부담 전부 떠안음, 확장이 느림 |
| 공용 클라우드 | 관리 아웃소싱, 확장 쉬움(자원 예측 불필요) | 업체 종속 — 비용 인상·장애·폐업 시 곤란 |
종속을 완화하는 길은 클라우드 위에 쿠버네티스 같은 이식 가능한 오픈소스 계층 을 얹거나(EC2 위의 Knative), 같은 오픈소스를 제공하는 여러 업체를 쓰는 멀티 클라우드(GKE·AKS 모두 쿠버네티스)입니다. 다만 하이럼의 법칙 탓에 종속을 완전히 없앨 수는 없습니다.
내 생각
-
‘가축’은 쿠버네티스 Deployment/ReplicaSet 그 자체다. desired replica 수를 선언하면 죽은 포드를 알아서 새것으로 채우는 동작이 곧 자가치유 가축 모델이다. 이 장은 그 사고방식의 뿌리(Borg)를 보여준다.
-
‘로컬은 휘발성, 호스트명 하드코딩 금지’는 12-factor app의 핵심과 같다. 무상태(stateless)·외부 백킹 서비스·환경 기반 설정이 곧 가축화의 전제 조건이다.
-
서비스 디스커버리 = Eureka·Consul·k8s Service/DNS. 백엔드 주소를 룩업으로 간접화하는 패턴이 서비스 로케이터·로드밸런싱과 같은 줄기다.
-
멱등성 + 클라이언트 할당 식별자는 결제 API에서 매일 쓰는 그것이다. Idempotency-Key 헤더로 중복 결제를 막는 패턴이 정확히 이 장의 처방이다. 재시도가 있는 분산 시스템이면 피할 수 없다.
-
lock-in이 컴퓨트 선택의 진짜 1순위 기준이다. 성능·가격보다 ‘나중에 빠져나올 수 있는가’를 먼저 본다. 그래서 클라우드 위에 k8s를 한 겹 깔아 이식성을 사두는 전략이 실무에서 합리적이다.
관련 개념
- Ch21 의존성 관리 — 컨테이너 추상화를 옥죄는 하이럼의 법칙(PID 고갈)이 여기서 다룬 의존성 문제와 같은 뿌리입니다
- Ch24 지속적 배포 — ‘한 번에 5%만 교체’ 같은 표준 설정 언어의 관리 의도가 단계적 출시·플래그 가드와 맞물립니다
- Ch01 소프트웨어 엔지니어링이란 — 이 장 전반에 작용하는 하이럼의 법칙의 출처입니다