# 개인 메신저 프로젝트 백엔드/플랫폼 아키텍처 합의안 이 문서는 기존 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-app`과 `messenger-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/복제/샤딩` 이 구조는 사이드 프로젝트의 실행 가능성과, 이후 실제 사용자가 붙었을 때의 확장 가능성 사이에서 가장 균형이 좋다.