팀장으로서 기능과 운영을 함께 책임진 MeetBack 회고
MeetBack은 여러 사람이 가능한 시간을 공유하고, 모임을 만들고, 의견을 나누며 약속을 정할 수 있도록 만든 팀 프로젝트다. 나는 이 프로젝트에서 팀장을 맡았고, 기술적으로는 인프라를 주로 담당했다. 동시에 Google 로그인과 Redis Pub/Sub 기반 실시간 공통 모듈도 구현했다.
처음부터 내가 세운 목표는 “화면이 열린다”보다 조금 더 멀리 있었다. 실제 사용자가 접속하는 경로, 여러 애플리케이션 replica, 데이터베이스 복제, 파일 공유, 백업, 모니터링, 보안 정책을 하나의 서비스 흐름으로 연결하고 싶었다.
내가 책임진 범위
내가 직접 설계하고 구축한 인프라의 큰 축은 다음과 같다.
- Docker Swarm Manager 3대와 Worker 2대
- Worker별 Spring Boot 애플리케이션 replica
- MySQL Primary 1대와 read only Replica 1대
- 서비스 파일용 NFS와 DB 백업·로그용 NFS
- VyOS를 이용한 DB 전용망 분리
- Cloudflare Tunnel 기본 경로와 NGINX Proxy Manager 예비 경로
- Redis Pub/Sub과 ACL
- Prometheus, Grafana, Alertmanager, Loki, Alloy
- Linux 서비스 계정 최소권한과 역할별 iptables 정책
기능 쪽에서는 Google이 서명한 ID Token을 서버에서 검증하고 sub를 소셜 계정 식별자로 쓰는 기반을 만들었다. 실시간 기능에서는 여러 JVM 사이의 이벤트를 Redis로 전달할 수 있도록 공통 publisher와 envelope 경계를 설계했다.
서버 수보다 장애 경계를 먼저 정했다
장비가 많아 보이는 구조를 만드는 것은 어렵지 않다. 그러나 “이 서버가 죽으면 무엇이 멈추는가”를 설명할 수 없다면 좋은 구조라고 생각하지 않았다.
나는 제어 영역, 실행 영역, 데이터 영역, 파일 저장소, 외부 진입, 관측 영역을 분리했다. Swarm Manager는 3대로 quorum을 유지하고 애플리케이션은 Worker에만 배치했다. DB는 VyOS 뒤 전용망으로 옮겼고, 파일과 백업도 같은 NFS export에 섞지 않았다. Monitoring은 애플리케이션과 별도 서버에서 운영했다.
장애 범위도 함께 정리했다. Worker가 한 대 내려가면 app은 남은 한 replica로 응답하지만 2/2 용량을 유지하지 못한다. MySQL Replica 승격은 수동이고, NFS 서버와 VyOS도 각각 단일 장애점이다. 이 내용을 기준으로 다음 확장 순서를 문서화했다.
팀장으로서 가장 자주 한 일은 기준을 맞추는 일이었다
프로젝트 후반으로 갈수록 “기능이 됐다”와 “배포할 수 있다” 사이의 차이가 커졌다. 로컬에서는 동작하지만 운영 Secret이 없거나, 한 JVM에서는 동작하지만 두 replica에서는 상태가 갈리거나, endpoint는 열리지만 실제 사용자의 로그인 상태가 유지되지 않는 문제가 있었다.
나는 팀원에게 단순히 완료 여부를 묻기보다 확인 기준을 함께 정했다.
- 인증은 버튼 클릭이 아니라 callback, Token 검증, session 또는 JWT 유지까지 본다.
- 실시간 기능은 WebSocket 101 응답만이 아니라 STOMP CONNECTED와 replica 간 전파까지 본다.
- DB는 process active가 아니라 실제 write, replica 반영, 원복까지 본다.
- 백업은 파일 존재가 아니라 gzip 무결성과 복원 가능성을 본다.
- 배포는 container가 Running인지만 보지 않고 외부
/readyz, NFS 쓰기, DB 연결을 함께 본다.
기능 우선순위나 구현 방식에 의견 차이가 생겼을 때도 있었다. 나는 누가 맞는지 빨리 결정하기보다 재현 명령과 로그, 완료 조건을 먼저 만들었다. 그러자 논쟁이 사람의 선호가 아니라 “어떤 조건을 만족해야 하는가”의 문제로 바뀌었다.
일정이 짧을수록 하지 않을 일도 정해야 했다
2주 남짓한 기간 안에 자동 failover, Redis HA, NFS 이중화, router 이중화, CI/CD까지 모두 넣는 것은 현실적이지 않았다. 얕은 기능 목록을 늘리는 대신 실제로 검증할 수 있는 범위를 선택했다.
MySQL은 자동 승격 대신 GTID·TLS 복제와 수동 failover runbook을 만들었다. Redis는 실시간 상태 캐시라는 역할로 제한했다. CI/CD는 제외하고 commit 기반 image tag, test gate, stack deploy와 rollback 확인 절차를 남겼다.
이렇게 범위를 나누고 나니 무엇을 구현했고 무엇이 아직 수동인지 훨씬 분명해졌다.
마지막 검증에서 확인한 것
프로젝트 종료 시점에 나는 각 도구의 화면을 따로 캡처하는 데서 끝내지 않고 전체 경로를 다시 검증했다.
- Swarm node 5/5 Ready
- MeetBack app 2/2, Worker마다 1개
- cloudflared connector 5/5
- 기본 Cloudflare Tunnel 경로와 예비 NPM 경로의
/readyzHTTP 200 - MySQL Replica IO/SQL thread
Yes, lag 0초 - NFS app data를 한 Worker에서 쓰고 다른 Worker에서 읽고 삭제
- Prometheus target 23/23 UP
- Alert rule 21/21 healthy, firing 0
- Telegram 시험 alert 발생과 resolve
- Linux 10대의 계정·SSH·iptables 정책 감사 통과
숫자는 정상 상태를 보여 주지만, 사용자 기능 검증에서는 문제도 남았다. HTTPS 뒤에서 애플리케이션이 HTTP redirect를 생성했고 session cookie에 Secure가 없었다. Presence는 WebSocket upgrade는 성공했지만 STOMP 연결 성공률이 낮았다. Kakao callback에서는 이미 사용된 인가 코드를 다시 교환할 때 발생하는 오류가 관찰됐다.
나는 인프라가 정상이라는 이유로 이 문제를 덮지 않았다. 네트워크 경로와 애플리케이션 상태 문제를 분리하고, 후속 과제로 명확히 남겼다.
이 프로젝트에서 얻은 가장 큰 변화
예전에는 구축 결과를 “설치했다”, “연결했다”, “동작한다”로 설명하곤 했다. 이번 프로젝트를 거치면서 나는 정상의 정의를 훨씬 구체적으로 말하게 됐다.
어떤 요청 경로를 통과했는지, 어떤 계정과 권한으로 실행됐는지, 장애가 어느 범위까지 번지는지, 그 상태를 무엇으로 확인했는지를 함께 설명해야 비로소 운영 가능한 결과라고 생각한다.
제한된 장비와 기간 안에서 기능, 배포, 데이터, 보안, 관측을 하나의 시스템으로 연결했다. 남아 있는 단일 장애점과 애플리케이션 이슈도 다음 작업 순서로 정리했다. 팀장으로서 내가 가장 만족하는 결과도 바로 그 점이다.
다음 글에서는 내가 직접 맡은 Google 로그인과 Redis 실시간 공통 모듈에서 어떤 신뢰 경계를 정했는지 정리한다.
실제 검증 화면
- 프로젝트 종료 다음 날 내가 다시 확인한 Swarm 상태다. Manager 3대와 Worker 2대가 모두 Ready·Active였고, Leader와 Reachable 상태도 정상적으로 유지됐다.
- App 2개가 Worker 1과 Worker 2에 하나씩 배치된 모습이다. 같은 화면에서 replica 수, VIP, 노드당 최대 1개,
stop-first업데이트 정책도 함께 확인했다.
