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

이 문서는 기존 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/복제/샤딩`

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