조 암스트롱이 Erlang의 동시성, 감독, 'let it crash' 사고방식을 어떻게 만들었고, 이 아이디어들이 오늘날 신뢰성 있는 실시간 서비스 구축에 어떻게 쓰이는지 살펴봅니다.

조 암스트롱은 단지 Erlang을 만든 사람이 아니라, 그 아이디어를 가장 명확하고 설득력 있게 설명한 사람입니다. 강연과 논문, 실용적 관점을 통해 그는 간단한 관점을 널리 알렸습니다: 소프트웨어를 계속 가동하려면 실패를 피하려고 애쓰는 대신 실패를 설계하라.
이 글은 Erlang적 사고방식의 가이드 투어이자, 채팅 시스템, 콜 라우팅, 실시간 알림, 멀티플레이어 조정처럼 일부가 잘못되어도 빠르고 일관되게 반응해야 하는 신뢰성 있는 실시간 플랫폼을 만들 때 왜 이 접근법이 여전히 유효한지 설명합니다.
실시간이 항상 "마이크로초"나 "하드 데드라인"을 뜻하는 건 아닙니다. 많은 제품에서 실시간은 다음을 의미합니다:
Erlang은 이러한 기대가 절대적으로 필요한 통신(telecom) 시스템을 위해 만들어졌고, 그 압박이 가장 영향력 있는 아이디어들을 만들었습니다.
문법으로 깊게 들어가기보다 Erlang을 유명하게 만든, 그리고 현대 시스템 설계에서 계속 반복되는 개념들에 초점을 맞춥니다:
이 과정에서 액터 모델과 메시지 전달을 연결하고, 감독 트리와 OTP를 알기 쉽게 설명하며 BEAM VM이 왜 이 접근을 실용적으로 만드는지도 보여줍니다.
Erlang을 직접 쓰지 않더라도 암스트롱의 틀은 시스템이 혼란스러울 때도 반응성과 가용성을 유지하게 해주는 강력한 체크리스트를 제공합니다.
통신 교환기와 콜 라우팅 플랫폼은 많은 웹사이트처럼 "유지보수를 위해 다운"될 수 없습니다. 통화를 처리하고, 청구 이벤트와 신호 트래픽을 24시간 내내 다뤄야 하며 — 종종 가용성과 예측 가능한 응답 시간에 대한 엄격한 요구가 있습니다.
Erlang은 1980년대 후반 에릭슨(Ericsson) 내부에서 이런 현실을 소프트웨어로 해결하려는 시도로 시작했습니다. 암스트롱과 동료들은 단순히 우아함을 쫓은 것이 아니라, 운영자가 지속적인 부하와 부분 장애, 현실의 난잡함 속에서도 신뢰할 수 있는 시스템을 만들려 했습니다.
사고 방식의 핵심 변화는 신뢰성이 "절대 실패하지 않음"과 같지 않다는 점입니다. 크고 장기적으로 운영되는 시스템에서는 무언가 반드시 실패합니다: 프로세스가 예기치 않은 입력을 만나거나 노드가 재부팅되거나 네트워크 링크가 불안정해지거나 의존성이 멈출 수 있습니다.
따라서 목표는 다음이 됩니다:
이 사고방식이 감독 트리와 "let it crash" 같은 아이디어를 합리적으로 만듭니다: 실패를 비정상적 재난으로 보지 않고 정상적인 이벤트로 설계합니다.
이 이야기를 한 사람의 선견지명으로만 설명하기 쉽지만, 더 유용한 관점은 간단합니다: 통신 업계의 제약이 다른 트레이드오프를 강요했다는 것. Erlang은 동시성, 격리, 복구를 우선시했는데, 이는 변화하는 환경 속에서 서비스를 유지하는 데 실질적으로 필요한 도구들이었습니다.
문제 우선의 프레이밍 덕분에 Erlang의 교훈은 오늘날에도—가용성과 빠른 복구가 예방보다 중요한 곳—잘 전이됩니다.
Erlang의 핵심 아이디어는 "한 번에 여러 일을 처리하는 것"이 나중에 붙이는 특별 기능이 아니라 시스템을 구조화하는 일반적인 방식이라는 점입니다.
Erlang에서는 작업을 아주 작은 "프로세스"로 나눕니다. 각 프로세스는 전화 통화 처리, 채팅 세션 추적, 디바이스 모니터링, 결제 재시도, 큐 감시처럼 한 가지 작업을 담당하는 작은 워커라고 생각하면 됩니다.
이들은 가벼워서 많은 수를 두어도 큰 하드웨어가 필요하지 않습니다. 하나의 무거운 워커가 모든 걸 처리하려 애쓰는 대신, 빠르게 시작하고 멈추고 교체할 수 있는 수많은 집중된 워커가 존재합니다.
많은 시스템은 여러 부분이 밀접하게 연결된 하나의 큰 프로그램처럼 설계됩니다. 이런 시스템이 치명적인 버그, 메모리 문제, 블로킹 연산을 만나면 실패가 넓게 파급되어 한 번에 전체를 내려앉힐 수 있습니다.
Erlang은 반대 방향을 밀어붙입니다: 책임을 격리하세요. 한 작은 워커가 잘못되어도, 관련 없는 작업을 중단하지 않고 그 워커만 교체할 수 있습니다.
이 워커들은 서로의 내부 상태를 들여다보지 않습니다. 대신 메시지를 보냅니다—공유된 지저분한 화이트보드가 아니라 쪽지를 주고받는 느낌입니다.
한 워커가 "새 요청", "사용자가 연결 끊음", "5초 후 다시 시도" 같은 메시지를 보내면 받는 쪽이 읽고 처리 방침을 결정합니다.
핵심 이점은 격리입니다: 워커들이 고립되고 메시지로 통신하므로 실패가 시스템 전체로 확산될 가능성이 줄어듭니다.
Erlang의 "액터 모델"을 이해하는 쉬운 방법은 많은 작고 독립적인 워커들로 이루어진 시스템을 떠올리는 것입니다.
액터는 자체 비공개 상태와 메일박스를 가진 독립 단위입니다. 액터가 하는 기본 동작은 세 가지입니다:
그게 전부입니다. 숨겨진 공유 변수도 없고 다른 워커의 메모리를 건드릴 수도 없습니다. 한 액터가 다른 액터에서 무언가가 필요하면 메시지로 요청합니다.
여러 스레드가 동일 데이터를 공유하면 두 개가 거의 동시에 같은 값을 바꿀 때 결과가 타이밍에 따라 달라지는 경쟁 상태가 생길 수 있습니다. 이런 버그는 간헐적이고 재현하기 어렵습니다.
메시지 전달에서는 각 액터가 자기 데이터를 소유하므로 다른 액터가 직접 변경할 수 없습니다. 모든 버그를 제거하지는 않지만 동일 데이터에 동시 접근해서 생기는 문제는 크게 줄어듭니다.
메시지는 "공짜로" 오지 않습니다. 액터가 처리 속도보다 더 많은 메시지를 받으면 메일박스(큐)가 커집니다. 그게 백프레셔입니다: 시스템이 간접적으로 "이 부분이 과부하다"라고 알려주는 것입니다.
실무에서는 메일박스 크기를 모니터링하고 한계를 설계합니다: 부하를 포기하거나, 배치하거나, 샘플링하거나, 더 많은 워커로 분산해서 큐가 무한히 늘어나지 않게 합니다.
채팅 앱을 상상해보세요. 각 사용자에 대해 알림을 전달하는 액터를 둘 수 있습니다. 사용자가 오프라인이면 메시지는 계속 도착해서 메일박스가 커집니다. 잘 설계된 시스템은 큐를 제한하거나 중요하지 않은 알림을 버리거나 다이제스트 모드로 전환해 한 느린 사용자가 서비스 전체를 저하시킬 수 없게 합니다.
"Let it crash"는 성의 없는 엔지니어링 구호가 아닙니다. 이는 신뢰성 전략입니다: 구성요소가 잘못되거나 예기치 않은 상태에 빠졌을 때 꾸역꾸역 버티지 말고 빠르게 멈추라는 것입니다.
한 프로세스 안에서 모든 가능한 모서리 케이스를 처리하려 하기보다, Erlang은 각 워커를 작고 집중되게 유지하라고 권장합니다. 만약 그 워커가 진짜로 처리할 수 없는 상태(손상된 상태, 가정 위반, 예기치 않은 입력 등)를 만나면 그 워커는 종료(exit)합니다. 시스템의 다른 부분이 이를 감지하고 다시 시작합니다.
이렇게 하면 중심 질문이 "실패를 어떻게 막나?"에서 "실패가 일어났을 때 어떻게 깔끔하게 복구하나?"로 바뀝니다.
모든 곳에 방어 코드를 넣으면 단순한 흐름도 조건문, 재시도, 부분 상태로 복잡해집니다. "Let it crash"는 프로세스 내부 복잡도를 일부 포기하는 대신 다음을 얻습니다:
핵심은 복구가 예측 가능하고 반복 가능해야 한다는 점입니다. 즉석에서 임기응변으로 처리하면 안 됩니다.
임시 네트워크 문제, 잘못된 요청, 정지한 워커, 서드파티 타임아웃처럼 실패를 복구할 수 있고 격리 가능한 경우에 가장 잘 맞습니다.
반면 치명적 결과를 초래하는 상황(예: 내구 저장소 없이 데이터 손실이 발생할 가능성이 있거나, 재시도가 허용되지 않는 안전 중요 작업)에는 부적절합니다.
크래시는 재시작이 빠르고 안전할 때만 도움이 됩니다. 실무적으로는 워커를 알려진 정상 상태로 재시작해야 합니다—종종 구성 재로딩, 메모리 캐시 재구축, 손상된 상태를 무시하지 않고 안전하게 이어가기 같은 작업을 의미합니다.
Erlang의 "let it crash"가 효과를 발휘하려면 크래시를 운에 맡기면 안 됩니다. 핵심 패턴은 감독 트리입니다: 슈퍼바이저가 관리자처럼 행동하고 워커가 실제 작업을 합니다. 워커가 오작동하면 관리자가 이를 감지하고 재시작합니다.
슈퍼바이저는 고장난 워커를 제자리에 두고 고치려 하지 않습니다. 대신 간단하고 일관된 규칙을 적용합니다: 워커가 죽으면 새로 시작하라. 이렇게 하면 복구 경로가 예측 가능해지고 코드 전반에 흩어진 임시 복구 로직이 줄어듭니다.
중요한 점은 슈퍼바이저가 언제 재시작하지 않을지도 결정할 수 있다는 것입니다—어떤 것이 지나치게 자주 크래시한다면 더 깊은 문제가 있다는 신호일 수 있고, 반복 재시작은 상황을 악화시킬 수 있습니다.
감독은 한 가지 방식만 제공하지 않습니다. 일반 전략은:
좋은 감독 설계는 의존성 맵에서 시작합니다: 어떤 컴포넌트가 어떤 것에 의존하는가, 그리고 "새로 시작"이 각 컴포넌트에 무슨 의미인가.
예를 들어 세션 핸들러가 캐시 프로세스에 의존한다면 핸들러만 재시작하면 나쁜 상태에 연결된 채로 남을 수 있습니다. 적절한 슈퍼바이저 아래에 그룹화하거나 함께 재시작하도록 설계하면 엉킨 실패 모드를 일관된 복구 동작으로 바꿀 수 있습니다.
Erlang이 언어라면 OTP(Open Telecom Platform)는 "let it crash"를 수년간 안정적으로 운영 가능한 시스템으로 바꿔주는 부품 상자입니다.
OTP는 단일 라이브러리가 아니라 서비스 구축의 지루하지만 중요한 부분을 해결하는 관례와 준비된 컴포넌트(behaviours)의 집합입니다:
gen_server는 상태를 유지하며 한 번에 요청을 처리하는 장기 워커supervisor는 명확한 규칙에 따라 실패한 워커를 자동 재시작application은 전체 서비스가 어떻게 시작·중지되고 릴리스에 맞는지 정의이것들은 마법이 아니라 콜백이 정해진 템플릿이라, 프로젝트마다 새로운 형태를 발명하지 않고도 코드를 알려진 형태에 맞춰 넣을 수 있습니다.
팀들이 종종 임시 백그라운드 워커, 자체 모니터링 훅, 맞춤형 재시작 로직을 만듭니다. 초기에는 잘 돌아가지만 문제가 터지면 유지보수가 어렵습니다. OTP는 모두를 같은 어휘와 라이프사이클 쪽으로 밀어붙여, 새 엔지니어가 들어와도 먼저 사용자 정의 프레임워크를 배울 필요 없이 공통 패턴을 활용하게 합니다.
OTP는 프로세스의 역할과 책임(워커는 무엇, 코디네이터는 무엇, 누가 누구를 재시작할지, 자동으로 재시작하면 안 될 것은 무엇인지)을 생각하게 합니다.
또한 명확한 네이밍, 명시적 시작 순서, 예측 가능한 종료, 내장 모니터링 신호 같은 좋은 위생 관행을 장려합니다. 결과적으로 지속적으로 운영되는 소프트웨어—장애에서 복구하고 진화하며 지속적으로 일을 수행하는 서비스—를 만들기 쉬워집니다.
Erlang의 큰 아이디어들(작은 프로세스, 메시지 전달, let it crash)은 BEAM 가상 머신(VM)이 없었다면 운영 환경에서 사용하기 훨씬 어려웠을 것입니다. BEAM은 이러한 패턴을 자연스럽고 견고하게 만드는 런타임입니다.
BEAM은 수많은 라이트웨이트 프로세스를 실행하도록 설계되었습니다. OS 스레드 몇 개에 의존해 애플리케이션이 잘 동작하기만을 기대하는 대신 BEAM은 자체적으로 Erlang 프로세스들을 스케줄링합니다.
실무적 이점은 부하 상태에서도 응답성이 유지된다는 점입니다: 작업을 작은 조각으로 나누어 공정하게 돌아가게 하므로 하나의 바쁜 워커가 시스템을 장악하지 않습니다. 이는 많은 독립 작업으로 구성된 서비스 구조와 완벽히 맞습니다—각 작업이 조금 하고 양보하는 구조입니다.
각 Erlang 프로세스는 자체 힙과 자체 가비지 컬렉션을 가집니다. 중요한 점은 한 프로세스의 메모리 정리가 전체 프로그램을 정지시키지 않는다는 것입니다.
또한 프로세스들은 격리되어 있습니다. 하나가 크래시해도 다른 프로세스의 메모리를 손상시키지 않으며 VM 자체는 계속 살아있습니다. 이 격리가 감독 트리를 현실적으로 만드는 기초입니다: 실패를 격리하고 실패한 부분을 재시작으로 처리하는 것이 가능해집니다.
BEAM은 분산을 간단한 개념으로 지원합니다: 여러 Erlang 노드(별도 VM 인스턴스)를 실행하고 메시지를 통해 통신하게 할 수 있습니다. "프로세스들이 메시지로 통신한다"는 개념을 이해했다면 분산은 같은 아이디어의 확장입니다—단지 일부 프로세스가 다른 노드에 존재할 뿐입니다.
BEAM은 원시적인 속도 보장은 목표가 아닙니다. 대신 동시성, 결함 격리, 복구를 기본값으로 만들어 신뢰성 이야기가 이론이 아닌 실용으로 느껴지게 합니다.
Erlang의 잘 알려진 기능 중 하나는 *핫 코드 스와핑(hot code swapping)*입니다: 런타임과 도구가 지원하는 범위 내에서 실행 중인 시스템의 일부를 최소한의 다운타임으로 업데이트하는 것. 실무적 약속은 "다시는 재시작하지 않겠다"가 아니라 "짧은 문제를 긴 장애로 만들지 않고 수정하라"는 것입니다.
Erlang/OTP에서는 런타임이 동시에 두 버전의 모듈을 로드할 수 있습니다. 기존 프로세스는 오래된 버전으로 작업을 끝내고 새 호출은 새 버전을 사용하게 할 수 있습니다. 이렇게 하면 버그를 패치하거나 기능을 배포하거나 동작을 조정할 때 모든 사용자를 강제로 끊지 않고 작업할 여지가 생깁니다.
잘 하면 이것은 신뢰성 목표에 직접 기여합니다: 전체 재시작이 줄어들고, 유지보수 창이 짧아지며, 실수로 인해 프로덕션에 문제가 생겼을 때 더 빨리 회복할 수 있습니다.
모든 변경이 라이브로 안전한 건 아닙니다. 신중함이 필요한 변경 예시:
Erlang은 제어된 전환 메커니즘을 제공하지만, 업그레이드 경로를 설계해야 합니다.
핫 업그레이드는 업그레이드와 롤백을 일상적 작업으로 취급할 때 가장 잘 작동합니다. 즉, 버전 관리, 호환성, 명확한 "되돌리기" 경로를 처음부터 계획해야 합니다. 실무에서는 라이브 업그레이드 기법을 단계적 롤아웃, 헬스체크, 감독 기반 복구와 결합합니다.
Erlang을 직접 쓰지 않더라도 교훈은 전이됩니다: 시스템을 "안전하게 변경하는 것"을 일급 요구사항으로 설계하세요.
실시간 플랫폼은 완벽한 타이밍보다 부분적으로 망가지는 동안에도 반응성을 유지하는 데 더 가깝습니다: 네트워크는 흔들리고, 의존성은 느려지고, 사용자 트래픽은 급증합니다. 암스트롱이 옹호한 Erlang 설계는 실패를 가정하고 동시성을 정상으로 취급하기 때문에 이런 현실에 잘 맞습니다.
Erlang식 사고가 빛나는 곳들:
대부분 제품은 "모든 동작이 10ms 내에 완료되어야 한다" 같은 엄격한 보장이 필요하지 않습니다. 대신 소프트 실시간이 필요합니다: 일반 요청에 대해 일관되게 낮은 지연 시간, 부분 장애 시 빠른 복구, 사용자가 거의 체감하지 못할 높은 가용성.
실제 시스템은 다음과 같은 문제를 맞닥뜨립니다:
Erlang 모델은 각 활동(사용자 세션, 디바이스, 결제 시도)을 격리하도록 권장하므로 실패가 확산되지 않습니다. 하나의 거대한 컴포넌트에 모든 것을 맡기지 않고 작은 단위로 사고하면 각 워커가 하나의 작업을 하고 메시지로 통신하며 고장 시 깔끔하게 재시작됩니다.
이 관점의 전환—"모든 실패를 막자"에서 "빠르게 격리하고 복구하자"—이 실시간 플랫폼이 압력 속에서도 안정적으로 느껴지게 만드는 핵심입니다.
Erlang은 "절대 다운되지 않는 시스템"이라는 약속처럼 들릴 수 있지만 현실은 더 실용적입니다. "Let it crash"는 신뢰성 도구이지, 근본적인 문제를 무시해도 된다는 면허가 아닙니다.
감독을 문제를 숨기기 위한 수단으로 잘못 사용하는 실수가 흔합니다. 프로세스가 시작하자마자 즉시 크래시하면 슈퍼바이저가 계속 재시작해 크래시 루프가 되어 CPU를 태우고 로그를 스팸하며 원래의 버그보다 더 큰 장애를 유발할 수 있습니다.
좋은 시스템은 백오프, 재시작 빈도 한계, 그리고 "포기하고 에스컬레이션"하는 동작을 추가합니다. 재시작은 건강한 동작을 복원해야지 깨진 불변식을 가리면 안 됩니다.
프로세스를 재시작하는 건 종종 쉽지만 정확한 상태를 복구하는 건 어렵습니다. 상태가 메모리에만 있으면 크래시 후 무엇이 "정상"인지 결정해야 합니다:
내결함성은 데이터 설계를 대체하지 않습니다. 오히려 이를 명시적으로 만들게 합니다.
크래시는 유용하지만 그것을 빨리 보고 이해할 수 있어야 합니다. 따라서 로깅, 메트릭, 트레이싱에 투자해야 합니다—단순히 "재시작했으니 괜찮다"로 끝내면 안 됩니다. 증가하는 재시작률, 커지는 큐, 느려지는 의존성을 사전에 포착하고 대응할 수 있어야 합니다.
BEAM의 강점이 있어도 시스템은 평범한 문제로 실패할 수 있습니다:
Erlang 모델은 실패를 격리하고 복구하는 데 도움을 주지만, 실패 자체를 제거하지는 못합니다.
Erlang의 가장 큰 선물은 문법이 아니라 장애가 불가피할 때에도 시스템을 가동 상태로 유지하는 습관입니다. 거의 모든 스택에서 이 습관을 적용할 수 있습니다.
먼저 실패 경계를 명확히 하세요. 시스템을 독립적으로 실패할 수 있는 컴포넌트로 분해하고 각 컴포넌트의 명확한 계약(입력, 출력, "나쁨"의 기준)을 정의합니다.
그다음 예방 대신 복구를 자동화하세요:
이 습관을 도구와 라이프사이클에 박아 넣으면 더 현실적으로 정착합니다. 예를 들어 Koder.ai 같은 도구로 챗 기반으로 웹/백엔드/모바일 앱을 vibe-code할 때 Planning Mode, 반복 가능한 배포, 스냅샷과 롤백 같은 워크플로우가 자연스럽게 명시적 계획과 안전한 반복을 장려합니다. 이런 개념은 Erlang이 대중화한 운영적 사고방식과 맞닿아 있습니다: 변화와 실패를 가정하고 복구를 단조롭게 만드세요.
다음 도구로 감독 패턴을 흉내낼 수 있습니다:
패턴을 복사하기 전에 실제로 필요한 것을 결정하세요:
실질적인 다음 단계가 필요하면 /blog의 관련 가이드를 보거나 /docs의 구현 세부사항을 살펴보세요. 또한 도구 평가를 위한 /pricing 페이지도 참조하십시오.
Erlang은 실용적인 신뢰성 사고방식을 대중화했습니다: 구성 요소가 실패할 것을 가정하고 그럴 때 어떻게 대응할지를 설계하라는 것입니다.
모든 충돌을 막으려 하기보다 결함 격리, 빠른 탐지, 자동 복구에 집중합니다. 이런 접근은 채팅, 콜 라우팅, 알림, 조정 서비스 같은 실시간 플랫폼에 특히 잘 맞습니다.
이 글에서 말하는 “실시간”은 대개 **소프트 실시간(soft real-time)**을 뜻합니다:
마이크로초 단위의 엄격한 보장보다는 정체, 연쇄 장애, 큰 지연을 피하는 쪽에 가깝습니다.
동시성-by-default는 시스템을 적은 수의 거대한 부품이 아니라 많은 작고 고립된 워커들로 구조화한다는 뜻입니다.
각 워커는 세션, 디바이스, 호출, 재시도 루프 같은 좁은 책임을 맡아 확장과 실패 격리가 쉬워집니다.
라이트웨이트 프로세스는 대량으로 만들 수 있는 작은 독립 워커입니다.
실무적 이점은:
메시지 전달은 공유 상태 대신 메시지로 조정하는 방식입니다.
각 워커가 자신의 내부 상태를 소유하고 다른 워커는 메시지로만 요청을 보낼 수 있으므로, 경쟁 상태(race condition) 같은 동시성 버그 유형을 크게 줄여줍니다.
백프레셔는 워커가 처리할 수 있는 것보다 더 많은 메시지가 도착해 메일박스가 쌓일 때 발생합니다.
실무적 대응 방법:
“Let it crash”는 잘못된 상태에 빠진 워커가 애매하게 버티지 말고 빠르게 실패(fail fast) 하라는 전략입니다.
복구는 감독 구조가 책임지므로, 코드 경로는 단순해지고 가정 위반도 빨리 드러납니다—단, 재시작이 안전하고 빠를 때만 유효합니다.
감독 트리는 **슈퍼바이저(관리자)**가 워커를 감시하고 실패 시 재시작 규칙을 적용하는 계층 구조입니다.
이렇게 중앙에서 복구 정책을 정의하면 ad‑hoc 복구 로직이 여기저기 흩어지지 않고:
OTP는 Erlang 시스템을 장기간 운영 가능하게 만드는 표준 패턴(behaviours)과 관례들의 집합입니다.
일반적인 구성요소 예시:
gen_server: 상태를 유지하며 요청을 한 번에 처리하는 장기 워커supervisor: 실패한 워커를 규칙에 따라 재시작application: 서비스의 시작/종료와 릴리스 내 역할 정의공통된 라이프사이클과 템플릿 덕분에 새 엔지니어가 코드를 이해하기 쉽습니다.
Erlang을 쓰지 않더라도 동일한 원칙을 적용할 수 있습니다:
추가 자료는 /blog, 구현 세부사항은 /docs(및 도구 평가를 위한 /pricing)를 참고하세요.