brunch

You can make anything
by writing

C.S.Lewis

by 에디의 기술블로그 Jan 24. 2023

Spring WebSocket Ping/Pong

설연휴를 (허무하게) 보내는게 아쉬워서, 연휴 마지막날 짧은 글을 작성해서 공유한다. 주제는, Spring WebSocket Ping/Pong 이다. ping/pong 이 왜 필요하고, ping/pong 구현 방법에 대해서 간단하게 소개하겠다. 이번 글은 한번이라도 WebSocket 를 공부해본 개발자를 대상으로 작성한 글이며, 웹소켓을 전혀 모른다면 이 글을 이해하기 어렵다. 웹소켓을 먼저 이해한 후에, 이 글을 읽는 것을 추천한다. 또한 스프링 프레임워크에 관심이 전혀 없다면 이 글을 읽는 것을 추천하지 않는다. (허접하지만) 스프링에 관심있고, 웹소켓 개발이 처음인 주니어 개발자는, 필자의 예전 글을 읽어보고 오길 바란다.

https://brunch.co.kr/@springboot/695



클라이언트에서 명시적으로 종료한 경우


클라이언트에서 명시적으로 연결을 종료하면, 웹소켓 서버에서는 종료되는 순간 afterConnectionClosed 메서드에서 클라이언트 종료를 감지할 수 있다. 

클라이언트에서 연결을 끊어보자. 필자는 간단하게 PostMan 을 사용해서 테스트했다. 


연결 종료 시, 아래와 같이 afterConnectionClosed 메서드가 실행되는 것을 확인할 수 있다. 

code 는 1000 으로 종료되었다. 

참고로, 포스트맨이 아닌 브라우저에서 웹소켓 연결 중, 브라우저를 그냥 꺼버리면 1000이 아니라 1001 코드가 넘어올것이다. 어쨋든, 명시적으로 종료 처리를 한 경우에는 소켓 서버에서 클라이언트가 종료되었다는 것을 확실히 알수가 있다. 


하지만.......


네트워크 순단 현상에 의한 끊김


사용자가 명시적을 종료하는 경우가 아닌, 예상하지 못하게 네트워크가 끊긴 경우에도 위와 같이 서버에서 알 수 있을까? 네트워크 끊김 현상을 테스트하기 위해서 다른 PC 의 웹브라우저에서 소켓 서버에 접속해보자. 그리고 인터넷의 와이파이를 잠시 끄면 어떻게 될까? 클라이언트에서는 인터넷을 껐기 때문에 서버와 연결을 끊어진 상황이다. 하지만, 서버에서는 클라이언트에서 연결이 끊겼는지 실시간으로 감지할 수가 없다. 명시적으로 종료처리를 하지 않았기 때문에 서버에서는 끊겼는지 여부를 실시간으로 모르고 있는 상황이 된다. 


물론, 서버의 소켓 타임아웃 설정을 하거나, 소켓서버 앞단에 로드밸랜서(ALB 등)이 있다면 로드밸랜서의 idle 타임아웃 설정에 의해서 시간이 지나면 연결이 끊김을 알수는 있겠지만, 연결이 끊겼을때 빠르게 감지하긴 쉽지 않다. 참고로, Spring WebSocket 구현 시 isOpen 메서드를 제공하지만, isOpen메서드 만으로 클라이언트의 상태를 완벽히 확인하긴 어려운 것으로 보인다. 


오랫동안 고민했지만 결국 유일하게 찾은 방법은 ping/pong 을 주고 받는 방법밖에는 없는 것 같다. 



혹시, 

Ping/Pong 외에 괜찮은 방법을 아시는 분 계시면, 댓글로 알려주시면 너무 감사드리겠습니다.



Ping / Pong 에 의한 클라이언트 상태 확인


ping/pong 을 구현하는 방법은 매우 심플하다. 


- 서버에서 먼저 ping 을 보내고 클라이언트에서 pong 을 응답하는 방법

- 클라이언트에서 먼저 ping 을 보내고 서버에서 pong 을 응답하는 방법


이 글에서는 서버에서 ping 을 보내는 방식을 주로 설명하겠다. 


참고로, 클라이언트에서 먼저 ping을 보내면.. 서버에서는 pong 타입의 메시지를 클라이언트에 응답해주면 된다. 서버에서는 주기적으로 클라이언트가 ping 을 전송하는지 체크해야 한다. 만약, ping 메시지가 서버에서 설정한 시간동안 ping이 오지 않는다면 클라이언트 접속이 끊겼다고 판단할수 있다. 그럼, ping 이 얼마나 오랫동안 오지 않으면 끊겼다고 판단하면 될까? 정답이 없다. 서비스 도메인 지식에 따라서 결정해야 한다. 서버에서 클라이언트 연결을 (거의) 실시간으로 감지해야 한다면 매우 짧은 주기로 ping/pong 을 주고 받아야 한다. 지금 설명한 내용처럼 ping/pong 은 클라이언트 ping 을 보낼수도 있지만 반대로 서버에서 ping 을 보낼 수도 있다. 어디서 보내든, ping 을 받은 경우 바로 pong 을 응답해줘야 한다. 이 글에서는 서버에서 ping 을 보내는 방식을 주로 설명하겠다. 


서버에서 ping 을 보내는 경우

서버에서는 주기적으로 클라이언트 웹소켓 세션에 ping 메시지를 보낸다. 필자는 단순하게 Spring @Secheduled 어노테이션을 사용해봤다. (참고로, 실무에서는 이렇게 하지 않는다...필자는 실무에서 Secheduled 어노테이션을 선호하지 않는다. 1대의 인스턴스로 운영되는 시스템은 거의 없기 때문에 단일 인스턴스에서 주기적으로 실행되는 스케쥴 로직은 바람직하지 않는 경우가 많다.)

서버에서 클라이언트의 모든 세션에 1초에 한번씩 ping 메시지를 발송한다. TextMessage 클래스에 단순하게 ping이라는 스트링 메시지를 전송하였다. MessageDTO 클래스를 만들고 메시지 type 을 ping 으로 설정하는게 좀 더 간결할것이다. 암튼, 클라이언트에서는 아래와 같이 1초에 한번씩 메시지가 오고 있다. 

클라이언트에서는 ping 메시지를 받는 즉시, pong 메시지를 응답해줘야 한다. postman 으로 간단하게 pong 를 메시지를 응답하였다..


서버에서는, 클라이언트로부터 pong 메시지를 받으면 클라이언트의 접속 상태가 정상이라고 판단하면 된다. if 조건문에 pong 이 수신에 대한 로직을 작성하면 된다.


블로그에서 소개하진 않지만, 필자는 심플하게 WebSocketSession 의 만료시간을 별도로 저장하였다. pong 메시지를 받으면 만료시간을 갱신해주는 방법을 사용하였다. 만약 일정시간동안 pong 를 수신하지 못한다면 해당 세션의 만료시간이 갱신되지 못하게되며, 해당 세션은 삭제된다. 물론, 주기적으로 모든 세션의 만료시간을 체크해주는 로직 또한 구현해야 한다. 시간 관계상 상세한 내용은 생략하겠다... 


클라이언트에서 먼저 ping 을 보내는 경우

참고로 실무에서는, 서버에서 ping 을 보내지 않고, 클라이언트에서 먼저 ping 을 보내도록 구현하는 경우도 많다. 원리는 동일하다. 클라이언트가 ping 을 보낸 후, 서버에서 pong 을 응답해준다. 만약, 클라이언트에서 ping 요청이 일정시간동안 오지 않는다면 해당 클라이언트는 접속 상태가 비정상이라 판단한다. 자세한 내용은 생략한다. 


ping/pong 의 주기는 어떻게 설정할까? 

정답이 없다. 도메인 지식에 따라서 결정해야 한다.


Spring 구현체 사용해서 ping/pong 구현


위에서는 ping/pong 메시지를 TextMessage 에 정의해서 송수신을 하였고, 클라이언트에서도 pong 메시지를 직접 구현해야하는 불편함이 있었다. 하지만, Spring WebSocket 을 사용한다면, Spring 에서 제공하는 기능을 활용해서 좀 더 심플하게 ping/pong 을 구현할 수 있다. 



WebSocket Protocol 에서의 Ping,Pong

해당 문서에서 5.5.2 Ping, 5.5.3 Pong 을 읽어보자. 

https://www.rfc-editor.org/rfc/rfc6455#section-5.5.2



읽어보면 OPCODE 에 Ox9 라는 ping 이 스펙에 정의되어있는 것을 확인할 수 있다. 상세한 번역은 생략하겠다. 각자 읽어보길 바란다.


WebSocket OPCODE


WebSocket 프로토콜에 정의된 OPCODE 는 아래와 같다. 


continue = 0x0 

text = 0x1 

binary = 0x2 

close: 0x8 

Ping: 0x9 

Pong: 0xA



참고로, Spring WebSocket 의 org.apache.tomcat.websocket.Constants 클래스에서도 확인할 수 있다. 


Spring WebSocket Ping/Pong

그럼, Spring WebSocket 를 충분히 활용해서 Ping/Pong 을 좀 더 심플하게 사용해보자..


Spring WebSocket 에서는 PingMessage 라는 클래스를 제공한다. sendMessage 를 사용할 때 PingMessage 를 사용하면 된다. 

내부적으로는, WsRemoteEndpoingImplBase 라는 클래스의 sendPing 이라는 메서드를 실행할 것이다. 이때 OPCODE_PING 를 사용한다. 

서버에서 위와 같이 ping 메시지를 보낸 후, 클라이언트는 ping 메시지를 받으면 바로 pong 을 반환한다. 개발자가 직접 구현하지 않아도 된다. (혹시, 해당 내용에 대해서 상세히 알고 있는 개발자는 제보 부탁드립니다.. 웹소켓 기본 스펙에 ping 을 받으면 pong 을 바로 응답하도록 기본 셋팅 되어있는지???)


클라이언트에서 pong 메시지를 반환하면 서버에서는 handlePongMessage 핸들러가 실행된다. handlePongMessage를 재정의해서, 비즈니스 로직을 정의하면 된다. 

위 내용은 서버에서 ping 을 보내고, 클라이언트에서 pong 를 응답하는 로직이었다. 반대로 클라이언트에서 ping을 보내고 서버에서 pong 을 반환하는 방법도 가능할 것이다. 단, pong 메시지를 처리해주는 핸들러가 있는 반면에, ping 메시지를 처리하는 핸들러 메시지는 따로 없는 듯 하다. 굳이 WebSocket 프로토콜의 Ping 메시지를 처리하고 싶다면, handleMessage 메서드를 재정의하는 방법이 생각나긴 한다. 필자가 실무에서 이렇게해보진 않았다...


위와같이, 메서드를 살펴보면 pongMessage 만 처리해주는 듯 싶다.. 만약, 개발자가 직접 pingMessage를 핸들링 하고 싶다면 아래 코드와 같이 handleMessage 를 재정의하도록 하자. 단, 반드시 ping 을 받으면 pong 을 응답하도록 구현해야 한다! 


잘 모르겠다. 시간 관계 상 생략하겠다. 



글을 마무리하면서..


WebSocket 관련해서는 공유하고 싶은 내용이 많지만, 시간 관계상 이정도로 짧게 마무리하겠다. 사실 필자가, Redis Pub/Sub 을 연동해서 멀티 인스턴스 환경에서의 소켓 서버 시스템을 구축했는데, 해당 내용에 대해서 나중에 시간이 된다면 공유하면 좋을 듯 싶다. 



암튼, 웹소켓을 공부중인 분들에게 조금이나마 도움이 되었길 바라며 허접한 글을 마무리한다. 



레퍼런스


https://brunch.co.kr/@springboot/695

https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers

https://www.rfc-editor.org/rfc/rfc6455#section-5.5.2

https://brunch.co.kr/@springboot/374

매거진의 이전글 스프링부트 프로메테우스 모니터링
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari