# Action Cable과 WebSocket 완벽 가이드: 2026년 Ruby on Rails 기술 면접 대비 > Ruby on Rails Action Cable과 WebSocket 심층 분석. 커넥션, 채널, 브로드캐스팅, Rails 8 Solid Cable, Redis 스케일링, 테스트 패턴까지 기술 면접에서 자주 출제되는 핵심 내용을 코드 예제와 함께 정리합니다. - Published: 2026-05-07 - Updated: 2026-05-07 - Author: Anthony Fillion-Maillet - Tags: ruby on rails, action cable, websockets, real-time, solid cable, rails 8 - Reading time: 12 min --- Action Cable은 WebSocket 지원을 Rails 프레임워크에 직접 통합하는 기능으로, 채팅, 알림, 실시간 대시보드와 같은 실시간 기능을 외부 의존성 없이 구현할 수 있게 합니다. 2026년 기술 면접에서 Action Cable의 내부 동작 원리――커넥션 라이프사이클부터 프로덕션 스케일링까지――에 대한 깊은 이해는 뛰어난 지원자와 그렇지 않은 지원자를 구분하는 핵심 기준이 됩니다. > **면접 핵심 포인트** > > Action Cable은 컨트롤러와 모델에서 사용하는 것과 동일한 Rails 규약으로 WebSocket을 통합합니다. Rails 8에서 도입된 Solid Cable은 데이터베이스 기반 어댑터로 pub/sub 메시징에서 Redis 의존성을 제거합니다. ## WebSocket 프로토콜 기초와 Rails에서의 역할 WebSocket 프로토콜(RFC 6455)은 단일 TCP 연결을 통해 지속적인 전이중 통신 채널을 수립합니다. HTTP의 요청-응답 사이클과 달리, WebSocket은 연결을 유지하며 클라이언트와 서버 양쪽에서 언제든지 메시지를 전송할 수 있습니다. Action Cable은 이 프로토콜을 Rails 친화적인 추상화로 감싸줍니다. 서버는 `/cable` 경로에서 WebSocket 업그레이드를 처리하고, 커넥션 인증을 관리하며, 채널을 통해 메시지를 라우팅합니다. 클라이언트 측 JavaScript 라이브러리가 구독의 생성과 관리를 자동으로 수행합니다. 일반적인 HTTP 요청은 밀리초 단위로 완료되고 종료되지만, WebSocket 연결은 수 분, 수 시간, 또는 세션 전체 기간 동안 유지됩니다. 이 근본적인 차이가 Action Cable의 모든 아키텍처 결정을 좌우합니다. ## Action Cable 아키텍처: 커넥션, 채널, 서브스크립션 Action Cable은 세 가지 핵심 추상화로 구성된 계층 아키텍처를 따릅니다. 커넥션은 인증을 처리하고, 채널은 비즈니스 로직을 캡슐화하며, 서브스크립션은 컨슈머와 특정 채널을 연결합니다. ```ruby # app/channels/application_cable/connection.rb module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :current_user def connect self.current_user = find_verified_user end private def find_verified_user # Cookies are available in WebSocket handshake if verified_user = User.find_by(id: cookies.encrypted[:user_id]) verified_user else reject_unauthorized_connection end end end end ``` 커넥션 클래스는 WebSocket 핸드셰이크마다 한 번 실행됩니다. 인증은 여기서 수행되며, 개별 채널에서는 수행하지 않습니다. `identified_by` 선언은 사용자 ID를 등록하여, 해당 연결의 모든 채널 구독에서 사용할 수 있게 합니다. ```ruby # app/channels/chat_channel.rb class ChatChannel < ApplicationCable::Channel def subscribed room = ChatRoom.find(params[:room_id]) # Authorization: verify user belongs to this room if room.members.include?(current_user) stream_for room else reject end end def receive(data) message = ChatMessage.create!( room_id: params[:room_id], user: current_user, body: data["body"] ) ChatChannel.broadcast_to( message.room, { id: message.id, body: message.body, user: current_user.name } ) end def unsubscribed # Cleanup: mark user as offline AppearanceTracker.mark_offline(current_user) end end ``` 채널은 세 가지 라이프사이클 콜백을 정의합니다: `subscribed`, `receive`, `unsubscribed`. `stream_for` 메서드는 구독을 특정 모델 인스턴스에 바인딩하여 네임스페이스가 지정된 스트림을 생성합니다. 해당 스트림으로의 브로드캐스팅은 연결된 모든 구독자에게 메시지를 전달합니다. ## 클라이언트 측 JavaScript 구독 클라이언트 측 컨슈머는 Action Cable 서버에 연결하고 구독을 관리합니다. 각 구독은 서버 측 채널에 매핑됩니다. ```javascript // app/javascript/channels/chat_channel.js import consumer from "./consumer" const chatChannel = consumer.subscriptions.create( { channel: "ChatChannel", room_id: roomId }, { connected() { // Called when the subscription is ready console.log("Connected to chat room", roomId) }, disconnected() { // Called when the subscription is closed console.log("Disconnected from chat room") }, received(data) { // Called when data is broadcast to the channel const messageList = document.getElementById("messages") messageList.insertAdjacentHTML("beforeend", `
${data.user}: ${data.body}
` ) }, sendMessage(body) { // Calls ChatChannel#receive on the server this.perform("receive", { body: body }) } } ) ``` `received` 콜백은 서버가 구독 중인 스트림에 브로드캐스트할 때마다 실행됩니다. `perform` 메서드는 클라이언트에서 서버로 데이터를 전송하여 해당 채널 메서드를 호출합니다. ## Rails 8의 Solid Cable: 데이터베이스 기반 Pub/Sub Rails 8에서는 Solid Queue, Solid Cache와 함께 "Solid 삼총사"의 일원으로 Solid Cable이 도입되었습니다. Solid Cable은 메시지를 데이터베이스 테이블에 저장하여 pub/sub 백엔드로서의 Redis를 대체합니다. ```yaml # config/cable.yml (Rails 8 default) development: adapter: solid_cable production: adapter: solid_cable connects_to: database: writing: cable polling_interval: 0.1.seconds message_retention: 1.day ``` Solid Cable은 각 브로드캐스트 메시지를 데이터베이스 테이블에 기록하고, 설정 가능한 간격으로 새 메시지를 폴링하는 방식으로 동작합니다. 기본 폴링 간격인 100ms는 대부분의 애플리케이션에서 거의 실시간에 가까운 전달을 보장합니다. 트레이드오프는 명확합니다. Solid Cable은 인프라 의존성(Redis)을 제거하는 대신, 약간 더 높은 지연 시간과 데이터베이스 부하가 발생합니다. 이미 PostgreSQL이나 MySQL을 운영 중인 애플리케이션에서는 이 트레이드오프가 합리적인 선택인 경우가 많습니다. 초당 수천 건의 메시지가 발생하는 고빈도 브로드캐스팅에서는 여전히 Redis가 더 적합합니다. ```ruby # config/database.yml (Rails 8 multi-database) production: primary: <<: *default database: myapp_production cable: <<: *default database: myapp_cable_production migrations_paths: db/cable_migrate ``` Solid Cable은 전용 데이터베이스를 사용하여 프라이머리 데이터베이스와의 경합을 방지합니다. 이 분리를 통해 메시지 폴링이 애플리케이션 쿼리에 간섭하는 것을 막습니다. ## 브로드캐스팅 패턴과 서버 측 트리거 모델, 잡, 컨트롤러에서의 브로드캐스팅은 프로덕션 Rails 애플리케이션에서 가장 흔히 사용되는 세 가지 패턴입니다. ```ruby # Broadcasting from a model callback class Notification < ApplicationRecord belongs_to :user after_create_commit :broadcast_to_user private def broadcast_to_user ActionCable.server.broadcast( "notifications_#{user_id}", { id: id, title: title, read: false } ) end end # Broadcasting from a background job class DashboardUpdateJob < ApplicationJob queue_as :default def perform(dashboard_id) dashboard = Dashboard.find(dashboard_id) stats = dashboard.compute_stats ActionCable.server.broadcast( "dashboard_#{dashboard_id}", { stats: stats, updated_at: Time.current.iso8601 } ) end end ``` 모델 콜백은 레코드 변경에 의해 트리거되는 간단한 알림에 적합합니다. 백그라운드 잡은 대시보드 통계 계산이나 데이터 집계와 같은 무거운 처리를 요청 사이클을 차단하지 않고 수행합니다. 브로드캐스트 자체는 항상 가볍습니다. 페이로드를 직렬화하고 어댑터에 발행하는 것이 전부입니다. ## Turbo Streams와 Action Cable 통합 Rails 8의 Hotwire는 Action Cable을 Turbo Streams의 전송 레이어로 사용하여, 커스텀 JavaScript를 작성하지 않고도 실시간 DOM 업데이트를 구현합니다. ```ruby # app/models/message.rb class Message < ApplicationRecord belongs_to :chat_room # Automatically broadcasts append to subscribers broadcasts_to :chat_room end # app/views/chat_rooms/show.html.erb <%= turbo_stream_from @chat_room %>
<%= render @chat_room.messages %>
``` `broadcasts_to` 매크로는 after_create, after_update, after_destroy 콜백을 생성하여 Turbo Stream 프래그먼트를 Action Cable을 통해 브로드캐스트합니다. 클라이언트 측의 `turbo_stream_from` 헬퍼는 해당 스트림을 구독합니다. 새 메시지를 생성하면, 렌더링된 파셜이 연결된 모든 클라이언트의 DOM에 자동으로 추가됩니다. 이 패턴은 실시간 기능을 모델 선언과 뷰 헬퍼만으로 축소합니다. 커스텀 채널도, JavaScript 핸들러도, 수동 DOM 조작도 필요 없습니다. ## 프로덕션 환경에서의 Action Cable 스케일링 프로덕션 배포에서는 어댑터 선택, 연결 수 제한, 수평 스케일링에 대한 신중한 고려가 필요합니다. ```ruby # config/cable.yml — Redis adapter for high-scale production production: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> channel_prefix: myapp_production ``` Redis는 pub/sub의 백본으로 동작하여, 한 Rails 프로세스에서 발행된 메시지가 다른 프로세스에 연결된 구독자에게 확실히 전달되도록 합니다. Redis(또는 Solid Cable) 없이 async 어댑터를 사용하면 브로드캐스트가 단일 프로세스로 제한되어, 멀티 프로세스나 멀티 서버 배포에서는 사용할 수 없습니다. 연결 한도는 서버에 따라 달라집니다. Puma와 Action Cable은 HTTP 요청과 동일한 프로세스에서 WebSocket 연결을 처리합니다. 각 WebSocket은 지속적인 연결을 유지하며 스레드(또는 Ruby 3.x의 파이버 기반 스케줄러에서는 파이버)를 소비합니다. 5개 스레드와 4개 워커의 일반적인 Puma 구성에서는 포화 상태 전까지 약 200개의 동시 WebSocket 연결을 처리할 수 있습니다. 수천 개의 동시 연결이 필요한 애플리케이션에서는 [AnyCable](https://anycable.io/)이 Ruby WebSocket 서버를 Go 기반 서버로 대체하여, 프로토콜 수준에서 연결을 처리하면서 gRPC를 통해 채널 로직을 Rails로 라우팅합니다. 이 아키텍처는 노드당 10,000개 이상의 동시 연결을 지원합니다. > **면접 인사이트** > > 면접관은 Action Cable의 스케일링 한계에 대해 자주 질문합니다. 핵심 답변 포인트: Action Cable 자체가 병목이 아니라, WebSocket 연결을 처리하는 Ruby 프로세스가 병목입니다. AnyCable은 연결 관리를 Go로 오프로딩하면서 채널 로직은 Ruby에 유지하여 이 문제를 해결합니다. ## Action Cable 채널 테스트 Rails는 채널 단위 테스트를 위한 `ActionCable::Channel::TestCase`와 커넥션 인증 테스트를 위한 `ActionCable::Connection::TestCase`를 제공합니다. ```ruby # test/channels/chat_channel_test.rb require "test_helper" class ChatChannelTest < ActionCable::Channel::TestCase test "subscribes to a valid room" do room = chat_rooms(:general) user = users(:alice) stub_connection current_user: user subscribe room_id: room.id assert subscription.confirmed? assert_has_stream_for room end test "rejects subscription for unauthorized user" do room = chat_rooms(:private) user = users(:outsider) stub_connection current_user: user subscribe room_id: room.id assert subscription.rejected? end test "broadcasts messages to room subscribers" do room = chat_rooms(:general) stub_connection current_user: users(:alice) subscribe room_id: room.id assert_broadcast_on(room, hash_including(body: "Hello")) do perform :receive, body: "Hello" end end end ``` `stub_connection` 메서드는 실제 WebSocket 없이 커넥션 컨텍스트를 설정합니다. `assert_has_stream_for`는 스트림 바인딩을 검증합니다. `assert_broadcast_on`은 블록 내의 브로드캐스트를 캡처하여, 올바른 페이로드가 올바른 스트림에 도달하는지 확인합니다. ## Action Cable 기술 면접 빈출 질문 Action Cable 관련 기술 면접에서는 프로토콜에 대한 이해, 아키텍처 결정, 프로덕션 운영 과제가 주로 출제됩니다. **Action Cable은 어떻게 커넥션을 인증하는가?** 인증은 WebSocket 핸드셰이크 중 `ApplicationCable::Connection#connect`에서 수행됩니다. HTTP 세션의 쿠키는 이 시점에서 사용 가능합니다. 토큰 기반 인증에서는 WebSocket URL의 쿼리 파라미터로 토큰을 전달하고 `connect`에서 검증합니다. **WebSocket 연결이 끊어지면 어떻게 되는가?** 클라이언트 라이브러리는 지수 백오프를 적용한 자동 재연결을 구현합니다. 서버 측에서는 해당 연결이 구독하고 있던 각 채널에 대해 `unsubscribed`가 실행됩니다. 상태 변경 정리(사용자 오프라인 표시, 잠금 해제)는 `unsubscribed`에서 처리합니다. **Solid Cable과 Redis 중 언제 어떤 것을 사용해야 하는가?** Solid Cable은 중간 수준의 실시간 요구 사항(초당 100개 이하의 메시지)을 가지며 Redis 인프라를 피하고 싶은 애플리케이션에 적합합니다. 고처리량 시나리오나 10ms 미만의 전달 지연이 필요한 경우에는 Redis가 여전히 필요합니다. > **흔한 실수** > > 개별 채널이 아닌 커넥션 클래스에 인가 로직을 배치하는 실수가 발생합니다. 커넥션은 인증(이 사용자는 누구인가?)을 담당하고, 채널은 인가(이 사용자가 이 채팅방에 접근할 수 있는가?)를 담당합니다. 이 책임을 혼동하면 보안 취약점이 발생합니다. ## 결론 - Action Cable은 커넥션, 채널, 서브스크립션을 핵심 추상화로 하여 WebSocket 프로토콜을 Rails 규약에 통합한다 - 인증은 `ApplicationCable::Connection`에서, 인가는 개별 채널의 `subscribed` 콜백에서 수행한다 - Rails 8의 Solid Cable은 데이터베이스를 pub/sub에 활용하여 Redis 의존성을 제거한다. 중간 수준의 처리량을 가진 애플리케이션에 적합하다 - Turbo Streams는 Action Cable을 활용하여 최소한의 코드로 자동 실시간 DOM 업데이트를 구현한다 - 프로덕션 스케일링에는 멀티 프로세스 배포 시 Redis 또는 Solid Cable이 필요하다. AnyCable은 WebSocket 관리를 Go로 오프로딩하여 10,000개 이상의 연결을 처리한다 - 채널 테스트에는 `stub_connection`, `assert_has_stream_for`, `assert_broadcast_on`을 사용하며, 실제 WebSocket 연결은 불필요하다 - 이러한 개념을 확실히 다지기 위해 [Action Cable & WebSocket 면접 문제로 연습](/ko/technologies/ruby-on-rails/interview-questions/action-cable-websockets)하는 것이 효과적이다 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/ruby-on-rails/action-cable-websockets-complete-guide