채팅 이라는 것은 우리에게 익숙하고 기술적으로도 많이 알려져 있어 기술적으로 우위를 가져가기 어렵다는 생각이 들었다.
따라서 기술적인 도전을 위해 대용량, 확장성, 안정성 이라는 키워드를 가지고 아키텍처를 설계했다.
웹 소켓 통신을 하기 위해서 가장 기초가 되는 Web Socket과 STOMP이다.
Web Socket은 서버와의 Socket 연결을 담당하고 STOMP는 메세지를 관리하고 PUB/SUB 기능을 제공해 특정 토픽을 구독 하고 있는 사용자는 특정 토픽의 발행자가 하는 메시지를 수신할 수 있다.
채팅의 경우 채팅을 보내는 사람이 발행자(PUB)가 되고 채팅을 받는 사람이(SUB)가 된다.
간단한 만큼 문제점이 존재한다. 서로 다른 서버가 있을 경우 채팅이 되지 않는다. 동일한 서버 2개; A 서버 B 서버가 있다면 A서버의 사용자가 채팅을 보내면 (PUB) 해당 서버에서 구독하고 있는 사용자들만 수신한다. 따라서 같은 채팅방일지라도 서버가 다르면 메세지를 정상적으로 수신할 수 없다.
이는 우리가 처음 생각했던 대용량, 확장성 이라는 키워드를 만족시키지 못했다.
REDIS는 No Sql DB, In-Memory, PUB/SUB 등 다양하게 사용된다. 우선적으로 서로 다른 서버에서의 채팅 메시지를 수신하기 위해서 사용한 방법을 설명한다.(PUB/SUB)
한 채팅방에 2명의 사용자가 채팅을 하고 있다고 가정한다. A 사용자는 A 서버에 접속 중이고 B 사용자는 B 서버에 접속하고 있는 중이다.
Web Socket과 STOMP만을 사용한다면 A 서버 사용자가 메세지를 보내도 B 서버 사용자는 수신할 수 없다. 반대도 마찬가지다.
REDIS DB(Server)의 PUB/SUB을 이용한다면 이를 해결 할 수 있다. STOMP가 서버 내에서(사용자 끼리) PUB/SUB를 담당한다면 REDIS는 서버 외부에서(서버 끼리) PUB/SUB를 담당한다.
채팅방에서 A 사용자가 메세지를 전송하면 STOMP를 이용하여 서버에서 다시 REDIS로 발행한다. REDIS에서는 이를 구독하고 있는 다른 서비스들에 이를 발행하고 발행된 결과를 STOMP가 다시 PUB을 하게 된다면 서로 다른 서버에 있는 모든 사용자가 SUB(수신)을 할 수 있다.
이렇게 된다면 정상적으로 서비스가 수행되는 것 처럼 보이지만 단점이 있다.
REDIS 서버가 죽어버리면 안에 있는 내용이 위험하고 채팅방의 채팅이 많아질 경우 채팅의 순서 보장 및 유실 가능성이 커진다.
이는 우리가 처음 생각했던 안정성이란 키워드를 만족시키지 못했다.
Web Socket과 STOMP를 이용해서 서버와 클라이언트간 소켓 연결을 활성화 하고, 서버 내부에서 사용자들간 메세지를 송수신 했다.
Redis를 이용하여 다른 서버들과 통신을 할 수 있게 했으며, 채팅 내역을 채팅 내역을 불러올 때 Redis에도 저장해 Key-Value 구조의 빠른 read를 사용했다.
여기까지만 보면 정상적인 구동 시 채팅 서버가 원활하게 돌아감을 알 수 있다.
하지만 서버에 문제가 발생했을 경우 Redis는 순차 처리를 보장하지 않고 메세지의 유실이 생길 수 있다. 이를 해결하기 위해 Kafka를 사용했다.
Kafka가 어떻게 적용되었는지 설명하기 전에 우선적으로 Kafka가 무엇인지 설명을 한다.
Kafka는 PUB/SUB 모델의 메시지 큐로 분산환경에 특화되어 있는 특징을 가지고 있는 서버이다.
위의 정리를 이용해 단순하게 설명을 하자면 다수의 서버가 하나의 서비스를 이루고 있는 경우 kafka를 이용하여 분산처리를 하는데 각 서버가 통신하는 방식은 PUB/SUB를 이용하고, 이 PUB/SUB 메세지들은 메시지 큐의 형태로 순차적으로 처리된다. 그리고 모든 메세지들은 로그로 처리된다. 위의 특성을 이용한다면 서비스의 안정성을 확보할 수 있다.
처리 방식은 다음과 같다.
A 사용자가 채팅방에 메세지를 보낼경우 이를 A 서버가 Kafka에 지정된 Topic으로 Kafka Producer를 이용하여 PUB을 한다. PUB을 하게 되면 Kafka서버에 전송이되고 Kafka는 연결되어 있는 Consumer(Kafka Listener)에게 해당 메세지를 보낸다. 수신 받은 서버 측에서는 특정 토픽에 대한 메세지를 받았을 때, 처리해야 할 로직을 호출하여 처리한다.
채팅의 경우 수신 받은 메세지를 이용하여 Redis로 발행해 모든 서버가 공유하게 하고 이후 STOMP를 이용하여 해당 서버에 있는 모든 사용자와 공유한다.
이러한 구조로 모든 채팅 메세지들은 Kafka의 메시지 큐를 이용하여 순차적인 처리를 보장받을 수 있으며 모든 내역을 로그로 기록되기 때문에 유실이 되더라도 복구할 수 있으며, Consumer에 장애가 생길경우 Kafka가 해당 메시지를 다른 Consumer에게 보낼 수 있고, 처리할 수 있는 Consumer가 없을 경우에는 Kafka가 해당 메세지를 가지고 있다 Consumer가 복구되는데로 처리되어 안정성을 확보할 수 있다.
또한 Kafka는 Producer와 Consumer가 분리되어 있기 때문에 확장성이 보장받는다. 서버의 확장이 필요할 때, 확장된 서버에 Producer와 Consumer를 설정하고 처리 로직만 추가하면 되기 때문에 확장에도 유리하다.
이러한 장점으로 인해 Kafka는 Event 기반 실시간 처리가 필요한 대규모 실시간 서비스 (카카오 댓글, LINE 등)에 사용되며 첫 설계 시 생각했던 안정성, 확장성을 가진 Event 기반 대규모 실시간 채팅 서버를 개발 할 수 있었다.
빌드/ 배포 자동화 (CI/CD) Continuous Integration/ Continuous Delivery
CI/CD 설계
실제 설계
Giphy Script 작성