[SERVER] - EducationClassProject : 실시간 채팅 기능 구현(WebSocket, STOMP)

2024. 9. 20. 20:54카테고리 없음

WebSocket + STOMP 프로토콜 사용

현재 개인 프로젝트로 진행하고있는 EducationClassProject는 학생들과 선생의 원활한 소통이 이루어져야하기에 실시간 채팅 기능을 도입하여 이 부분을 충족해야겠다고 생각하였습니다.

실시간 채팅은 Websocket과 STOMP 프로토콜을 사용하여서 구현하게 되었습니다.

 

RabbitMQ는 대규모의 트래픽을 처리하거나 내구성이 중요할 때, 특히 확장성과 안정성이 필요한 대규모 시스템에서 사용하기에 어느정도 고려를 해봤지만 아무래도 지금 프로젝트상 규모가 크진않고 실시간성이 더 중요하다고 판단되어 stomp 프로토콜을 사용하여 구현하게 되었습니다.

 

전체적인 채팅기능 모델 설계

  • 우선 전체 채팅방들의 목록들이 나오고 각각의 채팅방들옆에는 참여하기 버튼이 존재한다.
  • 선생의 역할이 있는 사용자는 채팅방을 생성 할 수 있다.
  • 만약 사용자가 해당 채팅방에 참여하고싶으면 참여하기 버튼을 클릭한다.
  • 비밀번호가 있는 방이라면 비밀번호를, 없는 방이라면 그냥 참여가 가능하다.
  • 그러면 사용자는 나의 채팅방목록에서 해당 채팅방( 내가 참여한 채팅방 )을 확인할 수 있다.
  • 사용자는 나의 채팅방 목록에서만 채팅방 접속이 가능하다. ( 전체 채팅방 목록 페이지에서는 참여하기만 가능 )

 

실시간 채팅의 전체적인 흐름

실시간 채팅의 전체적인 흐름을 살펴보면

clinet가 메세지를 구독합니다. ( client가 /topic/something에 대해 구독 요청을 보냅니다. )

그 후 server가 메세지를 broadcast 해줍니다. ( server는 /topic/something 경로로 메세지를 전송하고 해당 경로를 구독하고 있는 모든 클라이언트가 그 메세지를 수신하게 됩니다. )

 

메세지 전송 경로 설정 및 client 엔드포인트 등록

 

우선 config파일을 작성하여 client와 server간의 메세지 전송 경로를 정의해줍니다.

 

우선 클라이언트가 특정 주제를 구독할 수 있도록 브로커의 목적지를 정의해주었습니다. ( 저는 "/topic"으로 정의하였습니다. )

client측에서 만약 subscribe를 "/topic/something"으로 구독하고있다면 server는 해당 경로로 메세지를 전송할 수 있습니다.

 

다음으로는 client가 특정 목적지로 메세지를 보낼때 그 메세지가 어플리케이션으로 처리될 경로를 지정해주었습니다. ( /app으로 시작되는 경로는 모두 controller로 라우팅 됩니다. )

 

만약 client가 /app/something 경로로 메세지를 전송한다면 server는 해당 경로를 통해 @MessageMapping 어노테이션이 붙은 메서드가 해당 경로의 메세지를 처리해줍니다.

즉, /app으로 시작하는 경로는 server가 직접 처리하는 경로, /topic으로 시작되는 경로는 메세지 브로커가 client간에 메세지를 전달하는데 사용됩니다.

 

 

 

해당 client 코드를 보시면 구독을 하고 있는 것을 볼 수 있습니다.

우선 저는 client에서는 특정 경로를 지정해주지않았고, 전체 채팅의 성격을 띄도록 public으로 경로를 구독해주었습니다.

( 하지만 현재 서버에서는 채팅방 기능이 있기에 추후 수정해주겠습니다. )

 

다음으로 client가 websocket 연결을 맺을 수 있도록 엔드포인트를 등록해주었습니다. ( /ws로 지정해주었습니다. )

( 우선 테스트를 위해 모든 도메인에서 접근이 가능하도록 구현해주었는데 보안을 위해 다시 수정해주어야합니다. )

 

 

다음으로 컨트롤러에서 @MessageMapping 어노테이션을 통하여 client에서 보낸 메세지들을 처리해주었습니다.

 

메세지 경로 설정 문제 및 해결

 

client측에서 만약 subscribe를 "/topic/something"으로 구독하고있다면
server는 해당 경로로 메세지를 전송할 수 있습니다.

 

이때 저는 기존에 말씀드렸던 것처럼 해당 로직에 대해서 그럼 controller 단에서 경로 변수로 해당 경로를 입력받아서 메세지 브로커를 통해 메시지를 뿌려줘야겠다라고 생각하였습니다.

 

따라서 원래 @SendTo 어노테이션을 통해서 "/topic"경로에 뒤에 경로변수로 받아서 전달해주자라고 생각하였습니다.

 

하지만 해당 경로를 설정하는 어노테이션인 @SendTo는 동적인 경로를 설정해주지 못하였고, 그에 대해 어떻게 해결해 나아가야할지 고민하였습니다.

 

그러던 중 SimpMessagingTemplate를 사용하여서 client에서 메세지를 받을때 채팅방id도 같이 받은 후 해당 id를 통하여 경로를 설정해주어서 메세지를 뿌리자라고 생각하였습니다.

 

 

따라서 채팅 서비스의 유연성과 확장성이 넓어질 수 있었고, 해당 채팅방의 id를 추출하여 동적으로 경로를 설정하여 메세지를 뿌릴 수 있게 되었습니다.

 

실제 구현

우선 채팅방 엔티티와 채팅 엔티티를 구현해주었습니다.

 

다음으로 채팅방을 생성하는 기능과 채팅방을 입장하는 기능을 구현해주었습니다.

채팅방 생성시 비밀번호를 설정할 수 있고 만약 비밀번호가 설정된 비밀 채팅방이라면 입장시 비밀번호를 검증하게끔 구현하였습니다.

 

 

 

다음으로는 사용자가 채팅방을 입장하기 위해서는 전체 채팅방 내역을 보고 들어갈지 말지 판단을 해야하기에 전체 채팅방 내역 조회를 구현하였습니다.

 

 

또한 전체 채팅에서 사용자가 참여를 한 후 해당 참여를 한 채팅방을 따로 모아서 보여줘야하기에 ( 해당 채팅방을 보여주는 곳에서 사용자

가 채팅방 접속이 가능해야하기 때문 ) 사용자가 참여한 채팅방 내역 조회 기능을 구현하였습니다.

 

 

 

다음으로 사용자가 채팅방을 나가는 기능도 필요하다고 생각하여 채팅방 나가기 기능또한 구현하였습니다.

 

 

 

또한 저장된 메세지들에 대하여 client측에서 카카오톡 기능처럼 채팅방에 입장하게되면 채팅 내역이 다 보이도록 하기 위하여 채팅방별 채팅 내역 조회 api를 구현 해주었습니다. ( 채팅방별로 저장된 메세지가 조회되도록 구현하였습니다. )

 

 

이렇게 구현하게 된 이유는 우선 카카오톡처럼 기록이 저장되는 채팅방을 만들기 위해서는 메세지 저장은 필수라고 생각했습니다.

근데 만약 메세지 저장을 하게되는 로직이라면 굳이 실시간 채팅을 도입해야되나? 라는 질문을 스스로에게 던졌습니다. 

 

왜냐면 메세지를 저장하는 기능과 메세지를 조회하는 기능이있다면 client 측에서 debouncing이나 다른 기법으로 실시간 채팅과같은 기능을 흉내낼 수 있기에 이런식의 구현은 잘못되었나? 라고 생각하였습니다. ( 하지만 이 방식은 성능 상 매우 안좋아 사용자 경험을 해칠 것으로 판단하였습니다. )

 

그리하여 두 방식을 통합하여 진행하였습니다.

우선 사용자는 해당 채팅방에 들어가면 socket 연결을 통하여 실시간 메세지를 주고받습니다.

그럼 사용자가 느끼기에 debouncing 을 통한 조회보다 좀 더 빠르고 실시간적인 채팅을 할 수 있습니다.

 

그 후 사용자가 채팅방에서 벗어나게되면 socket 통신은 끊어지게되고 해당 실시간 채팅 내역은 데이터베이스에 저장됩니다.

그럼 사용자가 다시 채팅방에 들어온다면 들어올때만 채팅방별 메세지 기록을 조회하게 시켜준다면 사용자 입장에서는 채팅 기록이 남아있을 뿐만아니라 채팅의 속도도 빨라서 사용자 경험이 향상 될 것입니다.

그리하여 이 두가지 방식을 통합하여 구현하게 되었습니다.

 

실시간 채팅 기능을 구현하면서 구현한 부가 api들

채팅방 생성 api, 채팅방 입장 api, 채팅방 나가기 api, 채팅방 전체 조회 api, 내가 현재 참여중인 채팅방 조회 api