[Spring & Dev] 웹소켓에 대하여.
0. 들어가며🏃🏻♂️
진행 중인 팀 프로젝트에서 실시간 채팅 기능을 구현하기 위해 웹소켓을 사용해보았습니다. 예전 수업 과제로 실시간 코인 거래소를 만든 경험이 있는데 이 때는 Polling을 사용했어서 이번에도 그러면 되겠다라고 생각하고 있던 도중 실시간 채팅에 대해 조금 검색해 보니 웹소켓이라는 키워드를 발견할 수 있었습니다.
이번 글에서는 HTTP로 실시간 서비스를 구현하는 방법에 대해 먼저 알아보고 이를 통해 웹소켓이라는 프로토콜이 왜 등장하게 되었는지에 대해 알아보도록 하겠습니다. 또한 웹소켓 프로토콜이 무엇인지, 동작 방식은 어떤지 등에 대해 알아보고 Spring에서 웹소켓 구현은 어떻게 하는지 간단하게 알아보도록 하겠습니다.
1. 실시간 서비스를 HTTP로 구현해보기🏀
0) 실시간 서비스⏰
실시간이라는 개념은 웹 환경에서 데이터들을 빠르게 노출하기 위해 필요합니다. 카카오톡과 같은 채팅 앱, 게임, 주식 거래 사이트 등이 실시간성을 보장해야 하는 서비스의 예시입니다. 먼저 우리가 익숙한 HTTP 프로토콜을 사용해 실시간성을 구현하는 방식에 대해 알아보도록 하겠습니다. HTTP 프로토콜에서는 서버가 요청 없이는 응답을 보낼 수 없기 때문에 실시간성을 확보하기가 어려운데 이를 어떻게 해결하는지 살펴보도록 하죠!
1) Polling🍏
HTTP 프로토콜을 사용해 실시간성을 구현하는 방법 중 하나는 Polling입니다. Polling은 클라이언트가 서버에게 주기적으로 요청을 보내면서 데이터를 받아오는 방식으로 매우 간단하죠. 사실 실시간성을 구현한다기보다는 "실시간성인척" 하는 방법이라고도 할 수 있습니다. 또한 간단한 만큼 오버헤드도 큰데요. 만약 서버에서 데이터 업데이트가 이루어지지 않았다면 쓸데없는 요청이 계속 생기는 것이고 이는 불필요한 트래픽을 계속 만들어내는 것이라고도 할 수 있습니다.
예전에 Polling 방식을 사용해서 실시간 코인 거래소를 구현해 보았었는데 1초마다 서버에 요청을 하여 데이터를 받아오고 이를 화면에 업데이트하는 방식이었습니다. 이런 식으로 실시간성을 구현하면 자신이 정한 주기(N초)마다 화면을 업데이트하는 "실시간인척" 하는 방식이고, 만약 서버의 데이터가 변경되지 않는다면 불필요한 요청을 N초마다 계속 발생시키는 것이므로 아쉬운 부분이 많이 생긴다는 것을 쉽게 알 수 있습니다.
2) Long Polling🍎
Polling 방식의 단점을 보완한 방식이 Long Polling입니다. 위 그림을 살펴보면 알 수 있듯 클라이언트가 서버에 요청을 보낸 뒤 서버에 업데이트가 있을 때까지 연결을 유지합니다. 그러다가 응답을 받게 되면 HTTP 연결을 끊고 다시 재요청을 보내게 되죠. 이런 방식을 사용하면 Polling 과는 달리 불필요한 요청 수를 많이 줄일 수 있고, 업데이트가 있을 때만 응답을 받을 수 있게 됩니다. 하지만 이러한 방식 역시 한계가 존재합니다. 만약 서버에 업데이트가 자주 발생한다면 매우 많은 트래픽이 발생할 수 있다는 것이죠. 또한 다수의 클라이언트가 서버로 접속을 시도한다면 서버의 부담이 커지게 됩니다.
3) Streaming🐟🐠🐡
Streaming 방식은 서버에 요청을 보내고 연결을 끊지 않은 상태에서 계속 데이터를 수신하는 방식입니다. Streaming 방식을 사용하면 Long Polling과 다르게 응답 후 연결을 끊지 않아 HTTP 요청에 의한 재연결 과정이 없어 서버에 부담이 덜어집니다. 하지만 이때 서버가 일방적으로 클라이언트에게 Response를 전달할 뿐 클라이언트에서 서버로의 데이터 송신은 어렵다는 단점과 구현이 Polling, Long Polling보다 더 복잡하다는 단점이 있습니다.
2. 웹소켓😀
위에서 살펴봤듯 HTTP 프로토콜을 사용해서는 실시간성을 구현하기 아쉬운 점들이 존재합니다. 또한 실시간성 서비스를 위해서는 빠르게, 많이 데이터를 주고받아야 하는데 아래 사진과 같이 HTTP 프로토콜은 주고받는 메시지의 크기가 큽니다. 이 때문에 실시간성 서비스에서 HTTP 프로토콜 사용이 더 아쉬워집니다.
1) 웹소켓이란❓
이런 상황에서 HTTP의 대안으로 웹소켓이라는 프로토콜이 등장합니다. 웹소켓은 클라이언트와 서버가 실시간으로 메시지를 주고받을 수 있는 full-duplex commnuication channel, 쉽게 말해 양방향 통신을 가능하게 해주는 프로토콜이며 TCP 위에 구축되어있다고 합니다. 데이터 통신을 동시에 처리할 수 있는 통신 방법이며 클라이언트와 서버가 서로 원할때 데이터를 주고 받을 수 있게 해주는 것이죠.
이러한 특성 때문에 HTTP는 벽에 치는 탁구로, 웹소켓은 전화하는 상황으로 많이 비유합니다.
2) 웹소켓 동작 방식🧑💻
웹소켓의 동작 방식은 위 그림과 같습니다. 이 부분은 자세한 자료들이 많이 있으므로 이해 돕기용 간단한 설명만 서술하겠습니다.
- 클라이언트가 HTTP/HTTPS 요청을 서버에게 보내 서버와 HandShake를 한다. (웹소켓 연결해 주세요!)
- 이때 클라이언트는 서버에게 웹소켓 연결 요청을 위해 몇 가지 필드를 추가하여 요청을 보냅니다.
- 서버는 웹소켓 연결이 가능하다면 (응 그래~) 응답을 보내줍니다.
- 이렇게 HandShake를 마치면 그때부터는 클라이언트와 서버 양측에서 데이터를 전송할 수 있게 됩니다.
- 이제부터 클라이언트와 서버는 서로 데이터를 전송하죠
- 데이터 왔다 갔다~
- 이때 데이터는 텍스트와 바이너리만을 보낼 수 있고 해당 데이터 해독은 애플리케이션에 맡기게 됩니다.
- 이로 인해 HTTP와는 달리 매우 작은 크기의 데이터를 가지고 통신할 수 있게 되는 것입니다!
- 그 후 한쪽의 Close 신호를 통해 웹소켓 연결을 끊게 됩니다.
3) 웹소켓 단점🥲
이 설명만 들으면 웹소켓은 좋은 통신 방법으로 보이는데 단점은 없는 걸까요?
웹소켓은 HTML5 이후부터 사용이 가능하여 브라우저별로 지원하지 않는 경우가 존재합니다. 하나 이를 보완하기 위해 SockJS나 Socket.io 와 같은 라이브러리가 존재하고 이러한 라이브러리를 사용하면 자바스크립트로 웹소켓을 쓰는 것처럼, 즉 실시간 통신을 하는 척할 수 있게 됩니다.
또한 HTTP는 형식을 정해두었기 때문에 이 규약만 따라서 통신을 하게 되면 누구나 해석이 가능합니다. 하지만 웹소켓은 형식이 정해져 있지 않아 주고받는 주체 간에 따로 약속을 한 게 아니라면 애플리케이션에서 쉽게 해석하기가 어렵습니다. 또한 웹소켓은 문자열을 주고받을 뿐 그 이상의 일은 해주지 않습니다.
이러한 단점들이 존재하여도 실시간 통신을 위해 웹소켓을 사용하는 것은 일반적인 것 같습니다!
3. STOMP (Simple Text Oriented Messaging Protocol)📮
위에서 언급한 웹소켓 단점 중 형식을 정해두지 않아서 서로 약속을 해야만 해석하기가 쉽다라는 내용이 있었습니다. 이를 해결하기 위해 STOMP라는 웹소켓의 서브 프로토콜이 존재하는데요. STOMP는 클라이언트와 서버가 서로 통신하는 데 있어 메시지 형식, 내용 등을 정의해 줍니다. 따라서 이를 사용하면 애플리케이션에서 웹소켓 메시지를 어떻게 해석해야 할지, 어떤 포맷으로 오는지 등에 대한 관리를 하지 않아도 되어 편리하죠. HTTP 형식과 닮았지만 훨씬 가벼운 내용을 주고받습니다.
또한 STOMP 프로토콜은 Publisher와 Subscriber를 지정하고, 메시지 브로커를 사용하여 Publisher에게 전달받은 메시지를 Subscriber로 전달해주는 구조를 가지고 있습니다.
이에 대해 잘 정리되어 있는 블로그가 있어 해당 내용을 좀 더 자세히 알고 싶으신 분들은 아래 링크를 참고해 주세요!
4. 스프링에서 웹소켓 + STOMP 사용 예시📦
웹소켓 + STOMP를 스프링에서 사용하기 위한 간단한 예시를 코드를 통해 살펴보도록 하겠습니다. 코틀린 코드이지만 간단한 코드이고 자바와 크게 다를 것이 없어서 읽으시는데 불편함은 없으실 것 같습니다!
@Configuration
@EnableWebSocketMessageBroker // 웹소켓 + STOMP를 사용
class WebSocketConfig: WebSocketMessageBrokerConfigurer {
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
// /ws 라는 endpoint로 웹소켓 연결 요청하세요!
registry
.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}
override fun configureMessageBroker(registry: MessageBrokerRegistry) {
// 내장 Message Broker 사용
// /sub 이라는 prefix가 붙은 메시지를 발행 시 브로커가 처리
registry.enableSimpleBroker("/sub")
// 메시지 핸들러로 라우팅되는 prefix 설정
registry.setApplicationDestinationPrefixes("/pub")
}
}
@RestController
class ChatController(
val messagingTemplate: SimpMessagingTemplate,
) {
// "/pub/message"로 오는 채팅을 처리하는 핸들러
@MessageMapping("/message")
fun handleMessage(message: ChatMessage) { // ChatMessage는 내가 받는 메시지 DTO
// /sub/room/roomUUID를 subscribe 하는 사람들에게 메시지 발행
// 이 부분은 변수가 없을 시 @SendTo 어노테이션으로 변경가능
messagingTemplate.convertAndSend("/sub/room/${message.roomUUID}", message)
}
}
5. 정리📚
HTTP 프로토콜과 웹소켓 프로토콜의 가장 큰 차이는 아래와 같습니다.
- 수립된 커넥션을 어떻게 하는가.
- HTTP는 비연결성 프로토콜로 클라이언트가 요청을 보낼 때마다 연결 맺고 응답받은 후 끊는다.
- 웹소켓은 한번 연결 시 한쪽에서 연결 끊으라 요청 보내기 전까지 연결을 유지한다.
- 통신을 하는 방식
- HTTP는 요청과 응답이 한쌍이다. ➡️ 벽에 치는 탁구로 비유 가능
- 웹소켓은 연결된 채널을 통해 상대가 보내는 메시지를 들을 수도, 내가 메시지를 보낼 수도 있다. ➡️ 전화로 비유 가능
- 보내는 메시지량
- HTTP의 경우 정해진 규격에 따라 보내다 보니 많은 내용의 메시지를 보낸다.
- 웹소켓의 경우 HTTP 메시지에 비해 간단한 메시지를 주고받을 수 있다.
- STOMP를 사용해 웹소켓도 메시지를 규격화할 수 있다.
6. 나가며💨
이번 글에서는 실시간 통신을 위해 HTTP 프로토콜에서는 어떤 방식을 취할 수 있는지, 이때 생기는 문제점이 무엇인지를 알아보았습니다. 또한 HTTP 프로토콜의 단점을 보완하기 위해 웹소켓 프로토콜을 사용하려 했고, 정의, 통신 방법, 예시 코드 등을 알아보았습니다.
프로젝트에서 채팅 기능을 구현하려다 보니 웹소켓에 대해 알게 되었습니다. 단순히 예시 코드를 붙여 넣어 기능을 완성하기보다는 웹소켓이 무엇인지, 왜 웹소켓을 써야 하는지에 대해 알아보는 시간을 가짐으로써 팀원들에게 좀 더 설득력 있게 내가 쓴 코드를 설명할 수 있었던 것 같습니다. 또한 우리 서비스에 맞는 요구사항 변경이 생겼을 때 배경지식을 기반으로 기능을 조금 더 유연하게 변경해볼 수 있었던 터라 깊게 공부하는 것에 중요성을 다시 한 번 느끼게 된 것 같습니다.