[Spring] 실시간 채팅 기능 회고 - 2

일상 회고록·2024년 1월 15일
1

브릿지 프로젝트

목록 보기
2/4
post-thumbnail
post-custom-banner

안녕하세요!

이번 회고의 메인은 WetSocket + STOMP를 사용하며 발생한 트러블슈팅새로운 기술인 Kafka의 도입입니다.

바로 시작하도록 하겠습니다!

0. 사전 설명

채팅 회고 - 1 에서 설명했던 것처럼 WebSocket + STOMP 을 이용해 뚝딱뚝딱 채팅 기능을 구현하였습니다.

동작 플로우는 대략 이렇습니다.

  • 채팅하기 → 채팅방 생성(고유 ID 생성) → 웹소켓 연결 → 채팅방 구독 → 메세지 전송 → 채팅

0-1. ChannelInterceptor

프로젝트의 요구사항인 채팅 메세지의 읽음 / 안읽음 구현을 위해 채팅방에 현재 몇명이 접속 중인지 파악해야 했습니다.

  • 읽음 / 안읽음 처리를 위해서 현 채팅방에 접속한 인원이 몇명인지 판단 필요
    • Case1. 한 명(상대방)이 접속해 있는 경우 → 접속 시 기존의 쌓인 상대방 메세지 읽음 처리 + 메세지를 보낸 즉시 읽음 처리
    • Case2. 자신만 접속해 있는 경우 → 기존의 쌓인 상대방 메세지를 읽음 처리 + 새로 보낸 메세지는 안읽음 처리

Spring에서 제공하는 ChannelInterceptor 를 통해 MessageChannel로 보내지는 메세지(프레임)을 가로채 SUBSCRIBE / UNSUBSCRIBE 여부로 접속 인원을 카운트 했습니다.

ChannelInterceptor (Spring Framework 6.1.2 API)

위 코드처럼 MessageChannel로 메세지(프레임)가 도착하면 가로채어 원하는 동작을 할 수 있도록 인터셉터를 설정해주고,

preSend 메소드를 통해 도착한 메세지를 가로채 StompHeaderAccessor 로 감싸준 뒤,

Stomp의 Command 에 따라 적절한 동작을 하도록 했습니다.

1. 이슈 발생

문제는 예상하지 못했던 곳에서 발생했는데요,,

읽음 / 안읽음의 두가지 케이스 중, 상대방이 먼저 접속해 있는 경우 접속하게 되면 기존의 메세지 데이터가 읽음 처리됨과 동시에 상대방이 보는 채팅방 또한 안읽음 → 읽음으로 변경되어야 합니다.

카톡을 떠올려본다면 내가 채팅방에 있을 때 상대방이 들어오면 ‘1’ 표시가 바로 없어지는 것을 알 수 있죠.

즉, 포인트는 서버에 저장된 메세지의 상태 업데이트 뿐 아니라, 업데이트 된 메세지들을 클라이언트에게 재전달해야 했습니다.

1-1. 요구사항 구현

저는 ChannelInterceptor를 통해 채팅방 인원을 카운트하고 있었고, 이를통해 데이터를 업데이트 하는 로직을 서비스단에 구현한 상태였습니다.

문제는 변경된 메세지들을 다시 클라이언트에게 전송하는 것이었는데요.

채팅 메세지를 보내는 경우,

Controller 단에서 SimpMessagingTemplateconvertAndSendToUser 메소드를 통해 클라이언트에서 보낸 메세지를 서버에 저장 후, 다시 구독한 채팅방으로 메세지를 보내고 있었습니다.

(잘 이해가 안되신다면 Stomp을 공부하는걸 추천합니다.)

아! 그럼 서버에서 클라이언트로 메세지(프레임)를 보내고 싶을 때는 convertAndSendToUser를 사용하면 되는구나~ 하고

업데이트된 메세지들을 재전송하는 메소드를 서비스단에 구현한 뒤, 동일하게 convertAndSendToUser 메소드를 사용하기 위해 Service 단에도 SimpMessagingTemplate을 주입 해주었습니다. (무지성 주입 멈춰,,)


흐뭇한 미소를 띄며 실행해본 결과

이러한 끔찍한 에러를 던지며 서버가 강제 종료되었습니다,,


1-2. 에러의 정체

해당 에러는 SpringBoot 2.6.x 버전부터, 순환 참조를 default로 금지함으로서 발생되는 순환 참조 에러메시지 였습니다.

Circular References Prohibited by Default

Circular references between beans are now prohibited by default. If your application fails to start due to a BeanCurrentlyInCreationException you are strongly encouraged to update your configuration to break the dependency cycle. If you are unable to do so, circular references can be allowed again by setting spring.main.allow-circular-references to true, or using the new setter methods on SpringApplication and SpringApplicationBuilder This will restore 2.5’s behaviour and automatically attempt to break the dependency cycle.

무엇인지 한 번 파헤쳐보도록 하겠습니다.

먼저 스프링 @Bean 에 관한 사전지식이 필요합니다.

스프링의 경우 싱글톤 컨테이너를 통해 객체 인스턴스들을 싱글톤으로 관리합니다.

CBLIB를 이용하여 @Configuration이 붙은 클래스를 상속하는 임의의 클래스를 생성 후, Bean으로 등록합니다.

이후, 등록된 클래스 내의 @Bean 메소드들은 싱글톤 컨테이너에 중복되지 않게 Bean으로 저장되어 싱글톤을 보장받습니다.

위 지식을 바탕으로 순환 참조 에러를 살펴보면,


  1. WebSocketConfig 를 Bean으로 등록하려면 channelInBoundInterceptor Bean이 필요
  2. ChannelInBoundInterceptor 를 Bean으로 등록하려면 chatService Bean이 필요
  3. ChatService 를 Bean으로 등록하려면 simpMessagingTemplate Bean이 필요
  4. SimpMessagingTemplate을 Bean 으로 등록하려면 사전에 webSocketConfig Bean이 필요
  5. 어떠한 Bean도 생성되지 못하는 순환참조 발생!

위와같은 단계로 인해 에러가 발생하는 것이었습니다.

스프링 Bean에 대해 조금만 생각해보면 끄덕끄덕 납득되는 에러였지만, 저는 전혀 예상하지 못했습니다.


이제 에러를 파악했으니 해결방법에 대해 알아볼까요?

2. 시도와 해결

처음에 에러를 마주했을 때는 해결방법 따로 찾아보지 않고, Bean이 순환되어 등록되지 않는다는 걸 에러 모양?으로 깨달았습니다.

그래서 혼자 이상한 시도를 해보았는데,,

2-1. 새로운 객체 생성

왜인지 모르겠지만 단순한 생각으로 SimpMessagingTemplate 객체를 새로 생성해버리면 되지 않을까? 같은 바보같은 시도를 했습니다.

즉, convertAndSendToUser 가 필요한 메소드 내에서

SimpMessagingTemplate templete = new SimpMessagingTemplate;

위처럼 새로운 객체를 생성했습니다. (순환을 끊고 싶은 생각에 했던 시도였습니다)

서버가 작동되는데는 문제가 없었지만, STOMP 및 MessageChannel 과 해당 객체간의 연결이 이루어지지 않았기 때문에 당연히 클라이언트로 메세지가 보내지지 않았습니다.

2-2. 클래스 분리

정석 해결방법은 순환참조 고리를 끊도록 재설계하면 됩니다.

이 방법은 당시에는 떠올리지 못해서 결국 다른 방법으로 에러를 해결한 뒤, 채팅 개발을 모두 구현한 후에

다시 돌아와 적용해본 방법입니다.

고리를 끊기 위해서 마지막에 순환으로 연결되는 DelegatingWebSocketMessageBrokerConfiguration새로운 @Configuration 클래스의 Bean으로 등록했습니다.


위와 같이 클래스를 분리하여 Bean으로 등록하면 자연스레 순환 참조의 고리가 끊어지게 됩니다.

DelegatingWebSocketMessageBrokerConfiguration 을 Bean으로 등록하기 위해 1-2 처럼 webSocketConfig Bean이 먼저 생성되기 때문이죠.

해결 방법이 생각보다 간단했습니다,,

그럼 2-2를 알기전에 어떻게 순환참조 문제를 해결했을까요?

2-3. NEW 메세지 브로커

STOMP는 스프링에서 지원하는 메세지 브로커를 기본적으로 제공하고, 원한다면 Kafka 혹은 RabbitMQ 같은 별도의 메세지 브로커를 적용할 수 있습니다.

해결 방법을 부릅뜨고 찾아보던 중, 새로운 메세지 브로커를 사용하면 순환참조 없이 별도로 클래스를 분리해 메소드를 생성 후,

어떤 클래스든 해당 메소드를 사용하여 메세지를 보낼 수 있었습니다.

그래서 공부도 하고, 새로운 기술도 써볼 겸 다른 메세지 브로커를 사용해보기로 했습니다. (주 원인은 문제해결을 위해서 입니다)

3. 메세지 브로커

Kafka와 RabbitMQ 는 모두 메세지 브로커 역할을 할 수 있지만, 조금 다른 특징을 가지고 있습니다.

하나씩 간략하게 설명하고, 어떤 메세지 브로커를 선택하게 되었는지 알아보겠습니다.

3-1. Kafka

Kafka는 대표적인 이벤트 스트리밍 플랫폼입니다.

Kafka가 궁금하다면?

메세지 브로커와 이벤트 스트리밍 플랫폼 모두 이벤트를 수신하고, Consumer에게 전달하는 데에 목적이 있다는 것은 동일합니다.

하지만 이벤트 스트리밍 플랫폼의 경우 이벤트가 생성되면, 레코드 로그를 스트리머에 기록합니다.

이후 Consumer가 Topic에서 메세지를 가져간 후에도 이벤트 스트림에서 로그를 계속 유지하기 때문에, 문제가 발생해도 이벤트를 복구할 수 있는 장점이자 차이점이 있습니다.

또한 이벤트 스트리밍 플랫폼은 전통적인 메세지 브로커에 비해 유연하고, 느슨한 결합을 가져갈 수 있습니다.

따라서 분리와 확장에도 용이합니다.

메세지 브로커는 이벤트 브로커가 될 수 없지만, 이벤트 브로커는 메세지 브로커 역할을 할 수 있습니다.


3-2. RabbitMQ

RabbitMQ는 대표적인 메세지 브로커입니다.

메세지 브로커란 Publisher가 생산한 메세지를 메세지 큐에 저장하고, 저장된 데이터를 Consumer가 소비할 수 있도록 중간다리 역할을 합니다.

주의점이자 단점은 Consumer가 큐에서 데이터를 가져가면, 처리 후 짧은 시간내에 데이터가 삭제됩니다. 따라서 문제가 발생하는 경우 이벤트 복구가 어렵습니다.

3-3. 선택

물론 사이드프로젝트이기 때문에 많은 사용자가 몰려 대용량의 트래픽이 발생할 일은 극히 드뭅니다.

하지만 연습은 실전처럼, 실전은 연습처럼 해야하기 때문에,

  • 트래픽이 많고, 다양한 에러에 대응할 수 있는 방법이 명확
  • 유연하고, 느슨한 결합 장점

이유로 Kafka 를 선택하였습니다.

WebSocket + STOMP 기술로 채팅 기능을 구현하며 발생한 트러블 슈팅과 Kafka를 도입하게 된 배경에 대해 회고를 작성해보았습니다!

회고를 적으면서 얻어가는 부분이 많은 것 같습니다.

다음 채팅회고 - 3 에서는 Kafka를 사용하며 겪은 이슈들로 찾아 뵙겠습니다!

긴 글 읽어주셔서 감사합니다~! 😊

profile
하고 싶은 것들이 많은 개발자입니다
post-custom-banner

1개의 댓글

comment-user-thumbnail
2024년 9월 30일

안녕하세요! 이거와 똑같은 현상이 발생하지만 .... 혹시 2-2 가 되는게 맞을까요??
클래스만 분리했는데 잘 돌아가시나요?? 혹시 관련된 깃허브 좀 볼 수 있을까요?? 2-2 방식이용 ㅠㅠ

답글 달기