15시간을 컴퓨터 앞에서 불태웠다..
WebSocket을 다른 프로젝트로 분리하기
이번 포스팅에서는 내가 지난 몇 일간 뻘짓?과 엄청난 시간을 들여서 시도한 프로젝트에 대한 내용을 써볼까 한다. 아마 이번 게시물 시리즈는 나말고 다른 이들이 보면 이해를 못할 가능성이 크다. 왜냐면 내가 개인적으로 뻘짓한 것을 다음에 또 실수하지 않게 하려고 적은 것이기 때문이다. 그럼으로 괜한 시간 낭비를 안 하기 바란다.
머리가 나빠서 기록을 꼭 해 놔야 한다.
SpringBoot 프로젝트는 내가 이번 대학교를 다니면서 졸업작품을 했던 프로젝트를 대상으로 한다. 그래서 아마 이번 포스팅은 다른 사람들이 보는 데 이해가 가지 않을 수 있다.
사실 내 개인적인 기록용이라고 생각하면 될 것 같다.
먼저 내가 지난 몇 일간 한 일에 대해 말하기 앞서 먼저 사전 작업이 되어 있어야 한다.
해당 SpringBoot 앱은 아래 링크에서 찾을 수 있다.
이번 졸업작품으로 만든 얼굴 인식 기반 전자출입 명부이다. 상세한 내용은 설명하지 않는다.
그리고 이전 포스팅에서 설정한 것들이 있다 아래 링크를 참고한다.
추가로 Grafana를 연동시켜 놔야하는데 그것에 대한 포스팅은 아직 하지 않았다. 하지만 추후에 할 예정이다.
오늘 이야기하는 것은 위의 링크들에서 설치한 것들이 이미 설치되었다고 가정하고 이야기 한다.
사실 이번 포스팅을 쓰려고 생각하지 않았다. 왜냐면 SpringBoot 앱을 쿠버네티스 클러스터에 배포하는 것은 링크 여기서 다뤘기 때문이다.
물론 과정은 저 링크에서 소개하는 것과 같다. 하지만 문제가 생겼다. 애초에 나는 안정적인 서버 운영을 위해서 Replica를 여러 개 생성해 관리할 목적이였지만 WebSocket을 사용하는 내 앱이 Replica가 2개 이상이면 Lost Connection이 되면서 작동을 하지 않는 것이다. 내 SpringBoot 프로젝트는 첫 로그인 후에 화면이 WebSocket를 사용해 실시간으로 현황을 보여주는 데 거기서부터 막히는 것이다.
또 이상한 것은 pod를 하나만 배포했을 때는 작동을 하는 것이다. 최근 몇일 간 이것 때문에 스트레스를 너무 받았다. 어떻게 해야 할지 몰라서 구글링도 해보면서 여러 가설을 세우고 시도를 했다.
결론을 말하자면 내가 만든 SpringBoot 프로젝트의 구성에 문제가 있었다.
쿠버네티스에 웹 애플리케이션을 올린다는 것은 MicroService Web Application 구조로 올린다는 것과 같지만 내 프로젝트는 그 구조를 따르지 않았다. 한 프로젝트에 Compact하게 되어 있어서 각 기능들이 분리가 되어 있지 않다. 그것이 원인이였다.
MicroService 구조로 변환을 해야 했다. 여기서 완벽하게 MicroService 구조로 변환을 시키진 않을 것이다. 내가 기존 프로젝트에서 분리할 것은 딱 하나이다. 바로 실시간으로 데이터를 polling하는 부분이다. 이때 WebSocket를 사용하게 되는데 기존 내 SpringBoot 프로젝트의 WebSocket를 사용하는 구조가 매우 이상했다.
물론 내가 만들었다.. 일단 WebSocket이라는 것을 처음 써보기도 했고 SpringBoot도 처음 접하는 상태에서 만든 프로젝트라 많이 엉성하고 이상하다. (그래도 최대한 짜여진 틀에서 개발하려고 노력했다) 지금 생각해 보면 왜 이렇게 이상하게 코드를 짰는지… 많이 반성하고 돌아보게 된다. 아마 replica를 여러 개 만들어서 안 되는 이유는 나 때문일 것이다.
한번 간단하게 내 프로젝트에서 사용하는 WebSocket의 구조를 보자.
아래 사진에서 나오는 코드는 facilityStatus.js에서 나오는 코드의 일부분이다. 함수의 제목에 알맞게 해당 엔드포인트 /ws로 연결을 하는 것이다.
그리고 연결이 성공적이면 onConnected 함수를 호출하고 실패하면 onError를 호출한다.
아래 사진에서는 연결이 성공해서 호출되는 onConnected()함수이다. 여기서는 /topic/public를 구독을 하게 되고 이 특정 경로로부터 메시지가 올 경우 onMessageReceived 함수가 호출된다. 무엇보다 여기서 가장 이상한 점은 제일 아래 있는 sendMessage() 함수일 것이다. 이상한 게 맞다. 왜 이상한지 설명하겠다.
sendMessage() 함수에서는 해당 경로 /app/status.sendMessage로 메시지를 보내게 된다. 여기는 controller 패키지의 FacilityController.java에서 찾을 수 있다.
FacilityController.java
(...)
@MessageMapping("/status.sendMessage")
@SendTo("/topic/public")
public RealTimeStatusDTO sendMessage(@Payload RealTimeStatusDTO realTimeStatusDTO) {
RealTimeStatusDTO dto = statusService.getFacilityStatus();
return dto;
}
(...)
위 사진에서 주의 깊게 또 봐야할 것은 setTimeout 부분이다. 3초 후에 다시 sendMessage() 함수를 호출해서 반복하는 것을 볼 수 있다. 즉 3초마다 폴링하는 작업을 이렇게 구현한 것이다.
FacilityController.java는 해당 페이지에서 필요한 정보를 데이터베이스에서 가져와서 반환한다. 여기서 반환하는 dto는 다시 Message를 보내게 되는데 이는 /topic/public를 구독한 client에게 보내게 된다. 그럼 client는 아까 위해서 이야기했던 onMessageReceive 함수에서 저 리턴된 dto를 메시지로 받게 된다.
onMessageReceived 함수는 아래 사진과 같이 되어 있다. FacilityController.java에서 반환한 dto를 받아서 변수에 초기화 해주고 이로써 실시간으로 html에 있는 데이터가 실시간으로 변경되게 한 것이다.
최근 Vue를 공부하고 있는데 진짜 Vue의 양방향 데이터 바인딩이 그리워 지는 순간이다.
일단 현재 WebSocket를 사용해서 실시간으로 데이터를 페이지에 표시하는 방식이 매우 비효율적이다. 이를 바꿀 예정이다. 이 구조를 요약하면, js에서 더미 message를 보내고 그 message를 받은 백엔드에서 특정 함수를 호출하고 그 호출된 함수에서 데이터베이스에서 데이터를 조회한다음 다시 메세지를 보내는 방식이다. 다시 3초 후에 더미 message를 js 단에서 보내고 백엔드에서 받는 과정을 계속 반복한다. 나는 첫 과정을 생략하고 아예 백엔드 단에서 몇 초마다 함수를 호출되게 만들고 구독하는 client에게 데이터를 message로 보낼 것이다.
위의 구조로 쿠버네티스 클러스터에서 replica를 여러 개 생성한 다음 외부에서 접근할 수 있게 expose하고 접근하게 되면 아래 사진과 같은 에러가 발생한다.
위 사진은 파드가 2개 이상 replica가 있을 때 발생한다. 신기한 것은 이 에러가 발생할 수도 있고 발생하지 않을 수도 있다.
이게 무슨 말이냐면 웹소켓이 특정 pod에만 연결되는 것 같다. 왜 이런 생각을 하냐면 새로 고침을 계속하다면 웹 소켓이 작동할 때가 있는데 잘 작동하다가 다시 에러가 발생한다. 쿠버네티스는 요청을 부하분산하는 기능이 있는데 아마 WebSocket이 연결된 파드에 처음 내가 접근을 정상적으로 하다가 3초마다 메시지를 보내는 작업이 다른 파드로 연결되서 그런 것은 아닌가 생각된다. 그래서 메시지가 잘 보내지다가 중간에 WebSocket이 끊겨진다.
이를 고치기 위해 WebSocket 기능을 다른 프로젝트로 분리하고 이를 구독하는 Client에게 일정 간격으로 BroadCasting 하는 방식으로 바꾼다.
글이 너무 길어지는 것 같으니 다음 게시물에서 이어간다.
- Kubernetes (18) ,
- Docker (4) ,
- SpringBoot (45) ,
- Docker Hub (3) ,
- WebSocket (4) ,
- Prometheus (5) ,
- Grafana (2) ,
- Deployment (2) ,
- ReplicaSet (2)