Post

채팅 애플리케이션 서버 설계

채팅 애플리케이션 서버 설계

이번 포스팅에서는 특정한 비즈니스 상황 안에서 채팅 애플리케이션 아키텍처를 설계해보며, 다음과 같은 걸 배우는 것을 목표로 한다.

  • 자본적인 limit을 가지고 고성능 애플리케이션을 만드는 방법
  • 실시간 채팅 애플리케이션을 설계하는 방법

마치 실제 업무 환경에서 겪을 만한 배경이 주어지고, 그 안에서 설계를 진행할 것이다. 필자가 어떤 고민들을 통해 어떤 판단을 해서 논리를 만들어 나가는지, 또 독자는 어떠한 다른 판단들이 서는지 생각해보면서 글을 읽으면 좋을 거라 생각한다.


주어진 비즈니스 상황, 채팅 앱의 필요성 정의

먼저 우리에게 주어진 비즈니스 상황이 무엇인지 파악해보자. 우리 회사의 비즈니스 도메인은 ‘해외 치과’가 겪는 치기공물에 대한 수요/공급의 불일치를 한국 기공소의 공급을 통해 일치시켜 나가는데에 있다. 치기공물 의뢰 과정에는 의사가 환자의 케이스에 맞는 세부적인 요구사항을 기공소에게 이야기하고, 가능 여부 등등을 확인하는 과정이 필수적으로 필요하다.

이러한 도메인 속에서, 고객인 치과 측의 컴플레인은 주로 기공소와의 소통에서 발생한다. 따라서 운영진들은 그들의 대화를 팔로업하고, 고객이 불만을 갖는 것 같으면 그 불만의 맥락에서 성향을 파악하여 다른 기공소를 소개하는 방식으로 서비스하고 있었다. 이러한 서비스는 여러 기공소를 갖고 있는 우리의 플랫폼의 장점을 충분히 활용하고 있었다.

하지만 이 서비스엔 다음과 같은 어려움이 있음을 확인하였다.

  • 고객이 사용하는 다양한 채팅 앱을 통해 이러한 서비스를 제공하고 있기 때문에, 고객들의 대화를 팔로업할 때 여러 앱을 번갈아가면서 확인해야하므로 업무가 혼잡해진다.
  • 운영진이 실제로 사용하는 카카오톡과 같은 앱에서 사적인 채팅과 업무 상의 채팅이 구별하기 어려워진다.
  • 한국 기공사들이 영어로 소통하는 데에 어려움이 있다.
  • 추후 서비스가 커질 수록 많아질 대화들을 계속해서 인적자원을 들여 팔로업하는 것은 한계가 있다.

위와 같이 정리한 Pain Points를 해결할 수 있는 방법에 대해 고민했고, 다음과 같은 결론을 내렸다.

  • 비즈니스 안의 소통우리의 앱에서 관리할 수 있어야 한다. (업무 단일화 및 내부 기능 제공)
  • ‘치과 측의 채팅을 번역하는 기능’이 제공되어야 한다. (소통 원활화)
  • ‘채팅 내용에서 감정을 읽어내는 기능’이 필요하다. (컴플레인 감지)

이러한 방법들을 적용할 수 있는 방법은 채팅 애플리케이션을 개발하는 것이라고 판단 및 팀에 전파/설득한 후 작업을 시작했다. 다만 다음과 같은 한계적인 배경이 있었다.

  • 인프라 비용에 제한이 있었다. 따라서 투자는 조금씩, 아웃풋을 보이며 증가시켜 나가야한다.
  • 내부 개발 인원은 1명. 따라서 기술 부채에 예민하게 생각하자.

이 배경은 전체적인 의사결정에 계속해서 영향을 주므로 머리 속에 기억해두자.


채팅 애플리케이션 초기 설계

회사의 서비스 이용자 수는 작년부터 꾸준히 늘어나고 있는 상황이었다. 따라서 추후 수평 확장이 가능하도록 애플리케이션 구조를 설계했고, 실시간 채팅 기능이 필요하므로 이 기능에 적합한 웹소켓 프로토콜과 빈번한 채팅 발행을 효율적으로 처리하기 위한 WAS & DB를 채택해야 했다.

실시간 채팅 전파는 수평 확장 구조에서 브로드캐스트가 가능해야 했으므로, 이를 위한 이벤트 브로커를 설계에 포함시켰으며 ‘이벤트로 인한 부하’를 분산시키기 위해 각각의 WAS마다 고유한 키 값을 갖게 하고, 그 키 값을 기준으로 이벤트 전파 구역을 나누는 구조로 설계했다.

다음으로 각 기술적 의사결정의 논리들을 설명하려 한다.


애플리케이션 스택

사업은 초기 성장 단계로 비용을 최적화할 필요가 있었다. 그리고 인프라 비용을 최대한 아껴야 했기 때문에 제한된 예산 내에서 사용자를 최대로 수용하는 것을 중요한 목표로 설정했다.

또한, 메신저 서버(채팅 애플리케이션)의 도메인의 특성 상 동시 접속자 수가 많실시간 메세지 처리가 빈번하여 I/O bound 작업이 많다는 점도 선택 기준이 되었다.

WebMVC vs WebFlux

주어진 상황에서 적합한 프레임워크가 무엇인지 비교해보자.

비용 요소WebMVCWebFlux
메모리 사용량높음낮음
CPU 사용률높음낮음
동시 처리 성능제한적우수
I/O 대기 효율성낮음높음
확장성수직 확장 위주수평 확장 유리

WebMVC의 처리 방식

위 비교표에서 볼 수 있듯이, 전통적인 WebMVC 방식에서는 하나의 요청을 처리하는 동안 스레드가 블로킹되는 문제가 발생하여 I/O 집약적인 메신저 서비스에 부적합하다. 아래 예시를 통해 문제점을 자세히 살펴볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestController
public class MessageController {

    @PostMapping("/message")
    public ResponseEntity<String> sendMessage(@ResponseBody MessageRequest request) {
        // 1. 사용자 인증 - DB 조회 (평균 50ms 블로킹)
        User user = userService.authenticateUser(request.getToken());

        // 2. 번역 처리 - 외부 API 호출 (평균 200ms 블로킹)
        String translatedContent = translationService.translate(request.getContent());

        // 3. 감정 분석 - 외부 API 호출 (평균 150ms 블로킹)
        EmotionResult emotion = emotionService.analyze(request.getContent);

        // 4. 메세지 저장 - DB 저장 (평균 30ms 블로킹)
        Message message = messageService.saveMessage(request, translatedContent, emotion);

        // 5. 실시간 전송 - 웹소켓 브로드캐스트 (평균 20ms 블로킹)
        webSocketService.broadcastMessage(message);

        return ResponseEntity.ok("Message sent");
        // 총 450ms 동안 하나의 스레드가 블로킹
    }
}
  • 성능 저하: 예시의 비즈니스 로직 1회 수행에 총 450ms 동안 하나의 스레드가 완전히 블로킹된다.
  • 자원 낭비: 100개의 동시 요청 발생 시, 스레드 풀 크기(일반적으로 200개)의 절반이 점유되고, 컨텍스트 스위칭이 빈번하게 발생하여 대기 시간 동안 메모리와 CPU 자원이 낭비된다.

WebFlux의 처리 방식

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Mono<String> sendMessage(@RequestBody MessageRequest request) {
    return userService.authenticateUser(request.getToken())
            .flatMap(user -> Mono.zip(
                    translationService.translate(request.getContent()),
                    emotionService.analyze(request.getContent())
            ))
            .flatMap(tuple -> {
                String translation = tuple.getT1();
                EmotionResult emotion = tuple.getT2();
                return messageService.saveMessage(request, translation, emotion);
            })
            .flatMap(message -> webSocketService.broadcastMessage(message)
                    .thenReturn("Message sent"));
    // 스레드는 즉시 해재되어 다른 요청 처리 가능
} 

반면, WebFlux는 논블로킹(Non-blocking) 방식으로 I/O 작업을 처리하여 스레드 활용 효율을 극대화하고, 메신저 서비스의 높은 동시성 요구사항을 충족한다.

  • 효율적인 스레드 활용: I/O 대기 시간 동안 스레드가 다른 작업을 처리할 수 있어 스레드 풀을 효율적으로 활용한다.
  • 성능 향상: 번역과 감정 분석을 병렬 처리하여 총 처리 시간을 450ms에서 280ms로 단축한다.
  • 높은 동시성 처리: 적은 수의 스레드로도 수천 개의 동시 요청을 처리할 수 있다.

비용 효율성 실측 결과

실제 성능적으로 어떤 차이점을 갖는지 확인해보기 위해 아래와 같은 환경에서 테스트를 진행하였다.

  • AWS EC2 t3.medium (2 vCPU, 4GB RAM)
  • RDS PostgreSQL t3.micro
  • 외부 API 평균 응답시간: 100ms
  • 동시 사용자: 1000명

테스트 결과는 다음과 같았다.

기준WebMVCWebFlux
처리량600 RPS1,800 RPS
평균 응답시간3.2 sec0.7 sec
95% 응답시간6.8 sec1.3 sec
CPU 사용률82%38%
메모리 사용량3.6GB1.8GB
DB 커넥션 수100개8개(평균값)
스레두 수100개2개

채팅 도메인과의 적합성

채팅 도메인이 갖는 특성인 실시간성높은 동시성을 고려할 때, WebFlux는 다음과 같은 이점을 제공한다.

  • 웹소켓 연결 관리: WebFlux는 논블로킹 I/O를 통해 수천 개의 웹소켓 연결을 효율적으로 관리할 수 있다.
  • 메세지 브로드캐스팅: 대량의 메시지 전송 시에도 스레드 블로킹 없이 처리할 수 있다.
  • 외부 API 통합: 번역/감정 분석 API 호출과 같은 외부 연동 시 블로킹 없는 병렬 처리를 지원한다.

결론적으로 I/O 집약적 작업에서의 강력함, 동시적으로 많은 요청을 처리할 수 있는 능력, 거기에 비용 절감까지 노릴 수 있기 때문에 웹 기반 스택으로써 WebFlux을 채택하였다.

더하는 말

추가적으로, WebMVC와 WebFlux의 아키텍처 설계 철학을 포함한 세부적인 비교 또한 진행했다. 해당 내용도 확인하고 싶다면 링크를 클릭하여 확인해주길 바란다.


DB 스택

데이터베이스를 선택하는 기준에서도 비용 효율성과 확장성이 가장 중요하게 고려할 사항이었다.
거기에 더해 애플리케이션의 기반인 WebFlux와의 기술적 시너지와 채팅 도메인의 데이터 특성도 함께 고려해서 RDBMS(PostgreSQL), NoSQL(MongoDB), Cassandra(ScyllaDB) 세 가지 옵션을 비교 분석했다.

채팅 도메인의 데이터 특성은 다음과 같이 정의하였다.

  • 쓰기 중심: 메세지 생성이 조회보다 빈번
  • 시간순 데이터: 메세지는 시간 순으로 저장되고 조회됨
  • 비정형 데이터: 텍스트, 이미지, 파일 등 다양한 메세지 타입
  • 급격한 데이터 증가: 사용자 증가에 따른 메세지량 폭증
  • 실시간성: 낮은 지연시간 요구
구분PostgreSQLMongoDBCassandra(ScyllaDB)
초기 구축 비용⭐⭐⭐⭐⭐매우 낮음⭐⭐⭐⭐ 낮음⭐⭐ 높음
운영 비용⭐⭐⭐ 보통⭐⭐⭐⭐ 낮음⭐⭐ 높음
수평 확장성⭐⭐ 제한적⭐⭐⭐⭐ 우수⭐⭐⭐⭐⭐ 탁월
WebFlux와의 시너지⭐⭐⭐ 보통(R2DBC)⭐⭐⭐⭐⭐ 탁월⭐⭐⭐ 보통
개발 생산성⭐⭐⭐ 보통⭐⭐⭐⭐⭐ 매우 높음⭐⭐ 낮음

RDBMS 후보군으로 PostgreSQL이 선택된 이유

RDBMS 제품군들을 현재의 비즈니스 상황/도메인을 고려해 비교해본 결과 PostgreSQL가 후보군으로 선정되었다. 다른 것들이 선택되지 못한 이유는 다음과 같다.

Oracle은 라이선스 비용이 매우 높아 비용 효율성 요구사항에 부합하지 않는다. 특히 스타트업이나 비용에 민감한 프로젝트에서는 현실적으로 선택할 수가 없다.

MySQL/MariaDB는 채팅 도메인에 필요한 핵심 기능들에서 PostgreSQL에 비해 한계가 명확하다. 빈번한 쓰기 작업을 처리하는데 있어서 Row-level lockingMVCC를 조합해서 처리하기 때문에 락 경합 문제가 PostgreSQL에 비해 빈번하게 발생하기 때문이다. 또, 확장성 측면에서 샤딩 솔루션의 성숙도도 PostgreSQL이 더 뛰어나다.

SQL Server는 Windows 환경에 주로 최적화되어 있어 Linux 기반 인프라에서 제약이 있고, .NET/Microsoft 스택에 특화되어 있어 Java/Spring 환경에서 활용도가 떨어진다.

SQLite는 임베디드 DB로 동시 접속과 확장성 요구사항을 충족할 수가 없다.

PostgreSQL은 오픈소스이기 때문에 무료로 이용이 가능하고, ACID 특성을 완벽히 보장하면서도 높은 동시성 처리가 가능한 MVCC 아키텍처로 설계되었으며 샤딩/파티셔닝에 대한 기능이 풍부해서 가장 확장성이 뛰어나다.

이러한 이유로 PostgreSQL을 선택했다.

PostgreSQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- PostgreSQL에서의 메세지 저장 구조
CREATE TABLE messages (
    id BIGSERIAL PRIMARY KEY,
    chat_room_id UUID NOT NULL,
    user_id UUID NOT NULL,
    content JSONB NOT NULL, -- 메세지 내용을 JSONB로 저장 가능
    message_type VARCHAR(50) NOT NULL,
    created_at TIMESTAMP DEFAULT NOW(),
    INDEX idx_chat_room_time(chat_room_id, created_at)
);

-- 메세지 조회 시 복잡한 쿼리 필요
SELECT m.* u.name as username
FROM messages m 
JOIN users u ON m.user_id = u.id
WHERE m.chat_room_id = ? -- ?에 조회하려는 채팅방 기입
ORDER BY m.created_at DESC
LIMIT 50;

‘PostgreSQL’는 비용 측면은 만족하지만, 채팅 데이터를 효율적인 관리가 어려운 데이터 모델링 방식을 가졌으며 네이티브 샤딩 기능이 없어 다른 후보군에 비해 확장성이 뒤쳐진다.
분산 관리성을 제쳐두더라도 채팅 데이터는 결국 JOIN 연산을 필요로 할 가능성이 높고, 새로운 속성을 추가한다는 등의 요구사항을 적용하기 위해서는 스키마 변경이 적용되어야 하는 한계가 있다.

Cassandra(ScyllaDB)

‘Cassandra’는 초고성능 메세지 처리가 가능하지만, 현재의 비즈니스 상황에서는 과도한 선택이었다.

  • 높은 초기 구축 비용: 전문 인력과 복잡한 클러스터 설정 필요
  • 운영 복잡성: 작은 개발팀에서 관리하기 어려운 구조
  • 개발 생산성 저하: CQL 학습 곡선과 제한적인 쿼리 기능

위와 같은 이유로 오버엔지니어링이라 판단되었기에 선택하지 않았다.

MongoDB

데이터 특성과의 적합성

MongoDB의 임베디드 구조의 데이터 모델링 방식은 정규화를 필요로 하지 않아 JOIN 연산없이 모든 데이터를 조회한다.
이는 곧 데이터 지역성으로 인해서 성능까지 크게 향상되는 긍정적인 결과로도 이어진다.
또, 스키마리스 구조의 유연성을 갖기 때문에 새로운 메세지 타입 추가 시 즉시 반영하는 게 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
    _id: ObjectID("..."), // 시간 정보 포함
    chatRoomId: "room_123",
    userId: "user_123",
    userName: "김철수",
    content: {
        type: "text",
        text: "안녕하세요",
        translation: "Hello"
    },
    emotion: {
        sentiment: "postiive",
        confident: 0.85
    },
    createdAt: ISODate("2024-01-15T10:30:00Z")
}

// 단순한 조회
db.messages.find({ chatRoomId: "room_123" })
        .sort({ _id: 1 })
        .limit(50);

채팅 메세지는 시간순 조회가 주요 패턴인데, MongoDB의 ObjectId가 시간 정보를 포함하므로 별도의 인덱스 없이 효율적인 시간순 정렬이 가능하다는 점도 채팅 도메인과 적합하다.

MongoDB의 설계 철학

DB가 ‘어떤 의도로 설계되었는지’에 따라서 ‘어떻게 사용하는게 가장 좋을지’가 정해진다. 그 의도들을 살펴보면 RDBMS 보다 NoSQL인 MongoDB가 더욱 적합하다.

RDBMS는 ACID 원칙을 위해 동기 방식으로 설계되었다. 즉, 트랜잭션 무결성과 데이터 일관성 보장이 우선이며, 복잡한 관계와 제약조건 처리를 위한 순차적 실행 구조를 지녔다.

MongoDB는 확장성과 성능을 목적으로 비동기 방식으로 설계되었다. 문서 기반의 독립적 처리로 병렬 실행에 최적화되어 있으며, 이벤트 루프논블로킹 I/O 친화적 구조로 되어있다.

분산 환경에서의 고성능 처리가 MongoDB 설계의 목표였다는 점에서 현재 설계 방향성과도 일치한다.

그리고 공식문서를 살펴보더라도, PostgreSQL은 PostgreSQL: Asynchronous Command Processing 에서
“By itself, calling PQgetResult will still cause the client to block until the server completes the next SQL command” - 여전히 서버가 다음 SQL 명령을 완료할 때까지 클라이언트가 블로킹 됩니다, 라고 서술하고 있다.

MongoDB는 MongoDB Architecture Guide 에서 ‘비동기 처리와 이벤트 드리븐 아키텍처’를 설명하고 있다. 비동기 방식으로 동작하도록 설계되었다는 걸 서술하고 있는 것이다.

마지막으로, “‘WebFlux와의 시너지’는 어떤게 더 좋은가?” 의 관점에서 MongoDB부터 살펴보면, MongoDB Reactive Streams와 조합에서 요청부터 응답까지 전체 스택에서 블로킹 지점이 없다. PostgreSQL는 R2DBC를 통해 비동기 API를 사용할 수 있지만, 성숙도와 생태계 면에서 MongoDB 대비 아직 제한적이다.

이러한 논리들에 기반하여 MongoDB를 DB 스택으로 채택하였다.

더하는 말

추가적으로, RDBMS보다 MongoDB가 대용량 데이터 처리에 왜 더 뛰어난 성능을 보이는지에 대한 고찰 또한 진행하였다. 해당 내용도 확인하고 싶다면 링크를 클릭하여 확인해주길 바란다.


이벤트 브로커 스택

이벤트 브로커를 선택하는 기준으로 앞선 스택들과 동일하게 비용확장성을 설정하였지만, 앞선 스택들에서 비용과 확장성의 중요도를 앞세우고 러닝 커브가 높은 스택들을 선택했기 때문에 개발 및 운영 복잡성 최소화와 초기 서비스 단계에서의 빠른 개발 속도도 중요한 기준으로 설정하였다. 거기에 더해 메신저 도메인의 실시간 특성도 고려하여 ‘Redis Pub/Sub’, ‘RabbitMQ’, ‘Apache Kafka’ 세 가지 옵션을 비교 분석했다.

이러한 전제 하에서 채팅 애플리케이션의 메시지 브로커 요구사항을 다음과 같이 정의하였다.

  • 실시간 메시지 전파: 채팅방 내 참여자들에게 즉시 전달
  • 극저지연 처리: 사용자 경험을 위한 마이크로초 단위 응답
  • 개발 복잡성 최소화: 초기 서비스 단계에서의 빠른 구현
  • 운영 부담 최소화: 작은 개발팀에서 관리 가능한 구조
  • 메시지 지속성 불필요: 채팅 히스토리는 별도 DB에 저장
  • 초기 사용자 규모: 예상 동시 접속자 수 10,000명 이하

비교 분석표

기준Redis Pub/SubRabbitMQKafka
데이터 내구성없음선택적 내구성높은 내구성
수평 확장성제한적 수평 확장성중간 수준 확장성높은 수평 확장성
지연 시간초저지연낮음중간
운영 복잡성 최소화⭐⭐⭐⭐⭐⭐⭐⭐
개발 복잡성 최소화⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
실시간 메세지 전파⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
10K 동시 접속자 처리⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

Apache Kafka

‘Apache Kafka’는 대용량 메세지 스트리밍 처리가 가능하지만, 현재의 비즈니스 상황에서는 과도한 선택이었다.

  • 높은 초기 구축 비용: ZooKeeper/KRaft 설정, 파티션 설계 등 복잡한 클러스터 구성 필요
  • 운영 복잡성: 작은 개발팀에서 관리하기 어려운 분산 시스템 구조
  • 개발 생산성 저하: 토픽, 파티션, 오프셋 관리 등 개발에 필요한 작업량 증가
  • 상대적 고지연: 배치 처리 기반으로 실시간 메세징에 최적화되지 않음
  • 불필요한 지속성: 메세지 영속화 기능이 현재 요구사항에 오버스펙

위와 같은 이유로 오버엔지니어링이라 판단되기에 선택하지 않았다.

RabbitMQ

‘RabbitMQ’는 안정적인 메세지 전달이 가능하지만, 초기 서비스 단계에서 필요한 단순함을 만족하지 못했다.

  • 추가 인프라 비용: 별도 RabbitMQ 클러스터 구축 및 운영 필요
  • 복잡한 개념 구조: Exchange, Queue, Binding 등 복잡한 메세지 라우팅 개념
  • 설정 복잡성: 채팅방 별 큐 관리와 동적 라우팅 구현의 복잡성
  • 과도한 메세지 보장: ACK/NACK 메커니즘이 현재 요구사항에 불필요
  • 상대적 고지연: 디스크 I/O와 메세지 큐잉으로 인한 지연

현재 단계에서는 메세지 전달 보장보다 개발 속도가 더 중요했기 때문에 선택하지 않았다.

Redis Pub/Sub

메세지 브로커 요구사항과의 완벽한 적합성

Redis Pub/Sub의 인메모리 기반 구조는 메신저 서비스의 실시간 특성에 최적화되어 있다. 채널 기반의 간단한 구조는 ‘채팅방’ 이라는 도메인 개념과 직관적으로 매핑되어 자연스럽게 구현할 수 있다.

또한, 메세지 지속성이 불필요한 현재 요구사항에서 Redis Pub/Sub의 특성이 오히려 장점으로 작용하여 불필요한 오버헤드를 제거한다.

기존 인프라와의 완벽한 시너지

채팅 시스템 설계 기반에 세션 관리캐싱 관리 역할 또한 필요하다.
Redis Pub/Sub은 기존 Redis 인프라를 활용해 별도의 추가 인프라 없이 그러한 역할도 수행할 수 있는 선택지이다.
이는 인프라 비용 절감운영 복잡성 최소화라는 현재 단계의 목표와 완벽하게 일치했다.

또한 Redis의 단일 인스턴스에서 캐싱과 메세지 브로커 기능을 동시에 제공하여 네트워크 지연도 최소화한다.

개발 및 운영 단순성

Redis Pub/Sub은 극도로 단순한 API를 제공한다.
PUBLISH, SUBSCRIBE, PSUBSCRIBE 등 몇개의 명령어만으로 실시간 메세징을 구현할 수 있어 개발 속도가 매우 빠르다. 복잡한 설정이나 스키마 정의 없이 즉시 사용 가능하며, 모니터링도 기존 Redis 모니터링 도구를 그대로 활용할 수 있다.

장애 상황에서도 Redis 재시작만으로 간단하게 복구되며, 클라이언트는 재연결만 하면 되는 단순한 구조이다.

성능

마이크로초 단위의 극저지연을 제공하여 사용자가 체감하는 실시간성을 보장한다.
현재 목표인 10,000명의 동시 접속자 처리에 충분한 성능을 제공하며, 메모리 기반 처리로 높은 처리량을 보장한다.
채팅 도메인의 특성상 메세지 손실이 치명적이지 않고, 클라이언트 레벨에서 간단한 조회 또는 재전송 로직으로 보완 가능하다.

이러한 이유들로 현재 비즈니스 단계에서 Redis Pub/Sub을 채택하였다.

Redis Pub/Sub이 처리할 수 있는 동시 접속자 수: 기본적으로 10,000 명 (참고 문헌)

향후 확장성 고려사항

Redis Pub/Sub의 수평 확장성의 한계를 인지하고 있으며, 다음과 같은 마이그레이션 전략을 수립했다.

  • 동시 접속자 50,000명 초과 시: RabbitMQ 클러스터링으로 마이그레이션
  • 메세지 전달 보장이 중요해질 때: RabbitMQ의 ACK 메커니즘 활용 대규모 분산 환경 필요시: Apache Kafka로 최종 마이그레이션

현재 단계에서는 개발 속도와 운영 단순성이 확장성보다 우선순위가 높으며, 필요 시점에 점진적으로 마이그레이션하는 전략이 비즈니스 목표에 부합하므로 이러한 전략을 수립했다.


마무리하며

여기까지 특정한 상황에서 필요한 프로덕트를 고안하고 그 프로덕트의 복잡성, 비용, 생산성의 기준을 산정하여 아키텍처를 설계해보았다.

이 애플리케이션은 확장이 필요한 시점의 시작점에서는 스케일-업 으로, 이후에 스케일-아웃 으로 대응할 수 있는 구조를 갖는다. 좀 더 세부적인 절약점으로 처음엔 PostgreSQL을 사용하다가 MongoDB로 마이그레이션하는 방법도 있다. 하지만 1인 개발로 서비스되어야 하는 상황에서 마이그레이션 작업에 드는 비용이 적지 않을 것을 고려해야 한다.

개발자로써 어떤 판단을 내릴 땐 “항상 정답은 없다.”는 생각으로 고민을 거듭하지만 그 속에서 가장 정답에 가까운 답을 찾아내는걸 목표로 하게 된다. 그 과정이 고단하기도 하지만 동시에 즐겁기도 하다. 그 과정에 이 글이 독자의 힘이 되기를 바라며 글을 마치겠다.

This post is licensed under CC BY 4.0 by the author.