Stateful VS Stateless
REST API (Stateless 방식)
✔ 개념
- REST API는 Stateless(상태 비유지) 방식으로 동작합니다.
- 클라이언트 요청(Request)은 항상 독립적으로 처리되며, 서버는 클라이언트의 이전 요청 상태를 기억하지 않습니다.
- 각 요청은 필요한 모든 정보를 포함해야 하며, 서버는 이를 기반으로 응답을 반환합니다.
✔ 장점
✅ 확장성(Scalability) 높음
- 서버는 클라이언트의 상태를 유지하지 않으므로 로드 밸런싱 및 수평 확장이 용이합니다.
- 여러 서버에서 요청을 처리해도 문제가 없으며, 서버 간 상태 동기화가 필요하지 않습니다.
✅ 단순하고 유지보수 용이
- 상태를 관리할 필요가 없기 때문에 개발이 단순하고 유지보수가 쉬워집니다.
- API 요청이 독립적이므로 디버깅과 로깅이 용이합니다.
✅ 캐싱 활용 가능
- HTTP 기반이므로 응답을 캐싱하여 성능을 최적화할 수 있습니다.
- 클라이언트 측에서 캐시를 활용하면 서버 부하를 줄일 수 있습니다.
❌ 단점
⛔ 실시간성이 부족함
- 클라이언트가 변경 사항을 지속적으로 확인하려면 주기적인 폴링이 필요하여 비효율적입니다.
- WebSocket처럼 서버에서 즉시 데이터를 Push할 수 없습니다.
⛔ 매 요청마다 인증이 필요
- Stateless 방식이므로 요청마다 인증 정보를 포함해야 하며, 이를 처리하는 과정에서 성능 오버헤드가 발생할 수 있습니다.
- JWT, OAuth와 같은 인증 토큰을 사용하여 이 문제를 해결할 수 있지만, 여전히 매 요청마다 인증 검증이 필요합니다.
🔹 WebSocket (Stateful 방식)
✔ 개념
- WebSocket은 Stateful(상태 유지) 방식으로 동작하며, 클라이언트와 서버 간의 지속적인 연결(Persistent Connection)을 유지합니다.
- 한 번 연결되면, 양방향 통신(Bi-directional Communication)이 가능하며, 서버는 클라이언트의 상태를 기억하고 필요할 때 데이터를 Push할 수 있습니다.
✔ 장점
✅ 실시간 데이터 전송 최적화
- 서버는 클라이언트의 요청 없이도 데이터를 즉시 전송(Push)할 수 있어, 실시간 애플리케이션(예: 채팅, 주식 거래, 게임)에 적합합니다.
✅ 네트워크 오버헤드 감소
- REST API와 달리, 매 요청마다 새로운 연결을 생성하지 않으므로 네트워크 리소스 사용이 효율적입니다.
- 연결이 유지되므로 인증 과정을 한 번만 수행하면 됩니다.
✅ 양방향 통신 가능
- REST API는 클라이언트 → 서버 방향의 요청-응답 방식이지만, WebSocket은 서버 → 클라이언트도 가능하여 보다 유연한 데이터 흐름을 제공합니다.
❌ 단점
⛔ 확장성이 낮음
- 서버가 각 클라이언트와의 연결을 유지해야 하므로 많은 사용자가 접속할 경우 리소스 부담이 증가합니다.
- 로드 밸런싱이 어렵고, 클러스터링 시 세션 공유 또는 연결 유지 로직이 추가적으로 필요합니다.
⛔ 복잡한 상태 관리 필요
- 연결 상태를 유지해야 하므로, 클라이언트가 예기치 않게 연결 해제되거나 재접속할 경우 상태를 관리하는 추가적인 로직이 필요합니다.
- 예: 세션 유지, 연결 끊김 감지, 재연결 로직 구현.
⛔ 브라우저 및 네트워크 제한 사항 존재
- 방화벽, 프록시 등의 네트워크 환경에서 WebSocket 연결이 차단될 수 있습니다.
- HTTP 기반보다 보안 및 안정성을 고려한 추가적인 설정이 필요할 수 있습니다.
🔹 어떤 방식을 선택해야 할까?
✔ REST API가 적합한 경우
- 요청과 응답이 독립적인 경우 (예: CRUD API, 데이터 조회 서비스)
- 서버 확장성과 유지보수가 중요한 경우
- 네트워크 및 보안 이슈가 신경 쓰이는 경우
✔ WebSocket이 적합한 경우
- 실시간성이 중요한 경우 (예: 채팅, 스트리밍, 주식 데이터, 게임)
- 양방향 통신이 필요한 경우 (예: P2P 연결)
- 연결 유지가 가능하고 성능 최적화가 필요한 경우
Spring WebFlux의 org.springframework.web.reactive.socket
기반의 웹소켓과 전통적인 Spring MVC 기반 웹소켓을 비교해보겠습니다.
1. 아키텍처적 차이
비교 항목 | Spring WebFlux (org.springframework.web.reactive.socket ) | Spring MVC (전통적인 웹소켓) |
---|---|---|
스레드 모델 | Non-blocking, 이벤트 기반 (Netty, Undertow 등 지원) | Blocking, Thread-per-connection (Tomcat, Jetty, Undertow) |
리액티브 스트림 지원 | Flux<Message> , Mono<Message> 기반 | @MessageMapping 기반 |
동시성 처리 | 요청 처리에 많은 스레드를 사용하지 않음 (적은 리소스로 높은 성능) | 많은 동시 접속 시 스레드가 증가, 성능 저하 가능 |
서버 요구 사항 | Netty, Undertow, Jetty 등 리액티브 지원 서버 필요 | Tomcat, Jetty, Undertow 지원 |
부하 분산 | 가볍고 빠르며 부하를 효과적으로 처리 가능 | 높은 동접자 수 처리 시 성능 저하 가능 |
2. 성능 및 확장성
비교 항목 | Spring WebFlux (org.springframework.web.reactive.socket ) | Spring MVC (전통적인 웹소켓) |
---|---|---|
성능 | 대량의 동시 요청 처리에 유리 | 동시 연결이 많아지면 성능 저하 |
부하 분산 | 비동기 이벤트 기반 처리로 효율적 | 스레드 증가로 인한 오버헤드 발생 |
리소스 관리 | 적은 스레드로 많은 요청 처리 가능 | 많은 동시 요청 시 스레드 리소스 소모 증가 |
지연 처리 | 이벤트 기반으로 낮은 지연 시간 | 스레드가 많아지면 응답 지연 증가 |
- WebFlux 기반 웹소켓은 비동기 이벤트 기반 모델이기 때문에 최소한의 리소스로 높은 동시성을 처리할 수 있습니다.
- 반면 Spring MVC 웹소켓은 Blocking 모델로서 요청당 하나의 스레드를 할당해야 하므로 동접자가 많아질수록 성능 저하가 발생할 수 있습니다.
3. 프로그래밍 모델 차이
✅ Spring WebFlux 웹소켓 (비동기, 리액티브)
@Component class ReactiveWebSocketHandler : WebSocketHandler { override fun handle(session: WebSocketSession): Mono<Void> { val input = session.receive() .map { it.payloadAsText } .doOnNext { println("Received: $it") } val output = Flux.interval(Duration.ofSeconds(1)) .map { "Server response $it" } .map(session::textMessage) return session.send(output) } }
✅ Spring MVC 웹소켓 (동기, Blocking)
@Controller class TraditionalWebSocketHandler { @MessageMapping("/message") @SendTo("/topic/response") fun handleMessage(message: String): String { println("Received: $message") return "Server response: $message" } }
4. 사용 시 고려 사항
✅ WebFlux 웹소켓이 유리한 경우
- 대규모 동시 연결 처리 (ex. 실시간 채팅, 금융 데이터 스트리밍)
- 리소스가 제한적인 환경 (ex. 마이크로서비스)
- Reactive Stack을 기반으로 애플리케이션을 구축할 경우
- Netty 기반 서버를 사용할 때 최적화된 성능 제공
✅ Spring MVC 웹소켓이 유리한 경우
- 기존 MVC 프로젝트와의 통합이 필요할 경우
- 비교적 적은 연결 수를 처리하는 서비스 (ex. 내부 알림 시스템)
- 기존의
@Controller
기반 프로그래밍이 익숙한 경우 - 톰캣(Tomcat)을 사용해야 하는 경우 (WebFlux는 기본적으로 Netty를 사용)
5. 정리
비교 항목 | Spring WebFlux (org.springframework.web.reactive.socket ) | Spring MVC (전통적인 웹소켓) |
---|---|---|
비동기 지원 | ✅ 완전한 비동기 리액티브 모델 | 🚫 동기적 처리 |
성능 | ✅ 높은 동시성 처리 가능 | 🚫 동접자가 많으면 성능 저하 |
사용하기 쉬운가? | 🚫 다소 복잡한 코드 구조 | ✅ 전통적인 MVC 패턴과 유사 |
서버 호환성 | 🚫 Netty, Undertow 등 필요 | ✅ Tomcat, Jetty 지원 |
적용 사례 | ✅ 실시간 데이터 스트리밍, 채팅, 알림 | ✅ 단순한 웹소켓 기능 |
🔥 결론
- 대규모 동시 연결 및 성능이 중요한 서비스에서는
Spring WebFlux
의 비동기 리액티브 웹소켓이 적합합니다. - 기존 Spring MVC 기반 프로젝트와의 호환성이 중요하고, 동접자가 많지 않다면 전통적인
Spring MVC 웹소켓
이 더 쉬운 선택이 될 수 있습니다.
🚀 즉, 성능과 확장성이 중요한 경우 WebFlux, 익숙한 개발 방식과 기존 시스템 통합이 중요한 경우 MVC 웹소켓을 선택하는 것이 좋습니다.
🔹 액터 모델을 채택한 메시징/메신저 시스템
1. WhatsApp
- 기술 스택: Erlang 기반
- 특징:
- Erlang의 BEAM VM에서 동작하는 **Erlang OTP(Actor 기반 시스템)**을 활용
- 수백만 개의 동시 연결을 지원하는 강력한 메시징 처리 능력
- 고가용성을 위해 분산 시스템으로 설계됨
- 장점:
- 낮은 오버헤드와 높은 동시성을 지원
- 메시지 큐와 액터 기반 모델을 통해 안정적인 메시징 서비스 제공
2. Telegram
- 기술 스택: Erlang 및 C++
- 특징:
- 서버는 Erlang과 C++로 구축되었으며, 내부적으로 액터 모델을 활용하여 비동기 메시지 처리
- MTProto 프로토콜을 사용하여 효율적인 메시지 전송
- 분산된 데이터 센터 구조를 통해 성능 최적화
- 장점:
- 빠른 메시지 처리 및 낮은 지연 시간
- 확장성이 뛰어난 아키텍처
3. Microsoft Orleans
- 기술 스택: .NET 기반 액터 모델 프레임워크
- 특징:
- 마이크로소프트에서 개발한 가상 액터(Virtual Actor) 모델을 제공
- 내부적으로 Skype, Xbox, Teams의 메시징 시스템에 활용
- 분산 환경에서 동적인 액터 생성을 지원하여 대규모 메시징 시스템 구축 가능
- 장점:
- 비동기 메시지 처리를 최적화
- 클라우드 환경(Azure)에서 강력한 확장성 제공
4. Akka 기반 메시징 시스템
- 기술 스택: Scala, Java, Kotlin
- 특징:
- **Akka (현재 Apache Pekko)**를 활용하여 분산된 메시지 기반 시스템 구축
- Persistent Actor를 활용한 상태 유지형 메시징 가능
- Event Sourcing + CQRS 패턴을 적용할 수 있어 메시지 로그 기반의 안정적인 설계 가능
- 사례:
- LinkedIn: Akka 기반 메시징 시스템 사용
- Guardian 뉴스: Akka Streams 기반 스트리밍 서비스 구축
- Signal(부분적으로 Akka 사용)
5. RabbitMQ (Erlang 기반)
- 기술 스택: Erlang
- 특징:
- 메시지 브로커로 사용되지만, 내부적으로 Erlang의 액터 모델을 활용하여 높은 동시성 처리 가능
- AMQP 프로토콜을 사용하여 안정적인 메시지 큐 제공
- 장점:
- 메시징 시스템에 최적화
- 확장 가능하며, 분산 아키텍처에서 활용 가능
🚀 분석: 액터 모델과 웹소켓 연동의 장점
이 코드는 Akka (현재 Apache Pekko) 기반 액터 모델을 활용하여 **웹소켓을 관리하는 세션 관리자(SessionManagerActor)**를 구현한 것입니다.
액터 모델이 웹소켓과 연동될 때 가지는 장점은 다음과 같습니다.
🔹 1. 높은 동시성 및 확장성 (Scalability & Concurrency)
- 문제: 웹소켓 연결은 지속적인 세션을 유지해야 하며, 많은 사용자가 접속할 경우 서버 부담이 증가할 수 있음.
- 해결: 액터 모델을 사용하면 각 클라이언트의 세션을 독립적인 액터(Actor)로 관리할 수 있음.
- 효과:
- 액터는 경량 스레드로 동작하므로 수십만 개의 웹소켓 연결도 효율적으로 처리 가능
- 세션 관리를 비동기 메시징 기반으로 처리하여 블로킹을 방지
- 수평 확장(Scale-out)이 용이하여, 클러스터링(Akka Cluster)과 연계하면 더 많은 사용자를 처리 가능
💡 예제 코드 적용:
sessions: ConcurrentHashMap<String, WebSocketSession>
: 액터 내부에서 웹소켓 세션을 관리- 각 명령(
UserSessionCommand
)이 액터 간 메시지로 전달되어 병렬로 처리됨
🔹 2. 상태(State) 유지 및 장애 복구 (Fault Tolerance)
- 문제: 일반적인 REST API 기반의 웹소켓 관리 시스템에서는 클라이언트 상태를 별도로 관리해야 하며, 장애 발생 시 복구가 어렵다.
- 해결: 액터 모델을 활용하면 각 웹소켓 세션을 하나의 액터 단위로 관리하여 상태를 자연스럽게 유지할 수 있음.
- 효과:
- 액터 모델 자체가 상태 기반(Stateful) 아키텍처이므로, 세션과 구독 정보를 쉽게 유지
- 장애 발생 시 해당 액터만 복구하여 서비스 중단을 최소화
- Akka Persistence 적용 시 세션 및 구독 정보를 이벤트 소싱 기반으로 저장하여 장애 이후에도 빠른 복원 가능
💡 예제 코드 적용:
sessions.remove(command.session.id)
→ 연결이 끊어진 세션을 자동으로 정리topicSubscriptions.computeIfAbsent(command.topic) { mutableSetOf() }.add(command.sessionId)
→ 액터가 상태를 직접 유지하므로 상태 복원 가능
🔹 3. 비동기 이벤트 기반 처리 (Reactive & Event-driven)
- 문제: 웹소켓은 실시간 이벤트 처리가 핵심이지만, 기존 REST 기반 시스템에서는 동기적인 요청-응답 구조로 인해 이벤트 처리의 유연성이 떨어짐.
- 해결: 액터 모델은 **비동기 메시지 패싱(Asynchronous Message Passing)**을 기반으로 동작하며, 웹소켓과 자연스럽게 연계됨.
- 효과:
- 클라이언트가 특정 토픽을 구독(Subscribe)하면 액터가 이벤트를 관리하고 비동기적으로 메시지를 Push
- 대량의 클라이언트에게 동시에 이벤트를 전달 가능 (예: 실시간 채팅, 알림 시스템)
- REST API 기반의 Polling 없이 서버에서 직접 푸시(Push) 가능
💡 예제 코드 적용:
UserSessionCommand.SendMessageToTopic
→ 특정 토픽을 구독한 모든 세션에 비동기 푸시UserSessionCommand.SendMessageToSession
→ 특정 세션에 개별적으로 메시지 전송
🔹 4. 트래픽 부하 감소 (Efficient Resource Utilization)
- 문제: 대규모 웹소켓 연결을 유지하는 것은 서버 리소스 부담이 크며, 특히 HTTP Polling을 사용할 경우 네트워크 오버헤드가 증가함.
- 해결:
- 액터 모델은 자체적으로 비동기 메시지 큐 역할을 수행하여 네트워크와 CPU 부하를 최소화
- 액터 간 직접 메시지 전송이 가능하여 불필요한 HTTP 요청을 제거
- 효과:
- 네트워크 오버헤드 최소화 (웹소켓은 한 번 연결되면 지속 유지)
- 클라이언트가 필요할 때만 메시지를 푸시 받도록 최적화
- HTTP Polling과 비교하면 서버 부하가 현저히 줄어듦
💡 예제 코드 적용:
sendService.sendEventTextMessage
→ 웹소켓을 통해 바로 클라이언트에 메시지 전송topicSubscriptions
→ 불필요한 요청 없이 미리 등록된 구독자를 대상으로 메시지를 브로드캐스트
🔹 5. 확장성 있는 토픽 기반 메시징 시스템 (Scalable Pub/Sub Architecture)
- 문제: 단순한 클라이언트-서버 메시징 모델은 확장성이 떨어지며, 많은 클라이언트가 특정 이벤트를 구독할 경우 관리가 어려움.
- 해결:
- 액터 모델은 Publish-Subscribe 패턴을 자연스럽게 지원하며, 토픽(Topic) 기반 메시지 브로드캐스팅이 가능
- 추가적으로 Kafka, Redis Pub/Sub 등과 연계하면 더욱 확장 가능
- 효과:
- 특정 주제(Topic)에 대한 구독/해지를 쉽게 관리
- 대규모 실시간 방송 시스템(예: 주식 거래, 뉴스 알림, 스포츠 경기 알림)에 적합
💡 예제 코드 적용:
onSubscribeToTopic
→ 클라이언트가 특정 토픽을 구독하면 세션 ID를 저장onSendMessageToTopic
→ 해당 토픽을 구독한 모든 세션에 메시지를 전송