미니PC 서버를 실제 서비스처럼 운영하기

사용자 요청 경로, GitOps 상태, 관측, 백업을 나눠 다룬 회고

한강자리 백엔드는 미니PC 위에서 돌아간다. 이렇게만 쓰면 취미 서버처럼 들릴 수 있다. 하지만 App Store에 올라간 앱이 그 API를 호출하기 시작하면 이야기가 달라진다.

사용자는 서버가 어디 있는지 모른다. 주차 정보가 오래됐거나 알림이 늦게 오면, 그건 “작은 개인 프로젝트”의 사정이 아니라 앱이 못 미더운 순간이 된다. 이 지점에서 생각이 많이 바뀌었다.

”돌아간다”로는 부족했다

처음에는 “개인 프로젝트니까 단순하게”라는 생각이 있었다. API와 worker가 돌아가고, DB와 Redis가 붙고, 앱에서 응답이 오면 충분해 보였다.

하지만 사용자가 붙으면 말이 달라진다. “돌아간다”가 아니라 “문제가 났을 때 어디부터 볼 수 있는가”를 답할 수 있어야 했다.

flowchart LR
  Client["iOS 앱 / 위젯"] --> Public["사용자 요청 경로<br/>엣지 · 인그레스"]
  Public --> API["API 서비스"]
  API --> Data["Postgres / Redis"]
  Workers["워커<br/>수집 · 예측 · 알림"] --> Data
  Data --> API
  Runtime["GitOps 상태"] --> API
  Runtime --> Workers
  Signals["메트릭 · 로그 · 대시보드"] --> API
  Signals --> Workers

이 중 하나가 흔들리면 앱은 느려지거나 오래된 정보를 보여준다. 그래서 미니PC라도 실제로 쓰는 서비스라면 cloud VM처럼 다뤄야 했다. 서버 크기는 작아도 사용자가 만나는 실패는 작아지지 않았다.

사용자 요청과 관리 경로를 분리했다

가장 먼저 지킨 것은 사용자 요청 경로와 관리 경로를 섞지 않는 일이었다.

앱 사용자가 접근하는 API를 로그인이 필요한 관리용 보호선 뒤에 두면 안 된다. 반대로 운영자가 서버를 만지는 경로를 사용자 API와 같은 방식으로 열어두는 것도 위험하다.

특정 설정값을 공개하는 대신, 사용자의 요청과 운영자의 접근을 다른 성격으로 봐야 한다.

이 구분은 장애가 났을 때도 도움이 된다. 사용자 요청 경로가 막힌 문제인지, 실행 중인 서비스 내부 문제인지, 운영자가 쓰는 제어 경로 문제인지가 섞이면 복구 순서가 흐려진다.

서버 상태를 Git에 남겼다

작은 프로젝트에서도 “지금 서버가 왜 이 상태인가”를 설명할 수 있어야 했다. 그래서 서버가 되어야 하는 상태를 Git에 남기고, 실제 실행 환경이 그 상태로 맞춰가게 했다.

flowchart LR
  AppRepo["앱 저장소"] --> CI["CI<br/>테스트 · 빌드"]
  CI --> Image["버전 붙은 이미지"]
  CI --> Desired["원하는 배포 상태"]
  Desired --> Sync["GitOps 동기화"]
  Sync --> Runtime["API · 워커 · 데이터 서비스"]
  Runtime --> Smoke["배포 직후 확인"]

이 방식의 장점은 문제가 생겼을 때 더 크게 느껴졌다.

  • 어떤 이미지가 나갔는지 추적할 수 있다.
  • 실제 서버가 Git에 적힌 상태와 달라졌는지 볼 수 있다.
  • 배포와 배포 직후 확인을 같은 언어로 말할 수 있다.
  • 문제가 생겼을 때 “서버에 직접 뭘 했는지”를 추측하지 않아도 된다.

물론 GitOps가 모든 문제를 해결하지는 않는다. DB migration, worker 교체 배포, 운영 설정 교체, 외부 원천 실패 같은 일은 여전히 따로 봐야 한다. 그래도 서버가 되어야 하는 상태가 남아 있다는 것만으로 사고 범위가 줄어든다.

대시보드는 확인 순서에서 시작했다

그래프와 로그를 붙인다고 운영이 자동으로 좋아지지는 않는다. 먼저 실제로 어떤 순서로 볼지 정해야 한다.

한강자리에서 계속 봐야 하는 신호는 다음이었다.

  • 사용자 API가 살아 있는가.
  • API는 살아 있는데 특정 화면용 응답만 실패하는가.
  • worker가 원천 데이터를 계속 수집하고 있는가.
  • 마지막 성공 시각이 오래됐다고 볼 시간을 넘었는가.
  • cache 적중/실패 비율이 비정상적으로 변했는가.
  • 예측 run이 최신인가.
  • push outbox가 쌓이고 있지 않은가.
  • 백업은 만들어졌고, 복구가 검증됐는가.

대시보드는 이 신호를 모아놓은 화면이어야 했다. 그래프가 많아도 원인 후보를 줄이지 못하면 도움이 되지 않는다.

그래서 대시보드는 수치를 많이 보여주는 방향보다, 실제로 확인하는 순서에 맞춰야 했다. API가 살아 있는지, 특정 화면용 응답만 실패하는지, worker가 멈췄는지, 원천이 오래됐는지, cache가 대신 값을 내고 있는지를 먼저 보게 했다.

장애가 보이면 경로부터 따라갔다

문제가 생겼을 때 바로 코드를 의심하면 늦다. 한강자리에서는 요청이 지나가는 길을 따라가며 장애를 나눠 본다.

flowchart TD
  Symptom["사용자 제보 또는 스모크 실패"] --> Public{"사용자 API 정상?"}
  Public -->|아니오| Path{"엣지/인그레스 경로 문제인가?"}
  Path -->|예| Network["사용자 요청 경로 확인"]
  Path -->|아니오| Runtime["실행 환경 확인"]
  Public -->|예| Feature{"특정 화면만 실패하는가?"}
  Feature -->|예| Data{"원천 최신성 또는 캐시 문제인가?"}
  Data -->|예| Worker["워커 · 수집 · 캐시 확인"]
  Data -->|아니오| API["API 응답 · DB 조회 확인"]
  Feature -->|아니오| Client["앱 캐시 · 위젯 스냅샷 확인"]

이 순서에서 중요한 것은 특정 실행 방법이 아니라, 어떤 구간을 어떤 순서로 의심하는지다.

실제로 운영할 때도 이 순서가 마음을 가라앉혀준다. 장애가 보이면 손이 먼저 코드를 열고 싶어진다. 하지만 사용자 요청 경로가 막힌 문제와 원천 데이터가 오래된 문제는 전혀 다른 대응이 필요하다. 구간을 따라가면 최소한 엉뚱한 곳을 오래 파는 시간을 줄일 수 있다.

백업은 복구까지 해봐야 한다

백업 파일이 있다는 것과 복구할 수 있다는 것은 다르다. 한강자리에서는 데이터를 두 종류로 나눠 봤다.

데이터성격
공공데이터 원천에서 다시 가져올 수 있는 값다시 만들 수 있음
사용자 알림 설정, push subscription복구해야 하는 사용자 데이터
앱 이벤트와 audit운영을 되짚어 볼 근거
forecast/backtest 이력예측을 되짚어 볼 근거
cache다시 만들 수 있음

다시 만들 수 있는 cache와 사용자가 남긴 데이터를 같은 무게로 보면 안 된다. 복구 연습은 “백업이 실제로 복구되는가”를 확인하는 최소 절차다.

이렇게 나눠두면 장애 상황에서도 먼저 복구할 대상이 보인다. 다시 만들 수 있는 값이 사라진 것인지, 사용자가 남긴 설정이나 운영을 되짚어 볼 근거가 사라진 것인지에 따라 복구 순서가 달라진다.

마지막에 남은 변화

가장 크게 바뀐 것은 서버를 보는 태도였다. 예전에는 API와 worker가 켜져 있으면 충분하다고 생각했다. 지금은 사용자가 보는 값이 오래됐을 때 어디서 멈췄는지, 어떤 데이터는 다시 만들 수 있고 어떤 데이터는 복구해야 하는지 먼저 본다.

작은 서버를 썼기 때문에 운영이 쉬워진 것이 아니었다. 작은 서버라서 어떤 책임을 직접 떠안고 있는지 더 선명하게 보였다.

이미지 확대