20주년을 맞이한 Rails 생태계에서, 단순한 도구 이상의 철학을 지닌 Rails의 핵심 구성 요소 중 하나인 Action Cable의 심층적인 이해는 필수적입니다. Action Cable은 '개념적 압축'을 통해 복잡한 웹소켓 통신을 추상화하여 개발자가 비즈니스 로직에 집중할 수 있도록 돕습니다. 그러나 엔지니어로서 도구의 잠재력을 최대한 활용하고 발생 가능한 문제에 효과적으로 대응하기 위해서는 그 '블랙박스' 내부를 들여다보는 것이 중요합니다. 본 발표는 Action Cable의 내부 구조, 작동 방식, 그리고 개발자들이 직면할 수 있는 주요 문제점들을 심층적으로 분석하고, 이에 대한 해결 전략을 제시합니다.
Action Cable의 핵심 추상화는 ‘채널(Channel)’로, 실시간 스트림 구독을 관리하며 웹소켓의 저수준 복잡성을 숨깁니다. 클라이언트 측에서는 Consumer
가 웹소켓 연결을 래핑하고, Subscription
객체가 서버로부터의 업데이트를 수신합니다. 단일 사용자 세션은 하나의 Consumer
를 통해 여러 Subscription
을 다중화하여 사용하며, 이론적으로 구독 수에는 제한이 없지만 실제 운영에서는 고려할 사항이 많습니다. 또한, Action Cable은 Monitor
를 통해 연결 활성 상태를 유지하는데, 서버는 3초마다 핑(ping) 메시지를 보내 연결 끊김을 감지합니다. 하지만 모바일 환경에서 이 핑 간격은 배터리 소모를 유발할 수 있으며, 현재로서는 전역적으로만 설정 가능하고 특정 연결에 대한 유연한 설정은 어렵다는 한계가 있습니다.
Action Cable은 단순히 프레임워크의 한 부분이 아니라 서버와 클라이언트 간의 통신 프로토콜입니다. 이는 사용자 정의 클라이언트나 서버를 구현할 수 있게 하지만, 현재 프로토콜은 세션/메시지 식별자 부재, perform
액션에 대한 응답(acknowledgement) 부족, 그리고 에러 처리 메커니즘의 부재와 같은 여러 한계점을 가지고 있습니다. 특히 에러 처리의 부족은 서버 측에서 명령 처리 실패 시 클라이언트가 이를 인지하지 못하고 무한히 재시도하는 문제를 야기할 수 있습니다. 이는 subscribe
와 unsubscribe
명령이 서버의 스레드 풀에서 동시적으로 처리될 때 발생하는 경쟁 조건(Race Condition)과 맞물려 더욱 심화됩니다.
Action Cable 서버의 내부를 살펴보면, 클라이언트와 서버 간의 웹소켓 연결이 rack.hijack
메커니즘을 통해 Action Cable 서버에 의해 관리됩니다. 모든 작업은 Concurrent Ruby
의 ThreadPoolExecutor
(기본 4개 스레드)에서 처리되며, 작업 큐는 무제한입니다. 각 작업은 Rails Executor
컨텍스트 내에서 실행되어 데이터베이스 연결 반환과 같은 정리 작업을 수행합니다. 중요한 점은 connection
객체는 웹소켓 생명주기 동안 유지되지만, 실행 컨텍스트는 매 상호작용마다 재설정된다는 것입니다. 이러한 스레드 풀 기반의 동시성 처리는 동일 클라이언트로부터의 명령 순서가 보장되지 않아 경쟁 조건 문제를 야기할 수 있습니다. 또한, Action Cable 스레드 풀의 크기를 고려하여 데이터베이스 연결 풀 크기를 적절히 설정하지 않으면 데이터베이스 수준에서 병목 현상이 발생할 수 있습니다.
브로드캐스팅 과정에서도 두 개의 스레드 풀이 관여하여 메시지 전달 순서를 보장하지 못합니다. Action Cable은 ‘최대 한 번 전달(at most once delivered)’을 보장하며, 클라이언트가 재연결 시 놓친 메시지를 요청할 수 있는 메커니즘은 없습니다.
마지막으로, Turbo Streams는 Action Cable을 래핑하는 또 다른 추상화로, HTML 요소를 통해 실시간 기능을 구현하게 합니다. 편리함에도 불구하고, turbo_stream_source
요소가 페이지에서 사라져도 Action Cable Consumer
연결이 유지될 수 있어 불필요한 서버 자원 소모를 유발할 수 있습니다. 특히 Turbo 내비게이션, 특히 turbo:preview
기능 사용 시 turbo_stream_source
요소의 빈번한 추가/제거는 subscribe
/unsubscribe
호출을 증가시켜 서버의 경쟁 조건을 악화시키는 주범이 됩니다.
이러한 모든 문제들은 서버 재시작 시 수많은 클라이언트가 동시에 재연결을 시도하고 구독을 재개하면서 서버에 엄청난 부하를 주는 ‘Connection Avalanche’ 현상으로 귀결됩니다. 이는 고전적인 ‘Thundering Herd’ 문제의 일종으로, 서버의 CPU 및 메모리 사용량을 급증시키고 전체 애플리케이션의 성능 저하를 초래합니다. 이를 방지하기 위한 전략으로는 클라이언트 재연결 시 무작위 지연 시간 추가 (Rails 7에서 개선), 구독 병합 및 채널 수 줄이기, subscribe
호출 속도 개선, subscribe
명령 직렬화 (AnyCable 클라이언트에서 구현), 그리고 웹소켓과 HTTP 트래픽을 별도의 클러스터에서 처리하는 등의 방법이 제시될 수 있습니다. AnyCable과 같은 대체 서버를 사용하는 것도 효과적인 해결책이 될 수 있습니다.
Action Cable은 Rails에서 실시간 기능을 구현하는 강력한 도구이지만, 그 내부의 복잡성과 특정 한계점을 명확히 이해하는 것이 중요합니다. 특히 'Connection Avalanche'와 같은 성능 문제는 연결 수나 인스턴스 크기에 관계없이 발생할 수 있으며, 이는 Action Cable의 내부 동시성 처리 방식과 스레드 풀의 특성에서 기인합니다. 발표에서 제시된 Action Cable 프로토콜의 한계점과 내부 구조에 대한 이해는 개발자들이 이러한 문제들을 사전에 인지하고, 재연결 로직 무작위화, 구독 명령 직렬화, 데이터베이스 연결 풀 관리 등 다양한 전략을 통해 시스템의 안정성과 성능을 최적화할 수 있는 기반을 제공합니다. 결국, Action Cable의 '블랙박스'를 열어 그 작동 원리를 파악하고, 발생 가능한 문제에 대해 능동적으로 대응하는 것이 성공적인 실시간 애플리케이션 개발의 핵심입니다.