Projects

당근 개발자에게 배운 채팅구현 팀플에 적용하기 & 회고

채팅 시스템 구현

당근 테크 밋업 2024에서 배운 채팅 시스템 구현에 관한 이야기입니다. (일부 사진 출처 : 컨퍼런스 자료)

채팅 시스템 Basic Idea

채팅 서버는 연결을 유지하는 웹소켓을 이용하여 구현합니다.

서버는 연결된 사용자에 대해 내부 메모리에 UserID를 key로, Session 정보를 value로 갖도록 key-value 저장 시스템을 갖습니다.

이 상태에서 User A가 User B에게 채팅을 보내면 채팅 서버는 메모리 저장공간에서 User B를 찾아서 해당 세션으로 메시지를 전달하여 줍니다.

만약 서버가 여러대라면, 스레드 안전한 key-value store 시스템을 갖고 있어야합니다. 이를 구현하는 방식은 두가지가 있습니다.

  • 공유메모리 방식 : key - value 구조를 사용하는 공유메모리
  • Pub/Sub 방식

당근 채팅 팀은 공유 메모리 방식을 사용하고 있다고 합니다.

하지만 이러한 방식은 서버가 많아질 수록 확장해 불리해집니다.

공유 메모리에서 읽은 정보가 어떤 서버 인스턴스에 연결되어있는지 모를 뿐더러, 서버들이 서로 양방향 접근하는 방식은 쓰레드 안전하지 않고 수많은 버그를 발생시키기 때문입니다.

문제해결하기

당근 채팅팀은 이 문제를 메시지큐를 사용한 Producer-Consumer 패턴을 적용하여 해결하였다고 합니다.

각 채팅 서버는 메시지 큐에 이벤트를 발생시키고, 쌓인 메시지를 Consumer 인스턴스가 해소합니다.

이 변경의 장점은 다음과 같다고 합니다.

  • 공유 메모리에 대해 접근하는 서버 인스턴스의 개수가 제한됨
  • 서버가 서로 소통하지 않고 단방향으로 오퍼레이션 호출이 일어남

이어지는 스무고개 게임에서는 이 아이디어를 사용하여 구현하였습니다.

당근 채팅팀의 발표는 다음 링크에서 확인할 수 있습니다. 당근 채팅 시스템은 어떻게 만들까? | 2024 당근 테크 밋업


프로젝트: 스무고개 게임

2024년 11-12월에 진행된 프로젝트입니다.

이 프로젝트는 스무고개 게임을 구현하여 2명 이상의 사용자가 채팅 서비스에 접속하여 대화를 주고받는 프로그램입니다. 문제의 출제자는 ‘네’, ‘아니오’, ‘정답입니다’ 에 대한 대답을 통해서 프로그램이 수행되며, 1명 이상의 참여자는 20번의 질문 기회를 통해 정답을 맞추며 프로그램을 수행할 수 있습니다. 이 프로그램은 1:1 채팅과 단체 채팅을 구현합니다.

서버 애플리케이션은 Java를 기반으로 TCP/IP 소켓 통신 인터페이스를 제공합니다. 1개의 서버 소켓을 통해 사용자에게 요청 및 답변을 수행합니다. MySQL에 데이터를 저장하여 사용자가 게임을 진행한 기록에 대해 영속성을 보장합니다. 또한 AWS EC2에 배포되어 어느 곳 에서든 접근할 수 있도록 인터페이스를 제공합니다.

TCP/IP 소켓 통신을 제공하고 HTTP 통신을 구현하지 않지만, RESTful 원칙을 준수하도록 하여 효율적인 통신에 집중하였습니다. 채팅에 연결하는 요청을 제외하고 무상태 원칙, 비연결성 원칙, 인터페이스 일관화 등을 따르고 있습니다.

스레드 안전한 자료구조와 설계 및 스레드 자원 회수에 중점을 두며 많은 스레드 연결 속에서 안정적인 프로그램 구동을 목적으로 합니다.

  • JDK 17
  • IntelliJ IDEA, Gradle Project
  • MySQL
  • Docker

구현 내용

DTO 정의

사용자와 서버 간의 일관된 통신 인터페이스를 구현하기 위해 DTO를 가장 먼저 정의했습니다.

public class DTO { RequestType requestType; Object requestMsg; }

MVC 패턴 사용

팀 프로젝트에서 코드의 가독성과 유지보수성은 매우 중요하기에, 객체지향적 설계를 통해 관련되어 개선하고자 노력했습니다.

MVC 패턴은 Model – View – Controller 3개의 역할을 하는 클래스를 각각 작성하며, 클래스의 역할을 명확히 하고 코드의 유지보수성을 올려줍니다.

서버 소켓 컨트롤러 클래스

이 프로젝트에서는 한개의 서버 소켓을 이용하여 구현하며, 포트는 10001입니다. 이는 싱글턴 패턴으로 구현되어 프로그램 시작 시에 생성된 한개의 객체를 사용하도록 설계되었습니다. 

챗 스레드 목록 관리 클래스

이 프로그램에서 채팅 기능에서는 클라이언트와 서버가 소켓 연결을 유지합니다. 이를 효율적으로 관리하고, 정보를 사용하기 위해 만들어진 클래스가 ChatThreadsController입니다. 이 객체를 관리하는 객체 또한 싱글턴 패턴으로 프로그램 시작 시에 초기화되어 관리됩니다.

채팅id를 key로, 스레드의 목록을 value로 프로그램에서 사용됩니다.

생성자-소비자 패턴의 메시지 큐 시스템

여러 개의 채팅에 여러 명이 접속하면 수많은 스레드들이 생성되며, 동시성 문제가 발생하기 쉽다는 점을 인지하고 프로그래밍하였습니다. 또한 메시지를 보낸다면 1명 이상의 스레드에게 메시지가 발송되어야 하기에, 학기 초반에 배운 생성자-소비자 패턴의 메시징 큐를 하나 만들었습니다.

메시징 큐 시스템은 동시성을 관리하는 메시지 큐 컨트롤러(MsgQueueController) 객체와 메시징 큐에 있는 메시지를 분석하여 올바른 곳에 처리해주는 메시지 큐 소비자(MsgQueueConsumer) 시스템을 구현했습니다. 

로깅 시스템

서버사이드 프로그램의 모니터링과 디버깅을 위해 로깅 시스템이 별도로 구현되어 있습니다. 이는 아래에서 설명될 Log4j를 사용한 방식이며, format된 형식의 메시지를 화면에 출력합니다.

인메모리 및 데이터베이스 저장 시스템

프로그램을 구현하면서 자료구조에 저장하여 메모리상에 관리하는 시스템을 구축했습니다. 영속성이 보장될 필요가 없지만, 런타임에 관리되어야 하는 부분들에 대해 싱글턴 인스턴스로 관리하였습니다. 또한 채팅, 채팅방, 사용자를 저장하기 위해 MySQL기반의 데이터베이스가 사용되었습니다.


프로젝트 회고

회고는 4L(Liked, Lacked, Learned, Longed for) 방식으로 진행 

Liked : 좋았던 점

직접 소켓을 통해 통신 프로토콜을 만들면서, 이제는 정형화, 보편화된 RESTful 설계와 HTTP에 대해 고민하는 계기가 되었습니다.

  • 인터페이스의 일관성 : DTO를 개발 초기에 정의하고, 모든 통신은 DTO를 소켓을 통해 송수신 하며 진행
  • 무상태성(Stateless) : 각 요청은 이전 요청에 의존하지 않도록 설계. 메모리 또는 데이터베이스 기반의 정보를 바탕으로 프로그램의 흐름이 진행
  • 리소스기반 : HTTP 통신처럼 URI를 리소스기반으로 설계하지 않았지만, DTO에 RequestType을 통해 리소스를 명시하고 이를 기반으로 서버소켓이 핸들러를 매핑하도록 설계
  • 비연결성 : 모든 연결이 종료된 후에는 스레드가 종료되고 자원이 회수되어 다수의 연결에 의한 문제 발생 방지

Lacked : 아쉬웠던 점, 부족한 점

기존에 설계를 웹페이지로 설계를 했는데, 소켓 통신의 한계로 클라이언트 애플리케이션이 JavaFx를 사용하여 만들어졌습니다.

이는 브라우저는 HTTP 통신과 WebSocket을 공식적으로 지원하지만, TCP/IP 기반의 소켓 통신은 지원하지 않아 한계점을 맞이했습니다.

다른 팀의 발표를 들으면서 중간에 Proxy 서버를 개발하여 이를 해결할 수 있다는 것을 알게되었지만, 아쉬움으로 남습니다.

Learned : 배운 점

- HikariCP를 사용하여 데이터베이스 커넥션 풀 적용하기

사용자의 요청이 생기면 데이터베이스에 조회, 삽입 연산이 필요에따라 발생합니다.

그러나 클라이언트 애플리케이션의 설계상 요청이 자주 발생하고, 그때마다 MySQL-connector-j의 api를 사용하여 Connection을 가져와서 작업을 수행하는 것이 많은 런타임 에러를 발생시켰습니다.

HikariCP와 같은 커넥션풀 라이브러리를 사용하여 애플리케이션 시작단계에 미리 2개의 커넥션풀을 만들어 DataSource를 사용하는 방식으로 적용하였고, 문제를 해결했습니다.

이 문제는 트러블 슈팅에 꽤 오래 걸렸지만 왜 사용하는지 알 수 있게 해준 계기가 되었습니다.

- Docker를 통해 배포하기

Docker를 써본적이 없었는데, 배포에서 상당히 귀찮음을 느껴 사용하게되었습니다. 

컨테이너를 만들어서 진행하면 배포가 매우 편리하다는 사실을 배웠고, 겨울방학에 도커 공부를 본격적으로 해보고자합니다.

- 테스트코드로 클라이언트 Mocking 하기

테스트코드 작성은 Unit 테스트 정도만 작성하면서 프로그래밍을 했었습니다.

물론 Unit Test도 이 프로젝트에 포함되었지만, 클라이언트를 모킹하는 프로그램을 작성했습니다.

기존에 클라이언트 프로그램과 동시에 개발됨으로서 실제 동작을 테스트 하는 것이 지연됐었는데, 테스트코드로 통신부분만 작성하여 진행하여 정상 동작함을 확인할 수 있었고 개발 속도가 빨라졌습니다.

Longed for : 앞으로 바라는 것

- RESTful API에 대한 학습

이번 프로젝트를 기반으로 다음번 프로젝트에서 더 정형화된 프로그램을 작성하기 위해 REST 원칙에 대해 학습하고 이를 따르는 코드를 작성하는 연습을 도전합니다.

- TDD 개발 시도

테스트코드의 장점을 배웠으니 다음 프로젝트시에는 TDD를 수행하고자 합니다.

댓글

로딩 중...