WebSocket은 클라이언트와 서버 간의 양방향 통신을 가능하게 해주는 프로토콜로, 실시간 데이터 전송이 중요한 다양한 서비스에서 사용됩니다. 다음은 WebSocket이 주로 사용되는 예시입니다.
HTTP(HyperText Transfer Protocol)는 웹 상에서 클라이언트와 서버 간에 데이터를 주고받기 위해 사용되는 가장 기본적인 프로토콜입니다. HTTP는 요청과 응답을 주고받는 방식으로 동작하며, 주로 웹 페이지, 이미지, 비디오 등의 리소스를 전송하는 데 사용됩니다. 다음은 HTTP 사용 예시입니다.
아닙니다. HTTP에서도 실시간성을 보장하는 기법들이 존재합니다. Polling, Long Polling, Streaming 등이 그 예입니다. 예를 들어, 클라이언트가 서버로 지속적으로 요청을 보내거나, 한 번 요청을 보내고 커넥션을 끊지 않은 상태에서 서버로부터 메시지를 계속 받는 방식이 있습니다.
하지만, 웹소켓이 사용 가능한 환경이라면, 웹소켓을 이용해 실시간 서비스를 제공하는 것이 일반적입니다. 이제 웹소켓을 사용하는 이유와 HTTP와의 차이점을 자세히 알아보겠습니다
HTTP와 웹소켓의 가장 큰 차이점은 수립된 커넥션을 어떻게 하느냐 입니다.
HTTP는 비연결성 프로토콜로, 클라이언트가 요청을 보낼 때마다 연결을 맺고, 응답을 받은 후에는 연결을 끊습니다. 반면, 웹소켓은 한 번 연결을 맺으면 어느 한쪽에서 연결을 끊으라는 요청을 보내기 전까지 연결을 계속 유지합니다. 이렇게 하면 매번 연결할 때 발생하는 비용을 줄일 수 있습니다.
HTTP는 연결을 맺고 끊을 때 3-way, 4-way handshake를 거쳐야 하지만, 웹소켓은 이런 과정을 생략할 수 있습니다. 또 다른 차이점은 통신 방식입니다. HTTP는 요청과 응답이 한 쌍을 이루는 통신 방식으로, 원하는 결과를 얻기 위해서는 항상 서버에 요청을 해야 합니다. 반면, 웹소켓은 연결된 채널을 통해 상대가 보내는 메시지를 실시간으로 받을 수 있습니다.
이해를 돕기 위해서 비유를 들어보자면 HTTP는 마치 벽에 탁구공을 튕겨서 받는 것과 같다면, 웹소켓은 전화 연결과 비슷합니다. HTTP에서는 원하는 응답을 받기 위해 매번 요청을 보내야 하지만, 웹소켓에서는 한 번 연결을 수립한 후에는 메시지를 송수신할 수 있습니다.
개발자분들이 개발자 도구를 켜서 디버깅할때 많이 보셨을 화면입니다.
HTTP 프로토콜을 사용하면 매 요청마다 많은 양의 정보를 생성해 서버에 보내게 됩니다.
문제는 이러한 양의 정보들이 매 요청-응답 마다 계속 왔다갔다 해야한다는 것입니다.
실시간성을 요하는 서비스에서 많이 사용되는데 실시간성을 요한다는 것은 그만큼 http로 따지면 요청과 응답이 많다는거겠죠?
그때마다 이만큼의 데이터를 주고받는 것만해도 꽤나 부담이 될 수 있을 것 같아요.
물론 웹소켓도 처음 handshake를 할때는 http 프로토콜을 사용하기 때문에 아까와 유사한 양의 정보들을 주고받게 됩니다. 하지만 한번 연결이 수립되고 나면 이렇게 간단한 메시지만 오가는 것을 확인 할 수 있습니다.
통신이 한번 더 오간다고 하더라도 밑에 한줄밖에 추가되지 않는 것이죠. 굉장히 효율적이죠.
이런 것처럼 웹소켓을 사용하면 http에 비해 통신에 오가는 비용을 많이 줄일 수 있습니다.
그렇다면 이런 웹소켓이 모든 환경에서 사용할 수 있을까요?
제가 이것을 알아보기 위해서 Can I Use 사이트에서 지원 현황을 가져와봤는데요.
상당히 많은 브라우저에서 지원하는 것을 알 수가 있습니다.
하지만 그럼에도 빨간색으로 표시된 것처럼 모든 환경에서 사용할 수는 없습니다.
구버전의 Internet Explorer, Firefox, Safari에서는 지원하지 않습니다.
하지만 어떤 환경에서는 실시간성을 보장하고 어떠한 환경에서는 실시간성을 보장하지 않는다면 사용자 입장에서 문제가 될 수 있습니다. 그 때 사용할 수 있는것이 SockJS와 socket.io 라이브러리 입니다.
이 라이브러리를 사용하게 되면 웹소켓을 지원하지 않는 브라우저에서도 웹소켓을 사용하는 것과 같은 비슷한 기능을 제공할 수 있는데요. 아까 제가 http에서도 마치 웹소켓을 사용하는것처럼 보일수 있게 기법들이 존재한다고 했었잖아요? Polling이나 Streaming 같은 것들이요.
이 라이브러리들이 요청을 보낸 브라우저가 웹소켓을 지원하는지 확인해보고 그렇지 않은 경우 대안책으로 그 기법들을 대신 사용하는 거예요.
오른쪽의 표는 스프링이 SockJS를 지원하기 때문에 제가 SockJs의 표를 가져와봤는데요. 왼쪽부터 차례대로 브라우저별로 웹소켓 기술을 제공하고 있다면 웹소켓 기술을 사용하고 없다면 Streaming 그것도 없다면 Polling 방식을 사용해서 제공한다고 합니다. 개념에 대해서 알아봤으니까 이제 Spring에서 실제로 웹소켓을 사용하도록 제공하고 있는지를 알아볼거에요.
설정하는법은 매우 간단하죠
Spring에서 웹소켓을 설정하는 방법은 매우 간단합니다. @Configuration 클래스를 만들고, 필요한 클래스와 어노테이션, 예를 들어 WebSocketConfigurer와 @EnableWebSocket 어노테이션을 설정하면 됩니다. 이후 구체적인 설정을 추가해볼 것입니다.
우선 addHandler 메소드부터 볼게요. 스프링에서 웹 소켓을 사용하려면 클라이언트가 보내오는 통신을 처리할 핸들러가 필요합니다. 그래서 필요에 따라 구현한 핸들러를 웹소켓이 연결될 때 핸드쉐이크할 주소고아 함께 인자로 넣어주면 되는데요.
여기세어는 /user가 그 주소가 되겠네요. 그러면 그 핸들러를 조금 더 살펴볼게요
위의 코드는 예시로 작성한 간단한 웹소켓 핸들러입니다. 웹소켓 프로토콜은 기본적으로 텍스트와 바이너리 타입을 지원하는데, 필요한 경우 TextWebSocketHandler 또는 BinaryWebSocketHandler 클래스를 상속하고 구현하면 됩니다.
이때 오버라이드하는 메소드의 인자로 WebSocketSession을 받아옵니다. 이 객체는 HTTP의 세션과는 달리, 웹소켓 연결 시 생성된 연결 정보를 담고 있습니다. 핸들러에서 웹소켓 통신을 처리하기 위해 이 세션들을 컬렉션으로 관리하는 경우가 많습니다. 예를 들어, 연결된 세션들을 관리해 연결된 모든 클라이언트에게 메시지를 보낼 수 있습니다.
다시 설정으로 돌아와서 CORS입니다.
Spring에서 웹소켓을 사용할 때 기본적으로 same-origin만 허용하는 정책을 가지고 있습니다.
그래서 이부분에서도 setAllowedorigins와 같은 메소드로 CORS에 대한 설정을 할 수 있습니다.
다음은 SockJS설정인데요 제가 웹소켓을 지원하지 않는 브라우저 환경에서도 비슷한 경험을 제공하기 위해서 사용하는 것이 SockJS라이브러리라고하였는데요
이 withSockJS() 이한줄을 추가하는 것만으로도 SockJS라이브러리를 사용하도록 설정을 할 수있습니다.
지금까지 Spring에서 웹 소켓을 어떻게 지원하고 있는지 코드를 간단하게 살펴봤는데요.
제가 이 예시를 만들면서 SpringBoot를 사용해서 만들었습니다.
이 SpringBoot에서 WebSocket 의존성을 받아오니까 Spring Messaging이라는 것도 같이 딸려서 오더라구요.
근데 저는 분면이 구현할 때 Spring Messaging이라는 것은 본적이 없고, Spring WebScoket 만으로 구현했는데
그럼 이 Spring Messaging은 뭐길래 같이 딸려서오나 의문이 생기더라구요.
그래서 좀 알아봤는데 Spring-Messaging을 이해하고 사용하기 위해서는 우선 STOMP라는 프로토콜에 대해 이해해야합니다.
STOMP는 보시다시피 간단한 텍스트 기반 메시징 프로토콜인데요, 좀 더 풀어서 이야기하면 메시지 브로커라는 것을 화용하여 Pub-Sub(발행-구독)방식으로 클라이언트와 서버가 쉽게 메시지를 주고 받을 수 있도록 하는 프로토콜로 볼 수 있습니다.
여기서 몇가지 개념들이 좀 생소하게 느껴질수 있는데요.
저같은 경우 메시지 브로커와 pub-sub이 그랬습니다.
이에 대한 이해르 돕기 위해 최대한 쉽고 간단하게 설명해보자면
pub-sub은 일종의 메시징 패러다임으로 발신자가 어떠한 범주, 예를들어서 어떤 path나 경로와 같은 범주로 메시지를 발행하면 이 범주를 구독하고 있는 수신자들은 그 메시지를 받아볼 수 있는 방식이라고 보시면 좋을 것 같아요.
그리고 메시지 브로커는 발신자가 보낸 메시지들을 받아서 수산자들에게 전달해주는 어떤 것이라고 보시면 될 것 같습니다.
이 정도 설명으로는 이해가 어려울실 수 있겠지만 조금 이따 스프링에서 STOMP의 동작 흐름을 보면서 다시 설명을 해볼게요.
사실 STOMP는 웹소켓만을 위해서 만들어 진 것은 아니에요.
하지만 중요한 점은 웹소켓과 같은 몇몇 양방향 통신 프로토콜에서 함께 사용할 수 있다는 것. 그리고 Spring이 웹 소켓 위에 STOMP를 얹어 사용하는 방법을 지원한다는 것이죠.
여기까지 설명했으면 웹소켓만 사용하면 되지 왜 굳이 STOMP라는 서브프로토콜을 하나더 얹어서 사용하냐는 의문이 생길 수 있을겁니다.
그래서 왜 STOMP가 필요한지, 어떤 이점이 있는지 설명하겠습니다.
지금까지 알아본 것처럼 웹소켓은 텍스트와 바이너라 타입의 메시지를 양방향으로 주고 받을 수 있는 프로토콜입니다. 하지만 그 메시지를 어떤 형식으로 주고 받을지는 따로 정해진 것이 없죠.
그래도 웹 소켓만 사용한다고 하더라도 아까 제가 구현한 것처럼 간단한 애플리케이션 정도는 충분히 구현할 수 있습니다.
그러나, 프로젝트가 커지고 협업하는 사람들이 많아지면 어떨까요?
아마 클라이언트와 서버가 서로 어떤 형식으로 메시지를 주고 받을지, 메시지의 타입은 어떻게 명시할 것인지, 메시지의 본문과 설정 정보와 같은 데이터들은 서로 어떻게 구분할 것인지 등등 따로 정의해야 할 것이고 또 그것을 파싱하는 로직들도 따로 구현해야겠죠.
여기서 STOMP를 사용하면 형식을따로 고민할 필요도, 그것을 파싱하기 위한 코드를 따로 구현할 필요도 없습니다.
보시다시피 STOMP는 프레임이라고 해서 커맨드, 헤더, 바디로 이미 정의를 해두었거든요.
그래서 제가 실제로 통신이 오갈때 웹소켓만 사용하는 것과 STOMP를 사용했을 때의 메시지들을 제가 가져왔습니다. 웹소켓만 사용했을 때는 서버에서 보낸 날것의 메시지만 오가는 반면, STOMP를 사용하면 커맨드, 헤더, 바디의 형태로 메시지가 오가는 것을 볼 수 있습니다.
이따 코드로도 살펴볼테지만 프레임을 구축하거나 해석하는 어떠한 코드도 구현한 적이 없어요.
새로 구현할 필요없이 이런식으로 사용할 수 있어요
이제 본격적으로 코드를 살펴보기 전에 Spring이 STOMP를 사용하고 있을때의 동락흐름을 한 번 살펴볼게요.
왼쪽에 보시면 메시지를 보내고 싶어하는 발신자와 보고싶어하는 구독자들이 있어요.
발신자는 구독자들에게 메시지를 보내고 싶어하고 구독자들은 /topic이라는 경로를 구독하고 있다고 해볼게요.
이 상황에서 발신자는 바로 /topic을 destination 헤더로 넣어서 메시지를 송신할 수도 있겠지만, 서버 내에서의 어떤 처리 혹은 가공이 필요하다면 /app으로 메시지를 송신하게 됩니다.
그리고 서버가 일을 모두 마쳤으면 가공되거나 처리된 메시지를 /topic이라는 경로를 담아 다시 전송하게되면 메시지 브로커에게 전달되게 되고 메시지 브로커는 전달 받은 메시지들을 /topic을 구도하고 있는 구독자들에게 최종적으로 전달하게 되겠죠.
이런 흐름도 가능하지만 사실 서버의 어떤 처리나 메시지를 가공할 필요가 없다면 발신자가 바로 메시지 브로커를 통해 구독자들에게 보내는 것고 가능해요.
이제 코드를 살펴볼건데요
설정하는 것은 웹소켓만 사용했을때와 매우 유사하죠.
또 필요한 어노테이션과 인터페이스를 구현해주기만 하면 됩니다.
조금 더 자세하게 보시면 웹소켓만 사용할 때에 비해 설정할 것들이 조금 늘어났는데요.
우선 configureMessageBroker 메소드부터 보겠습니다.
STOMP에서는 메시지 브로커를 사용한다고 말씀드렸는데 그 메시지 브로커를 설정하는 부분입니다.
enableSimpleBroker라는 메소드는 스프링에서 제공하는 내장 브로커를 사용하겠다는 설정입니다.
파라미터로 받고 있는 값의 의미는 해당 값이 prefix로 붙은 메시지가 송신되었을때 그 메시지를 메시지 브로커가 처리하겠다는 의미입니다.
여기에서는 메시지가 /queue 혹은 /topic이 prefix로 앞에 붙은 경로로 메시지가 송신되었을때 심플 브로커가 그 메시지들을 받고 구독자들에게 전달해주겠죠.
참고사항으로 말씀드리면 일종의 컨벤션으로 /queue라는 prefix는 메시지가 일애일로 갈때,
그리고 /topic은 메시지가 일대다로 브로드캐스팅 될때 주로 사용한다고 합니다.
그 밑의 setApplicationEstinationPrefixes 메소드는 바로 브로커로 가는 경우가 아니라 아까 흐름에서 보신 것처럼 메시지에 어떤 처리나 가공이 필요한 경우 핸들러를 타게 할 수있다고 말씀드렸죠?
바로 그 핸들러로 메시지가 라우팅 되도록하는 설정입니다. 여기서는 메시지 앞에 /app이 붙어있는 경로로 발신되면 해당 경로를 처리하고 있는 핸들러로 전달 되겠네요.
그리고 그 핸들러는 이렇게 생겼네요. 아까 WebSocke을 사용했을때와 다르게 생겼습니다.
STOMP를 사용하게 되면 따로 상속을 받거나 할 필요없이 컨트롤러 어노테이션을 사용하여 익숙한 방식으로 사용할 수 있습니다.
MessageMapping 어노테이션ㅇ느 기존에 알던 requestMapping과 비슷한 역할을 한다고 보면 됩니다.
HTTP 요청이 들어왔을때 그 요청의 경로에 맞는 핸들러에게 처리를 위임하듯이, STOMP 웹소켓 통신을 통해 메시지가 들어왔을때도 메시지의 dstination 헤더와 MessageMapping에 설정된 경로가 일치하는 핸드러를 찾고 그 핸들러가 처리를 하게 됩니다.
여기에서는 아까 Configuration에서 설정해준 /app이라는 prefix와 합쳐져서 /app/hello라는 destination 헤더를 가진 메시지들이 이 핸들러를 거치게 되겠네요.
밑에 SenTo 어노테이션의 경우 핸들러에서 처리를 마친 후 반환 값을 /topic/greeting의 경로로 다시 메시지를 보내겠다는 거예요. 여기에서는 처리를 마치고 반환된 Greeting 객체를 /topic/greeting 경로로 다시 보내는것이니까 앞에 /topic이 붙었으니 Simple브로커로 전달 될거예요.
다시 설정으로 돌아와서 다음으로 registerStompEndpoints메소드입니다.
이 메소드는 아까 웹소켓 코드에서 봤던 addHandler 메소드와 굉장이 비슷해요.
인자로 들어가는 경로는 웹소켓의 /user 처럼 처음 웹소켓 핸드쉐이크를 위한 주소이고, 그리고 여기에서도 뒤에 cors 설정과 SockJS 설정을 그대로 해줄 수 있습니다.
조금 다른점이 있다면 여기서는 Handler를 설정해줄 필요가 없습니다.
STOMP를 사용하게 되면 아까 웹소켓만을 사용했을 때와는 다르게 하나의 연결 주소마다 핸들러 클래스를 구현하고 설정해줄 필요없이, 컨트롤러 방식으로 간편하게 사용하는 방식이기 때문이죠.
이것도 STOMP를 Spring에서 사용하는 장점이라고 볼 수 있겠네요.
참고자료 및 출처
https://www.youtube.com/watch?v=rvss-_t6gzg&list=LL&index=5&t=5s