kotalk/docs/archive/planning/01-backend-platform-architecture.md
2026-04-16 09:24:26 +09:00

18 KiB

개인 메신저 프로젝트 백엔드/플랫폼 아키텍처 합의안

이 문서는 기존 Rocky Linux VPS 위에 Docker로 채팅 서버를 구축하는 것을 전제로 한 구현 지향형 백엔드/플랫폼 설계안이다. 목표는 카카오톡 PC 버전 수준의 빠른 체감 응답성과 안정적인 실시간 동기화 경험을 내는 것이다.

합의에 참여한 전문 관점:

  • 리얼타임 백엔드 아키텍트
  • 플랫폼 엔지니어
  • PostgreSQL 데이터 엔지니어
  • SRE/관측성 엔지니어
  • 보안 엔지니어
  • 스토리지/백업 엔지니어

1. 전제와 설계 원칙

전제:

  • 서버는 기존 Rocky Linux VPS에 배포한다.
  • Docker 사용 가능하다.
  • 초기 개발 대상은 Windows PC 클라이언트다.
  • 초기는 개인 사이드 프로젝트지만, 구조는 다중 디바이스와 사용자 증가를 막지 않아야 한다.

설계 원칙:

  • 초기에는 단일 VPS에 맞는 단순성을 우선한다.
  • 그러나 프로토콜, 데이터 모델, 메시지 ID, 스토리지 인터페이스는 이후 수평 확장을 막지 않게 설계한다.
  • 채팅 메시지의 원본 저장소는 처음부터 PostgreSQL로 통일한다.
  • 실시간 연결과 영속 저장을 분리해 생각한다.
  • MinIO는 첨부파일 저장소로는 괜찮지만 백업 저장소로 간주하지 않는다.
  • 운영 편의보다 복구 가능성을 우선한다.

2. 최종 권장 아키텍처 한 줄 요약

초기 최적안은 단일 애플리케이션 계열 + PostgreSQL + Redis + MinIO + Caddy 조합이다.

  • 클라이언트 외부 프로토콜은 HTTPS REST + WSS(WebSocket over TLS)를 사용한다.
  • 서버 애플리케이션은 논리적으로 API/Realtime/Worker로 분리하되, 초기에는 같은 코드베이스와 같은 이미지에서 역할만 나눠 배포한다.
  • 영속 데이터는 PostgreSQL 하나를 기준축으로 삼는다.
  • Redis는 presence, 세션 인덱스, 단기 캐시, fan-out 보조 용도로만 사용하고, 진실의 원천으로 쓰지 않는다.
  • 첨부파일과 프로필 이미지는 MinIO에 저장하되 S3 호환 계층으로 추상화해서 이후 외부 오브젝트 스토리지로 쉽게 옮길 수 있게 한다.

3. 왜 이 아키텍처가 현재 VPS에 가장 적합한가

이 VPS는 단일 머신이다. 따라서 초기에 Kafka, 다수의 마이크로서비스, 복잡한 서비스 메시를 올리면 운영 부담이 가치보다 커진다. 반대로 단일 앱 컨테이너 하나에 모든 역할을 몰아넣고 Redis 없이 메모리만 사용하면, 나중에 멀티 인스턴스나 재시작 복구, presence 일관성, fan-out 제어에서 다시 뜯어고쳐야 한다.

따라서 가장 균형이 좋은 선택은 아래와 같다.

  • 앱은 단순하게 유지한다.
  • 상태 저장은 PostgreSQL로 고정한다.
  • 연결 상태와 단기 이벤트 전달은 Redis에 맡긴다.
  • 첨부파일은 애플리케이션 로컬 디스크가 아니라 S3 호환 저장소로 보낸다.
  • 프록시는 Caddy로 단순화한다.

이렇게 하면 MVP에서는 운영 난이도가 낮고, 이후 앱 복제본을 늘릴 때도 방향이 유지된다.

4. 외부 프로토콜 권장안

4.1 클라이언트 통신

권장:

  • 인증, 초기 동기화, 대화방 목록, 과거 메시지 조회, 첨부 업로드 시작/완료: HTTPS REST
  • 실시간 메시지 수신, 읽음 상태, 타이핑 상태, presence, 서버 이벤트 푸시: WSS

선택 이유:

  • Windows 데스크톱에서 구현과 디버깅이 쉽다.
  • 프록시/Caddy/TLS 환경에서 안정적이다.
  • 모바일/웹 클라이언트로 확장해도 동일 패턴을 유지하기 쉽다.
  • gRPC streaming보다 도입 장벽이 낮고, 브라우저/데스크톱 지원이 직관적이다.

권장하지 않는 초기 선택:

  • 순수 TCP 커스텀 프로토콜: 성능 이점보다 운영/보안/프록시 복잡성이 커진다.
  • MQTT: 메신저 도메인 모델과 인증/권한/히스토리 조회가 결국 별도 API를 필요로 하므로 단순해지지 않는다.
  • WebRTC data channel 중심 설계: P2P는 NAT, 오프라인 저장, 멀티 디바이스 동기화에 불리하다.

4.2 앱 레벨 메시지 프레임

초기부터 다음 개념은 반드시 포함한다.

  • message_id: ULID 또는 UUIDv7
  • conversation_id
  • sender_user_id
  • client_request_id: 중복 전송 방지용
  • server_sequence: 대화방 단위 또는 사용자 inbox 단위 순서값
  • sent_at, delivered_at, read_at
  • event_version
  • device_id

이 필드가 있으면 이후 재전송, 읽음 동기화, 멀티 디바이스, 중복 ACK 처리, 서버 재시작 복구가 쉬워진다.

5. 서비스 구성 권장안

초기 컨테이너 구성:

  • messenger-caddy 외부 80/443 수신, TLS 종료, WebSocket 업그레이드 처리, 정적 헬스 라우팅
  • messenger-app REST API와 WebSocket 게이트웨이 역할 수행
  • messenger-worker outbox 처리, 알림 발행, 썸네일/미디어 후처리, 정리 배치 작업
  • messenger-postgres 핵심 영속 데이터 저장소
  • messenger-redis presence, session map, short-lived cache, publish/subscribe 보조
  • messenger-minio 첨부파일, 프로필 이미지, 미디어 저장
  • messenger-backup PostgreSQL 백업과 MinIO 메타/버킷 동기화 작업
  • messenger-prometheus 메트릭 수집
  • messenger-loki 로그 저장
  • messenger-grafana 대시보드와 알림 뷰
  • messenger-node-exporter 또는 유사 호스트 메트릭 수집기 VPS CPU, 메모리, 디스크, 네트워크 관측

중요한 점:

  • messenger-appmessenger-worker는 같은 코드베이스를 쓰는 것이 좋다.
  • 초기에는 마이크로서비스로 쪼개지 않는다.
  • 역할만 분리해 컨테이너를 나누면, 장애면을 분리하고 추후 스케일 아웃하기 쉬워진다.

6. 데이터 저장 전략

6.1 PostgreSQL을 중심으로 두는 이유

채팅 서비스는 결국 다음을 안정적으로 다뤄야 한다.

  • 대화방 생성/멤버십
  • 메시지 영속화
  • 읽음 상태
  • 첨부 메타데이터
  • 차단/숨김/핀/알림 설정
  • 운영 감사 로그

이 영역은 관계형 정합성이 중요하다. PostgreSQL은 트랜잭션, 인덱스, JSONB, 전문 검색, WAL 기반 백업, 추후 복제까지 한 번에 가져갈 수 있다.

초기에는 NoSQL보다 PostgreSQL이 훨씬 안전하다.

6.2 핵심 테이블 범주

초기부터 분리해서 설계할 범주:

  • 사용자/프로필
  • 디바이스/세션
  • 친구 관계 또는 연락처 매핑
  • 대화방
  • 대화방 멤버
  • 메시지
  • 메시지 첨부 메타
  • 메시지 전달 상태
  • 읽음 커서
  • 리액션
  • 차단/뮤트/보관
  • 감사 로그
  • 아웃박스 이벤트

핵심 원칙:

  • 메시지 본문은 PostgreSQL에 저장한다.
  • 첨부파일 바이너리는 MinIO에 저장하고 DB에는 메타와 경로만 둔다.
  • 읽음 상태는 사용자별 커서 모델을 우선한다. 메시지별 읽음 row를 무분별하게 만들면 커진다.

6.3 Redis 역할 제한

Redis는 아래만 맡기는 것이 맞다.

  • 온라인 사용자 presence TTL
  • 웹소켓 세션 라우팅 인덱스
  • 단기 rate limit 카운터
  • 최근 조회 캐시
  • 멀티 인스턴스 fan-out 보조 pub/sub

Redis를 진실의 원천으로 두면 복구와 디버깅이 어려워진다. 메시지 원본이나 유일한 unread 상태를 Redis에 넣는 것은 피한다.

7. 실시간 메시징 처리 흐름 권장안

메시지 전송 처리 흐름:

  1. 클라이언트가 client_request_id 포함 메시지 전송
  2. 서버가 권한과 대화방 멤버십 확인
  3. PostgreSQL 트랜잭션에서 메시지 row 저장
  4. 같은 트랜잭션에서 outbox 이벤트 저장
  5. 커밋 성공 후 송신자에게 서버 확정 ACK 반환
  6. worker가 outbox를 읽고 수신자 online 세션으로 fan-out
  7. 수신 클라이언트 ACK 수신 후 전달 상태 갱신
  8. 읽음 이벤트는 별도 커서 업데이트로 처리

이 구조의 장점:

  • DB 커밋 전송 실패와 푸시 실패를 분리할 수 있다.
  • 서버가 내려갔다가 올라와도 outbox 기준으로 재처리 가능하다.
  • 이후 Redis pub/sub에서 NATS JetStream 같은 것으로 바꿔도 모델이 유지된다.

8. 첨부파일/미디어 저장 전략

초기 권장:

  • 원본 파일은 MinIO에 저장
  • 업로드는 서버에서 발급한 제한적 업로드 정책 또는 서버 경유 업로드 중 하나를 선택
  • MVP에서는 서버 경유 업로드가 구현은 쉽지만, 대용량에서 병목이 된다
  • 가능하면 초반부터 S3 호환 업로드 패턴으로 설계하는 것이 낫다

권장 메타데이터:

  • object_key
  • original_filename
  • mime_type
  • byte_size
  • image_width, image_height
  • checksum
  • upload_status
  • virus_scan_status 또는 reserve 필드

확장 포인트:

  • MinIO를 나중에 Cloudflare R2, AWS S3, Backblaze B2로 옮기기 쉽게 추상화
  • 썸네일과 프리뷰는 worker에서 비동기 생성

9. 인증/세션 전략

Windows PC만 먼저 개발하더라도 처음부터 아래를 고려한다.

  • 사용자 계정과 디바이스 세션 분리
  • device_id를 가진 장치별 refresh session 유지
  • access token은 짧게, refresh token은 서버 추적 가능하게
  • 강제 로그아웃, 특정 디바이스 로그아웃, 세션 폐기 가능해야 함

권장:

  • 앱 로그인 후 디바이스 세션 생성
  • WebSocket 연결 시 access token 검증
  • 세션 폐기 시 WebSocket 강제 끊기

이렇게 해야 추후 모바일 앱을 붙여도 모델을 바꾸지 않는다.

10. VPS 배포 전략

10.1 배포 방식

권장:

  • Docker Compose 기반 단일 프로젝트 스택
  • systemd에서 compose 스택을 부팅 시 자동 기동
  • 상태 저장 볼륨은 명확히 분리

권장 볼륨 범주:

  • PostgreSQL data
  • Redis data
  • MinIO data
  • Grafana data
  • Prometheus data
  • Loki data
  • 앱 업로드 임시 디렉터리

주의:

  • 기존 VPS에 다른 서비스가 있다면 네트워크와 도메인을 분리해야 한다.
  • 외부 80/443을 이미 다른 Caddy가 쓰고 있다면, 새 메신저 스택은 기존 프록시에 역방향 프록시로 붙이는 방식이 가장 안전하다.
  • 메신저 전용 서브도메인을 분리한다. 예: msg.example.com, api.msg.example.com, ws.msg.example.com, media.msg.example.com

10.2 네트워크/포트

외부 공개는 최소화한다.

  • 외부 공개: 80, 443, 22
  • 내부 전용: PostgreSQL, Redis, MinIO 관리 포트, Grafana, Prometheus, Loki

원칙:

  • DB/Redis/MinIO는 외부에 직접 열지 않는다.
  • 관리자 UI가 필요하면 VPN 또는 SSH 터널 기반 접근만 허용한다.

10.3 리소스 운영 방침

초기에는 다음 우선순위로 리소스를 배정한다.

  1. PostgreSQL 메모리 보장
  2. 앱/WebSocket 연결 처리 여유 확보
  3. MinIO 디스크 여유 확보
  4. 관측성 스택은 과하지 않게 운영

관측 스택이 너무 무거우면 다음 순서로 경량화한다.

  • Grafana 유지
  • Prometheus retention 축소
  • Loki retention 축소
  • 고해상도 scrape interval 완화

11. 관측성 설계

최소한 아래는 반드시 수집한다.

애플리케이션 메트릭:

  • 활성 WebSocket 연결 수
  • 인증 성공/실패 수
  • 메시지 송신 TPS
  • 메시지 저장 지연
  • outbox 적체 수
  • fan-out 지연
  • 읽음 이벤트 처리량
  • 업로드 성공/실패 수
  • 대화방별 초당 메시지 폭주 탐지

인프라 메트릭:

  • CPU
  • 메모리
  • 디스크 사용량
  • 디스크 IOPS
  • 네트워크 in/out
  • 컨테이너 재시작 횟수
  • PostgreSQL connections, replication lag 향후 대비
  • Redis memory eviction 여부

로그:

  • JSON 구조화 로그
  • request_id, user_id, device_id, conversation_id, message_id 등 상관 키 포함
  • 인증 실패, 권한 오류, 메시지 저장 실패는 별도 레벨 관리

대시보드 우선순위:

  1. 서비스 헬스
  2. 메시지 송수신 지연
  3. DB 상태
  4. 첨부 업로드 상태
  5. 에러 로그 상위 유형

12. 백업 및 복구 전략

가장 중요한 원칙:

  • 같은 VPS 안의 다른 디렉터리에 저장한 사본은 백업이 아니다.
  • MinIO에 저장한 DB 덤프도 MinIO가 같은 머신에 있으면 재해 복구가 아니다.

권장 백업 구조:

  • PostgreSQL: 일간 전체 백업 + 지속적 WAL 아카이빙
  • 백업 저장 위치: 외부 오브젝트 스토리지
  • MinIO 버킷 데이터: 주기적 외부 스토리지 동기화
  • 설정 파일과 compose 파일: Git 또는 암호화된 설정 저장소 별도 보관

복구 목표:

  • RPO 목표: 5분에서 15분
  • RTO 목표: 1시간 이내

권장 운영 습관:

  • 월 1회 복구 리허설
  • 최소 스테이징 환경으로 복원 테스트
  • 백업 성공 여부만 보지 말고 실제 복구 성공 여부 확인

13. 보안 권장안

초기 사이드 프로젝트라도 아래는 반드시 한다.

  • root 비밀번호 SSH 로그인을 끄고 키 기반 인증으로 전환
  • fail2ban 또는 동등한 차단 장치 적용
  • firewalld/ufw로 외부 포트 최소화
  • Docker secrets 또는 최소한 권한 제한된 env 파일 사용
  • DB/Redis/MinIO 비밀번호 분리
  • TLS 강제
  • 관리자 계정 MFA 고려
  • 민감 로그 마스킹

애플리케이션 보안 체크포인트:

  • rate limiting
  • 대화방 멤버십 권한 검사
  • 첨부파일 MIME 및 확장자 검증
  • 업로드 크기 제한
  • 악성 파일 검사 훅 준비
  • 세션 폐기와 토큰 로테이션

14. 스케일링 경로

단계 1. MVP

구성:

  • 단일 VPS
  • 단일 messenger-app
  • 단일 messenger-worker
  • PostgreSQL 단일 인스턴스
  • Redis 단일 인스턴스
  • MinIO 단일 인스턴스

적합한 상황:

  • 개인 프로젝트 초기
  • 소수 사용자
  • 운영자가 혼자일 때

단계 2. 같은 아키텍처로 성능 확장

구성 변화:

  • 더 큰 VPS 또는 별도 앱 서버
  • messenger-app 복수 인스턴스
  • Redis 기반 세션 공유와 fan-out
  • PostgreSQL 튜닝 및 읽기 부하 분산 준비

필수 전제:

  • WebSocket 세션 상태가 로컬 메모리에만 있지 않아야 함
  • outbox 기반 비동기 처리 구조가 이미 있어야 함

단계 3. 상태 저장 계층 분리

구성 변화:

  • PostgreSQL을 관리형 또는 별도 전용 서버로 이동
  • MinIO를 외부 오브젝트 스토리지로 이동
  • Redis를 별도 인스턴스로 분리
  • 앱은 여러 대로 수평 확장

이 단계에서 바뀌면 좋은 것:

  • 로드밸런서 도입
  • 무중단 배포
  • 관측성 경보 체계 정교화

단계 4. 더 큰 사용량

구성 변화:

  • 내부 이벤트 버스를 Redis pub/sub에서 NATS JetStream으로 교체 또는 병행
  • PostgreSQL 읽기 복제본 도입
  • 검색을 외부 검색엔진으로 분리
  • 대화방 파티셔닝 또는 사용자 샤딩 검토

Kafka는 이 프로젝트 규모에서 너무 이른 선택일 가능성이 높다. NATS JetStream이 운영 난이도와 기능 균형이 더 좋다.

15. 마이그레이션 경로를 부드럽게 만드는 설계 포인트

초기부터 반드시 지켜야 하는 항목:

  • 메시지 ID는 시간 정렬 가능한 전역 고유값 사용
  • DB 마이그레이션 체계 도입
  • 아웃박스 패턴 도입
  • 첨부 저장소는 S3 호환 인터페이스 뒤에 숨기기
  • 세션/프레즌스는 Redis 기반으로 외부화 가능하게 만들기
  • API 응답과 WebSocket 이벤트에 버전 필드 두기
  • 기능 플래그 도입

이것만 지켜도 단일 VPS에서 시작한 시스템을 크게 깨지 않고 다음 단계로 옮길 수 있다.

16. 자가 구현 서버 vs Matrix/기타 프로토콜 비교

16.1 이번 프로젝트의 권장 선택

이번 프로젝트는 자가 구현 서버가 더 적합하다.

이유:

  • 카카오톡 PC 스타일 UX를 맞추려면 제품 흐름과 프로토콜을 세밀하게 제어해야 한다.
  • Matrix는 강력하지만 연합, 브리지, 규격 준수, 이벤트 모델 이해 비용이 크다.
  • 단일 VPS에서 개인 프로젝트로 운영하기에는 Synapse 계열은 무겁고 운영 난이도가 높다.
  • Element류 생태계와 맞물리는 장점보다, 제품 개성과 속도에서 제약이 더 크게 느껴질 가능성이 높다.

16.2 Matrix가 적합한 경우

  • 오픈 프로토콜과 연합이 중요할 때
  • 외부 클라이언트 호환성이 중요할 때
  • 자체 프로토콜 설계보다 표준 기반 확장을 원할 때

16.3 기타 선택지

  • XMPP: 성숙했지만 현대적인 제품 UX에 맞춘 구현에서는 다시 커스텀 계층이 많이 필요하다.
  • MQTT: IoT와 경량 pub/sub에는 좋지만 메신저 영속 모델과 권한 모델이 핵심이 아니므로 주축으로는 비권장이다.

결론:

  • MVP와 제품 완성도 우선이라면 자가 구현
  • 개방형 생태계 우선이라면 Matrix

이번 케이스에서는 자가 구현이 맞다.

17. 실제 구축 순서 권장안

  1. 도메인과 서브도메인 구조 확정
  2. Docker Compose 스택 초안 구성
  3. PostgreSQL 스키마와 마이그레이션 체계 확정
  4. 인증/세션 모델 확정
  5. REST + WSS 이벤트 계약서 작성
  6. outbox 기반 메시지 저장/전달 흐름 구현
  7. Redis presence 및 세션 인덱스 도입
  8. MinIO 첨부 업로드 흐름 도입
  9. Prometheus/Loki/Grafana 기본 대시보드 구성
  10. 외부 백업 파이프라인 연결
  11. 장애 복구 리허설
  12. 부하 테스트 후 튜닝

18. 최종 합의안

가장 현실적이고 좋은 방향은 아래다.

  • 프로토콜은 REST + WebSocket
  • 서버 구조는 모놀리식 코드베이스 + app/worker 역할 분리
  • DB는 PostgreSQL
  • 캐시/프레즌스는 Redis
  • 첨부 저장소는 MinIO(S3 호환)
  • 프록시는 Caddy
  • 관측성은 Prometheus + Loki + Grafana
  • 백업은 외부 오브젝트 스토리지로 분리
  • 확장 경로는 단일 VPS -> 앱 복제본 -> 상태 저장 계층 외부화 -> NATS/복제/샤딩

이 구조는 사이드 프로젝트의 실행 가능성과, 이후 실제 사용자가 붙었을 때의 확장 가능성 사이에서 가장 균형이 좋다.