프로세스 기반 동시성을 첫 원리부터 설명한다. BEAM이 무엇이 다른지, OTP가 어떻게 복원력을 코드로 인코딩하는지, 그리고 왜 업계가 계속 같은 아이디어를 재발명하는지 다룬다.
2026년 2월 22일 · Variant Systems
프로세스 기반 동시성에 대한 첫 원리 가이드 — BEAM이 무엇이 다른지, OTP가 어떻게 복원력을 코드로 인코딩하는지, 그리고 왜 모두가 계속 그걸 재발명하는지.
elixir beam otp concurrency actor-model erlang

몇 달마다 한 번씩, AI나 분산 시스템 분야에서 누군가가 동시적이고 상태를 가진 에이전트를 실행하기 위한 새로운 프레임워크를 발표하곤 합니다. 상태는 격리되어 있고. 메시지 패싱을 하고. 실패하면 재시작하는 슈퍼바이저가 있습니다. 그러면 BEAM 언어 커뮤니티는 그걸 보며 고개를 끄덕이고 다시 자기 일로 돌아갑니다.
이런 일이 계속 반복되는 이유는, 프로세스 기반 동시성이 정말 어려운 문제를 해결하기 때문이고, BEAM 가상 머신은 1986년부터 그 문제를 해결해 왔기 때문입니다. 라이브러리로가 아니라. 채택하는 패턴으로도 아니라. 런타임 자체로요.
Dillon Mulroy는 이를 직설적으로 말했습니다:

3만 명이 그걸 봤고, 많은 사람들이 그 감각을 그대로 느꼈습니다. 파이썬 AI 생태계는 서로 독립적으로 같은 아키텍처로 수렴하는 에이전트 프레임워크를 만들고 있습니다 — 격리된 프로세스, 메시지 패싱, 슈퍼비전 계층, 장애 복구. 이 패턴들이 우연히 OTP와 비슷한 것이 아닙니다. 문제 자체가 이런 형태를 요구하기 때문에 비슷한 겁니다.
이 글은 “왜 Erlang이 옳았는가”에 대한 뜨거운 한 줄 평이 아닙니다. 그 한 줄 평 아래에 있는 가이드입니다. 첫 원리에서 출발하겠습니다 — 동시성이 실제로 무엇을 의미하는지, 공유 상태가 왜 모든 것을 망가뜨리는지, 그리고 프로세스가 어떻게 판을 바꾸는지. 끝에 가면 OTP의 패턴이 왜 계속 재발명되는지, 그리고 BEAM 런타임이 다른 플랫폼들이 완전히 복제할 수 없는 방식으로 왜 그 패턴들을 작동하게 만드는지 이해하게 될 겁니다.
저희는 Elixir를 전문적으로 씁니다. 가장 큰 운영 시스템 — 헬스케어 SaaS 플랫폼 — 은 8만 줄 이상의 Elixir로 돌아가며 실시간 스케줄링, AI 기반 임상 문서화, 백그라운드 작업 오케스트레이션을 처리합니다. 저희에게 이건 이론이 아닙니다. 하지만 처음 접했을 때의 저희 자신에게 설명하듯이 풀어보겠습니다.
프로그램은 동시에 여러 일을 해야 합니다. 수천 개의 웹 요청을 동시에 처리해야 할 수도 있고, 각자 대화 상태를 유지하는 AI 에이전트를 실행해야 할 수도 있으며, 실시간 대시보드를 제공하면서 오디오 전사 처리를 해야 할 수도 있습니다.
하드웨어는 가능합니다. 현대 CPU는 여러 코어를 가지고 있죠. 문제는, 당신의 프로그래밍 모델이 그것들을 어떻게 쓰게 해주느냐입니다.
근본적인 접근은 두 가지가 있습니다.
락을 이용한 공유 상태. 여러 스레드가 같은 메모리에 접근합니다. 뮤텍스, 세마포어, 락으로 손상을 막습니다. 대부분의 언어가 이 모델입니다 — Java, C++, Go(고루틴이 있어도 공유 메모리가 기본 모델인 건 여전합니다), Python(GIL 때문에 더 나쁩니다), Rust(빌림 검사기가 더 안전하게 만들어 줍니다).
공유 상태의 문제는 “작동하지 않는다”가 아닙니다. “작동하다가 어느 순간부터 작동하지 않는다”입니다. 레이스 컨디션은 재현하기 가장 어렵고, 테스트하기 가장 어렵고, 추론하기 가장 어려운 버그입니다. 시스템이 더 동시적으로 될수록 락 경합이 점점 더 모든 것을 느리게 만듭니다. 그리고 공유 메모리의 단 하나의 손상된 조각이 전체 시스템으로 연쇄 전파될 수 있습니다.
메시지 패싱을 이용한 격리 상태. 각 동시 실행 단위가 자기 메모리를 가집니다. 통신은 메시지 전송만으로 합니다. 공유 메모리도 없고, 락도 없고, 레이스도 없습니다.
이것이 액터 모델입니다. Carl Hewitt가 1973년에 제안했고, Erlang은 1986년에 런타임으로 구현했습니다. 그리고 몇 년마다 업계는 다시 이를 발견합니다.
BEAM 프로그래머가 “프로세스”라고 말할 때, 운영체제 프로세스를 뜻하는 게 아닙니다. OS 프로세스는 무겁습니다 — 메모리 수 MB, 생성 비용이 크고, 컨텍스트 스위칭 비용도 큽니다. 스레드를 뜻하는 것도 아닙니다. 스레드는 메모리를 공유하므로 동기화가 필요하죠. 그린 스레드나 코루틴도 아닙니다. 더 가볍긴 하지만 보통 힙을 공유하고 진정한 격리를 제공하지 못합니다.
BEAM 프로세스는 다른 무언가입니다:
마지막 포인트가 소프트웨어를 바라보는 방식을 바꿉니다. 대부분의 언어에서는 프로그램의 한 부분에서 문제가 생기면 피해 범위를 예측하기 어렵습니다. 스레드에서 널 포인터가 나면 다른 스레드가 의존하는 공유 상태가 오염될 수 있습니다. Node.js에서 비동기 핸들러의 예외가 처리되지 않으면 전체 프로세스가 죽을 수 있습니다 — 모든 연결, 모든 사용자, 모든 것이요.
BEAM에서는 실패의 피해 범위가 정확히 한 프로세스입니다. 언제나.
# Spawn a process that will crash
spawn(fn ->
# This process does some work...
raise "something went wrong"
# This process dies. Nothing else is affected.
end)
# This code continues running, unaware and unharmed
IO.puts("Still here.")
이건 try/catch로 에러를 숨기는 게 아닙니다. 크래시한 프로세스는 사라졌습니다 — 메모리는 회수되고, 상태는 해제됩니다. 다른 모든 것은 계속 실행됩니다. 질문은 이것입니다: 누가 이를 알아차리고, 다음에 무엇이 일어나는가?
프로세스가 메모리를 공유하지 못한다면, 어떻게 통신할까요?
모든 BEAM 프로세스에는 메일박스가 있습니다 — 들어오는 메시지를 담는 큐입니다. 프로세스 식별자(PID)를 이용해 특정 프로세스에 메시지를 보냅니다. 메시지는 수신자의 메일박스에 복사됩니다. 송신자는 기다리지 않습니다(기본적으로 비동기입니다). 수신자는 준비가 되면 자기 메일박스에서 메시지를 처리합니다.
# Process A sends a message to Process B
send(process_b_pid, {:temperature_reading, 23.5, ~U[2026-02-22 10:00:00Z]})
# Process B receives it when ready
receive do
{:temperature_reading, temp, timestamp} ->
IO.puts("Got #{temp}°C at #{timestamp}")
end
몇 가지 주목할 점이 있습니다:
메시지는 공유가 아니라 복사됩니다. 메시지를 보낼 때 데이터는 수신자의 힙으로 복사됩니다. 비용이 들어 보일 수 있고, 아주 큰 메시지에서는 실제로 그럴 수 있습니다. 하지만 두 프로세스가 같은 데이터를 수정하는 상황이 원천적으로 불가능해집니다. 이 트레이드오프는 가치가 있습니다 — 기본값으로 정합성을 사는 겁니다.
receive의 패턴 매칭. receive 블록은 Elixir의 패턴 매칭으로 메일박스에서 선택적으로 메시지를 꺼냅니다. 매칭되지 않는 메시지는 나중을 위해 메일박스에 남습니다. 즉, 별도의 라우팅 로직 없이도 서로 다른 문맥에서 서로 다른 메시지 타입을 처리할 수 있습니다.
백프레셔가 내장되어 있습니다. 어떤 프로세스가 처리할 수 있는 속도보다 더 빠르게 메시지를 받으면 메일박스가 커집니다. 이건 보이고, 모니터링할 수 있습니다. 프로세스의 메일박스 길이를 검사하고, 알림을 걸고, 이를 바탕으로 아키텍처 결정을 내릴 수 있습니다. 스레드 기반 시스템에서는 과부하가 지연 증가, 데드락, OOM 크래시 같은 형태로 나타나는데, 이런 증상은 진단과 원인 귀속이 더 어렵습니다.
메시지 패싱 모델은 자연스러운 아키텍처를 만들어냅니다. 각 프로세스는 자기 상태를 가진 자급자족 단위로서 한 가지 일을 잘합니다. 프로세스는 메시지를 통해 시스템으로 조합됩니다 — 마치 마이크로서비스 같지만, 단일 런타임 안에서 네트워크 홉 대신 나노초 단위의 메시지 전달로요.
이건 BEAM 생태계에서 가장 오해받는 개념입니다.
“Let it crash”는 “에러를 무시하라”가 아닙니다. “엣지 케이스를 처리하지 말라”도 아닙니다. 의미는 이것입니다: 일을 하는 코드와 실패를 처리하는 코드를 분리하라.
대부분의 언어에서는 비즈니스 로직과 오류 복구가 뒤엉켜 있습니다:
def process_payment(order):
try:
customer = fetch_customer(order.customer_id)
except DatabaseError:
logger.error("DB failed fetching customer")
return retry_later(order)
except CustomerNotFound:
logger.error("Customer missing")
return mark_order_failed(order)
try:
charge = payment_gateway.charge(customer, order.total)
except PaymentDeclined:
notify_customer(customer, "Payment declined")
return mark_order_failed(order)
except GatewayTimeout:
logger.error("Payment gateway timeout")
return retry_later(order)
except RateLimitError:
sleep(1)
return process_payment(order) # retry
try:
send_confirmation(customer, charge)
except EmailError:
logger.warning("Confirmation email failed")
# Continue anyway? Or fail? Hard to decide here.
return mark_order_complete(order)
모든 함수 호출이 에러 처리로 감싸집니다. 해피 패스 — 실제 비즈니스 로직 — 는 방어 코드 아래에 묻힙니다. 그리고 새로운 실패 모드가 생길 때마다 분기 하나가 더 늘어납니다. 코드는 읽기 어렵고, 테스트하기 어렵고, 바꾸기 어렵게 됩니다.
BEAM에서는 해피 패스를 씁니다:
defmodule PaymentProcessor do
use GenServer
def handle_call({:process, order}, _from, state) do
customer = Customers.fetch!(order.customer_id)
charge = PaymentGateway.charge!(customer, order.total)
Notifications.send_confirmation!(customer, charge)
{:reply, :ok, state}
end
end
저 호출들 중 하나라도 실패하면, 프로세스는 크래시합니다. 그건 버그가 아니라 설계입니다. 슈퍼바이저(다음에 다룰 겁니다)가 이 프로세스를 지켜보고 있습니다. 슈퍼바이저는 크래시했을 때 무엇을 해야 하는지 압니다: 재시작할지, 작업을 재시도할지, 혹은 상위 슈퍼바이저로 에스컬레이션할지.
비즈니스 로직이 깔끔한 이유는, 오류 복구가 별도의 관심사로서 별도의 프로세스에서 처리되기 때문입니다. 이것은 무모해지자는 이야기가 아닙니다. 복구 로직을 있어야 할 곳에 두자는 겁니다 — 모든 함수에 뒤엉키게 하지 말고, 슈퍼비전 트리에 두자는 것이죠.
핵심 통찰은 이것입니다: 크래시한 프로세스는 상태를 잃지만, 시스템은 그것을 전제로 설계되었기에 괜찮습니다. 중요한 상태는 데이터베이스나 ETS 테이블에 둡니다. 프로세스 자체는 값싸고, 재시작해도 깨끗이 다시 시작할 만큼 충분히 무상태에 가깝고, 자신의 일에만 집중합니다.
슈퍼바이저는 다른 프로세스를 감시하고, 그들이 죽으면 반응하는 것만이 일인 프로세스입니다. 슈퍼바이저는 트리로 구성됩니다 — 슈퍼바이저가 다른 슈퍼바이저를 감독하여, 복구 전략의 계층을 만듭니다.
defmodule MyApp.Supervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(_opts) do
children = [
{PaymentProcessor, []},
{NotificationService, []},
{MetricsCollector, []}
]
# If any child crashes, restart only that child
Supervisor.init(children, strategy: :one_for_one)
end
end
:one_for_one 전략은 이렇게 말합니다: PaymentProcessor가 크래시하면 그것만 재시작하라. NotificationService와 MetricsCollector는 그대로 두어라. 다른 전략도 있습니다:
:one_for_all — 어떤 자식이 크래시하든 모든 자식을 재시작합니다. 자식들이 서로 의존적이라서 하나 없이 기능할 수 없을 때 사용합니다.:rest_for_one — 자식이 크래시하면 그 자식과, 그 뒤에 시작된 모든 자식을 재시작합니다. 뒤에 있는 자식들이 앞의 자식들에 의존할 때 사용합니다.슈퍼바이저는 강도 제한(intensity limit)도 강제합니다. 예를 들어 “이 자식을 5초 안에 최대 3번까지 재시작하고 — 그 이후에도 계속 크래시하면 이 서브트리 전체를 종료한 다음, 내 부모 슈퍼바이저가 무엇을 할지 결정하게 하라”라고 할 수 있습니다. 이렇게 하면 크래시 루프가 자원을 무한히 소모하는 것을 막습니다.
# Restart up to 3 times in 5 seconds, then give up
Supervisor.init(children, strategy: :one_for_one, max_restarts: 3, max_seconds: 5)
슈퍼비전 트리는 단순한 에러 처리 메커니즘이 아닙니다. 애플리케이션의 아키텍처 다이어그램입니다. 잘 구조화된 Elixir 애플리케이션을 보면, 슈퍼비전 트리가 다음을 말해줍니다:
대부분의 코드베이스에서는 이런 정보가 문서(있다면)나 시니어 엔지니어의 머릿속에 있습니다. OTP 애플리케이션에서는 코드 자체에 인코딩되어 있습니다.
OTP는 Open Telecom Platform의 약자입니다 — Ericsson 기원에서 온 이름인데 이제 아무도 문자 그대로 받아들이진 않습니다. OTP의 실체는 이것입니다: 동시성 시스템을 만들기 위한 검증된 패턴들의 집합.
가장 중요한 것들:
Elixir 애플리케이션의 대부분의 프로세스는 GenServer입니다. GenServer는 다음을 수행하는 프로세스입니다:
defmodule SessionStore do
use GenServer
# Client API
def start_link(user_id) do
GenServer.start_link(__MODULE__, %{user_id: user_id, messages: []})
end
def add_message(pid, message) do
GenServer.cast(pid, {:add_message, message})
end
def get_history(pid) do
GenServer.call(pid, :get_history)
end
# Server callbacks
def init(state), do: {:ok, state}
def handle_cast({:add_message, message}, state) do
{:noreply, %{state | messages: [message | state.messages]}}
end
def handle_call(:get_history, _from, state) do
{:reply, Enum.reverse(state.messages), state}
end
end
이건 대화 기록을 보유하는 프로세스입니다. 사용자 세션마다 하나씩 스폰할 수 있습니다. 각각은 격리되어 있습니다 — 자기 메모리, 자기 메일박스, 자기 라이프사이클. 동시 사용자 1,000명은 곧 이런 프로세스 1,000개를 의미하며, 각 프로세스는 상태가 차지하는 만큼을 더해 대략 2KB 정도를 소비합니다. 나머지는 스케줄러가 처리합니다.
전형적인 접근과 비교해 보세요: 모든 요청 핸들러가 읽고 쓰는 공유 데이터 구조(Redis, DB 테이블, 또는 인메모리 맵). 물론 동작합니다. 하지만 이제 캐시 무효화, 쓰기 시 레이스 컨디션, 상태 저장소 연결 풀링, 저장소가 다운되면 어떻게 되는지 등을 생각해야 합니다.
GenServer에서는 상태가 곧 프로세스입니다. 관리해야 할 외부 저장소가 없습니다. 무효화할 캐시가 없습니다. 프로세스가 자기 상태의 단일 진실 공급원(single source of truth)입니다.
OTP Application은 하나의 단위로 시작하고 멈출 수 있는 컴포넌트입니다. 자체 슈퍼비전 트리, 자체 설정, 자체 라이프사이클을 가집니다. 당신의 Elixir 프로젝트 자체가 Application이며, 다른 Application(Phoenix, Ecto, Oban 등)에 의존합니다.
애플리케이션이 시작되면, 슈퍼비전 트리가 루트에서부터 시작됩니다. 모든 프로세스는 관리됩니다. 공중에 떠 있는 것이 없습니다 — 모든 프로세스는 감독되고, 모든 슈퍼바이저도 감독되며, 애플리케이션 루트까지 올라갑니다.
이는 대부분의 웹 프레임워크가 서버를 시작한 뒤 import 시점, 모듈 로드 시점, 초기화 시점에 각종 일이 암묵적으로 벌어져 추론하기 어려운 것과 대조적입니다. OTP에서는 시작 순서가 명시적이고 계층적입니다.
다른 언어들도 라이브러리로 액터 모델을 구현할 수는 있습니다. Akka는 JVM에서 이를 합니다. Python에서도 asyncio를 엄격히 쓰면 근사할 수 있습니다. 하지만 BEAM에는 VM 자체를 바꾸지 않고는 복제할 수 없는 런타임 레벨의 속성이 있습니다.
BEAM 스케줄러는 각 프로세스의 리덕션(대략 함수 호출)을 셉니다. 대략 4,000 리덕션 이후에는 해당 프로세스를 선점하고 다음 프로세스로 전환합니다. 프로세스는 거부할 수 없습니다. 협력적으로 yield할 필요도 없습니다.
즉, 어떤 프로세스도 시스템을 굶길 수 없습니다. 한 프로세스가 무한 루프에 빠지거나, 비싼 계산을 하거나, 느린 작업에서 블록되어도, 다른 모든 프로세스는 정상적으로 계속 실행됩니다.
Node.js는 이를 할 수 없습니다. 이벤트 루프는 협력적입니다 — 어떤 콜백이 CPU를 500ms 잡아먹으면 그 500ms 동안 다른 어떤 것도 실행되지 않습니다. asyncio를 쓰는 Python도 같은 한계가 있습니다. Go는 더 낫습니다(고루틴은 Go 1.14부터 선점형 스케줄링이 됩니다). 하지만 고루틴은 메모리를 공유하므로, 격리가 해결하는 문제 범주가 다시 들어옵니다.
각 BEAM 프로세스는 자기 힙과 자기 가비지 컬렉터를 가집니다. 어떤 프로세스의 힙이 GC를 필요로 할 때, 그 프로세스만 멈춥니다. 다른 모든 프로세스는 계속 실행됩니다.
이 차이는 매우 큽니다. JVM, Go, Python, Node.js에서는 가비지 컬렉션이 시스템 전체 이벤트입니다. GC 중단이 짧을 수는 있지만(Go의 GC는 훌륭합니다), 실행 중인 모든 작업에 영향을 줍니다. 수천 개의 동시 연결을 처리하는 시스템에서는 10ms의 중단도 모든 연결에 영향을 미칩니다.
BEAM에서는 어떤 프로세스의 GC 중단은 정확히 하나의 연결, 하나의 세션, 하나의 에이전트에만 영향을 줍니다. 그리고 프로세스가 작기 때문에(기억하세요, 약 2KB), 개별 GC 이벤트도 매우 작습니다.
선점형 스케줄링과 프로세스별 GC의 조합은 BEAM에 독특한 특성을 줍니다: 소프트 실시간 보장. 하드 실시간은 아닙니다 — RTOS가 아닙니다. 하지만 수천 개의 동시 작업 전반에 걸쳐 일관되고 예측 가능한 지연을 제공합니다.
이것이 WhatsApp이 Erlang으로 서버 한 대에서 200만 연결을 처리했던 이유입니다. Discord가 Elixir로 수백만 동시 사용자를 다루는 이유입니다. 통신 교환기(원래의 사용처)가 이 수준의 신뢰성을 요구하는 이유입니다. 그리고 BEAM이 수천 개의 동시 에이전트가 반응성 있고 격리된 실행을 필요로 하는 AI 에이전트 시스템에 자연스럽게 잘 맞는 이유입니다.
실행 중인 BEAM 시스템을 멈추지 않고 새 코드를 배포할 수 있습니다. 실행 중인 프로세스는 새 함수 호출을 하기 전까지는 기존 코드를 계속 실행하고, 새 호출 시점에 투명하게 새 버전으로 전환합니다. WebSocket이 끊기지 않습니다. 에이전트 세션도 떨어지지 않습니다. 다운타임도 없습니다.
이건 이론이 아닙니다. Ericsson은 전화 교환기가 배포 때문에 내려가면 안 되기 때문에 이를 만들었습니다. 실무에서는 대부분의 Elixir 팀이 롤링 배포를 사용합니다. 하지만 이 기능은 런타임에 존재하며, 연결 연속성이 중요한 시스템 — 장시간 실행되는 AI 에이전트 세션, 실시간 협업 도구, 금융 시스템 — 에서는 진짜 차별점입니다.
이 패턴들은 학문적이지 않습니다. 지금도 운영 시스템에서 돌아가고 있습니다.
Phoenix는 HTTP 및 WebSocket 연결을 BEAM 프로세스로 처리합니다. 각 연결은 격리됩니다. Phoenix Channels는 단일 서버에서 100,000+ 동시 WebSocket 연결을 처리하는 일이 흔합니다. LiveView(서버 렌더링 기반 인터랙티브 UI)는 연결된 사용자마다 상태를 가진 프로세스를 유지합니다. 그 프로세스가 UI 상태를 보유하고, 이벤트를 처리하며, 업데이트를 푸시합니다. 어떤 사용자의 LiveView 프로세스가 크래시하면 그 사용자만 재연결을 보게 됩니다. 다른 누구도 영향을 받지 않습니다.
Elixir의 대표 백그라운드 잡 라이브러리인 Oban은 잡을 감독되는 프로세스로 실행합니다. 실패한 잡은 슈퍼바이저가 재시도합니다. 잡 큐는 프로세스 메일박스를 통해 백프레셔를 가집니다. 스케줄된 작업은 OTP 타이머를 사용합니다. 전체가 슈퍼비전 트리입니다.
지금 모두가 연결 짓고 있는 부분입니다. AI 에이전트는:
이는 BEAM 프로세스에 그대로 대응됩니다. 에이전트 세션마다 프로세스 하나. 상태는 프로세스에 존재. 실패는 프로세스 크래시 — 슈퍼바이저가 재시작. 수천 개의 동시 에이전트는, 수백만 개를 처리하도록 만들어진 VM에서 2KB짜리 프로세스 수천 개일 뿐입니다.
파이썬 생태계는 asyncio, Pydantic 상태 모델, try/except 체인, 커스텀 재시도 로직으로 이를 만들고 있습니다. 돌아가긴 합니다 — 상당한 엔지니어링 노력과 함께요. 하지만 결과물은, 그것을 위해 설계되지 않은 런타임 위에서 사용자 공간(userspace)으로 구현한 액터 모델입니다. BEAM은 이를 VM 레벨에서 제공하며, 나중에 덧붙일 수 없는 보장까지 함께 줍니다.
George Guimarães는 대응 관계를 정확히 매핑했습니다: 격리된 상태는 프로세스, 에이전트 간 통신은 메시지 패싱, 오케스트레이션은 슈퍼비전 트리, 장애 복구는 슈퍼바이저, 에이전트 발견은 프로세스 레지스트리, 이벤트 분산은 프로세스 그룹. 1990년대부터 런타임에 내장되어 있던 것들입니다.
이를 활용하려는 Elixir 네이티브 AI 도구도 등장하고 있습니다: 에이전트 워크플로우를 위한 Jido, 슈퍼비전 트리 안에서 트랜스포머 모델을 실행하는 Bumblebee, 그리고 제어 가능한 에이전트 파이프라인을 위한 스텝 모드 실행을 제공하는 LangChain 바인딩.
전통적인 웹 프레임워크(Rails, Django, Express)는 수 밀리초 안에 끝나는 요청에 최적화되어 있습니다. AI 에이전트 상호작용은 5~30초가 걸립니다 — LLM 호출만 해도 몇 초가 걸리고, 에이전트는 여러 호출을 연쇄할 수 있습니다.
대부분의 웹 서버는 이를 위해 만들어지지 않았습니다. 요청당 스레드(thread-per-request) 모델에서 30초 요청이 많아지면 처리량을 유지하려면 훨씬 더 많은 스레드가 필요합니다. 연결 풀은 빠르게 고갈됩니다. 타임아웃은 연쇄적으로 터집니다.
BEAM은 전화 통화를 위해 설계되었습니다 — 원래의 장수 연결입니다. 전화 통화는 상태를 보유하고, 몇 분 동안 지속되며, 시스템은 이를 수백만 개 동시 처리합니다. “전화 통화”를 “AI 에이전트 세션”으로 바꾸면 아키텍처는 동일합니다.
트레이드오프에 대해 솔직해집시다.
다른 언어가 노력으로 할 수 있는 것:
규율 있는 팀이라면 적절한 라이브러리와 아키텍처로 Python, TypeScript, Go에서 BEAM이 제공하는 것의 70% 정도는 얻을 수 있습니다. 많은 애플리케이션에는 그 정도로 충분합니다.
BEAM 런타임이 필요로 하는 것:
이건 런타임에 “추가”할 수 있는 기능이 아닙니다. 런타임이 어떻게 만들어졌는지의 속성입니다. JVM은 메모리 모델을 근본적으로 바꾸지 않고서는 프로세스별 GC를 추가할 수 없습니다. Node.js는 이벤트 루프를 교체하지 않고서는 선점형 스케줄링을 추가할 수 없습니다. Python은 GIL을 없애려면… 뭐, 그들은 그걸 하고 있긴 합니다.
BEAM이 덜 잘하는 것:
이 패턴이 반복되는 건 문제가 반복해서 나타나기 때문입니다.
1990년대에 Java의 스레딩 모델은 동시 컴퓨팅의 해답이어야 했습니다. 충분하지 않았습니다. Akka가 2009년에 JVM에 액터 모델을 가져왔습니다.
2010년대에 Node.js는 이벤트 루프와 단일 스레드 async에 베팅했습니다. I/O 바운드 웹 서버에는 잘 먹혔습니다. 하지만 CPU 바운드 작업이나 진정한 병렬성에는 먹히지 않았습니다. 워커 스레드가 덧붙여졌죠. 그래도 격리된 상태 기반 동시성에는 충분하지 않았습니다.
2020년대에는 AI 에이전트 프레임워크가 격리되고, 감독되며, 동시적이고, 상태를 가진 프로세스를 필요로 합니다. AutoGen은 스스로를 “이벤트 드리븐 액터 프레임워크”라고 설명합니다. LangGraph는 공유 리듀서를 가진 상태 머신을 만듭니다. CrewAI는 작업 출력들을 체이닝합니다. 각자 OTP처럼 보이는 무언가를 향해 가고 있습니다 — 하지만 이를 위해 설계되지 않은 런타임 위에서요.
1986년의 Erlang 통찰은 이것이었습니다: 동시적이고 장애 허용적인 시스템에는 격리가 “사후적으로 추가되는 것”이 아니라 “기초 속성”으로 필요하다는 것. 공유 메모리 모델에 격리를 덧붙이려는 모든 런타임은 결국 더 복잡하고, 덜 신뢰할 수 있고, 처음부터 격리를 기본값으로 둔 시스템보다 추론하기 어려운 시스템으로 귀결됩니다.
BEAM만이 동시 시스템을 만드는 유일한 방법은 아닙니다. 하지만 가장 일관성 있는 방법입니다. 런타임, 언어, 패턴, 철학이 모두 같은 목표를 향해 정렬되어 있습니다. 업계가 계속 독립적으로 같은 아키텍처에 도달한다면, 그건 우연이 아닙니다. 올바른 해법으로의 수렴입니다.
저희는 헬스케어 플랫폼부터 실시간 인프라까지 Elixir와 BEAM 위에서 운영 시스템을 구축합니다. 프로젝트에서 Elixir를 평가 중이거나, 기존 BEAM 코드베이스에 도움이 필요하다면 연락 주세요.
Variant Systems
Build. Fix. Audit.
Company
Explore
ServicesTechnologiesIndustries
Connect
© 2026 Variant Systems. All rights reserved.