이전글 : https://www.gpters.org/dev/post/we-preparing-server-moves-7xUjhs9hPufxZbk
지난주 사례 발표에서 자꾸 말을 버벅거렸습니다. 대본을 안보고 발표 한 시간 전까지 슬라이드를 고치고 있 었어서도 그랬지만, 저 혼자 사이트 접속이 잘 됐는지 테스트할 때와는 다른 상황에 당황했기 때문이기도 했습니다.
Zoom과 화면 공유를 켜고, Chrome과 Hermes와 Docker Desktop까지 띄우자 메모리 사용률이 98%까지 올라갔었습니다.
저도 뭐라 말할지 몰라 버벅거렸는데 노트북도 같이 버벅이고 있더라고요.
이번에는 저번에는 못 보였던 청중들에게 제공할 링크 화면을 보완하고 하나의 완성된 시연을 보여주고 싶었습니다. Zoom을 끌 수는 없었고, RAM을 따로 구비할 겨를도 없고 결국 무언가를 제 메모리에서 내보내줘야 했습니다.
그렇게 미니 홈서버를 지향하는 스터디에서 발표 이틀 전에 Docker Desktop에서 Render로 서버를 옮기게 되었습니다.
💦 목차
지난 발표에서 서버보다 먼저 멈춘 노트북
Render로 옮기자 53만 건의 상권 DB가 유료 디스크가 됐습니다
급하게 갈아엎을수록 되돌아갈 길부터 남겼습니다
LTE에서는 접속은 성공했는데 검색이 안 됐습니다
아직 Zoom을 켠 환경에서의 테스트가 남았습니다
1. 지난 발표에서 서버보다 먼저 멈춘 노트북
이 프로젝트는 원래 제 Windows 노트북의 Docker Desktop에서 실행됐습니다.
Express 서버, 53만 건이 들어 있는 SQLite, 지도 화면, 운영자 관제 화면을 Docker 컨테이너로 묶었습니다. 컨테이너를 다시 만들어도 데이터가 남는지 확인했고, 서버가 꺼지면 다시 시작하는 설정과 health check도 붙였습니다.
그래서 처음에는 Docker까지 만들었으니 발표 준비도 거의 끝났다고 생각했습니다.
하지만 Docker는 앱의 실행 환경을 포장할 뿐, 실행하는 컴퓨터의 자원을 늘려주지는 않았습니다.
변경 전: Docker 컨테이너를 노트북(로컬)에서 실행
→ 변경 후: 같은 Docker 컨테이너를 Render에서 실행Windows에서 Docker Desktop을 실행하면 WSL도 함께 자원을 사용합니다. 여기에 Zoom, 화면 공유, Chrome, Hermes가 동시에 올라왔습니다. 당시 사용 메모리가 98%까지 올랐습니다.
이제야 Docker를 계속 쓸지와 로컬에서 Docker를 실행할지를 같은 질문으로 묶으면 안 된다는 것을 알았습니다.
Docker가 불필요했던 것은 아닙니다. 지금까지 만든 Docker 이미지를 Render가 대신 실행하게 하면 됩니다. 바꿔야 했던 것은 포장 방식이 아니라 실행 장소였습니다.
그래서 로컬 Docker는 개발과 되돌리기용으로 남기고, 발표용 서버는 Render의 관리형 Web Service로 옮기기로 했습니다.
2. Render로 옮기자 53만 건의 상권 DB가 유료 디스크가 됐습니다
처음에는 현재 Docker 이미지를 Render에 올리면 끝날 줄 알았습니다.
그런데 기존 구조에는 약 221MB의 SQLite 파일이 있었습니다. 서울 상가 537,489건과 북마크가 들어 있는 데이터였습니다. 로컬에서는 이 파일을 Windows 폴더에 두고 컨테이너에 연결했습니다. 컨테이너가 바뀌어도 파일은 노트북에 남았습니다.
Render에서 같은 수명을 원하면 Persistent Disk가 필요했습니다. Web Service 비용과 별도로 계속 보존할 저장 공간을 붙여야 했습니다.
여기서 처음으로 persistent가 단순히 “데이터가 안 날아감”이라는 기술 기능이 아니라는 것을 체감했습니다.
무엇을 얼마나 오래 남길 것인가
재배포 뒤에도 남아야 하는가
그 보존에 매달 비용을 낼 것인가
백업과 복구는 누가 책임질 것인가
이 질문에 답해야 영속 저장이 필요하다고 말할 수 있었습니다.
이번 발표에서 청중이 만드는 북마크는 장기 보관할 업무 데이터가 아니었습니다. 발표가 끝나면 사라져도 되는 실습 상태였습니다. 반면 53만 건 상가 DB는 크고 오래 보존해야 했습니다.
그래서 저는 부랴부랴 상권정보의 Open API 사용 권한을 받고 구조를 바꿨습니다.
기존
53만 건 SQLite에서 상가 검색
+ SQLite에 북마크 영구 저장
변경
공단 Open API에서 상가 검색
+ 검색한 결과 일부만 작은 임시 SQLite에 저장
+ 북마크도 발표 세션 동안만 유지Render에는 Persistent Disk를 별도로 붙이지 않았습니다. 배포하거나 서버가 다시 시작되면 검색 스냅샷과 북마크는 초기화됩니다.
이 선택은 기능을 줄인 것이 아니라 데이터의 실제 수명에 맞춘 것이었습니다.
개념
이번 프로젝트에서 이해한 의미
컨테이너
앱과 실행 환경을 포장한다. 컴퓨터의 RAM과 디스크 한계까지 없애주지는 않는다.
관리형 서버
서버 운영의 일부를 플랫폼에 맡긴다. 데이터 수명과 비용까지 자동으로 결정해주지는 않는다.
Persistent Disk
클라우드 서버에 붙여 사용하는, 서버 재시작·재배포 뒤에도 내용이 남는 저장 디스크
재배포 뒤에도 남는 저장 공간이다. 오래 남길 가치와 비용을 함께 결정해야 한다.
Open API
큰 원천 데이터를 직접 들고 있지 않고 필요할 때 조회할 수 있다. 대신 외부 기관의 지연, 장애, 호출 제한에 의존한다.
관리형 서버로 옮겼더니 서버만 바뀐 것이 아니라 데이터 구조까지 바뀌었습니다.
배포는 데이터를 어떤 상태를 어디에 얼마나 오래 둘지 다시 묻는 일이기도 했습니다.
3. 급하게 갈아엎을수록 되돌아갈 길부터 남겼습니다
발표가 이틀 남았고 Open API 권한을 이제 막 받았으니, 처음에는 기존 DB를 지우고 싶었습니다.
그때 후와고(반려봇)가 나중에 잘못 될 경우를 생각해서 리허설까지 통과한 뒤 제거하자고 제안했고 저도 곧 동의했습니다.
그래서 새 구현보다 먼저 기존 상태를 동결했습니다.
전체 테스트 130개 통과
SQLite 파일 약 221MB
상가 537,489건
기존 북마크 6건
검색 → 북마크 생성 → 삭제 → rollback 뒤 원상복구 확인
기준선이 생기고 나서야 Open API 전환을 시작했습니다. API 방식이 실패하면 SQLite 공급자로 돌아갈 수 있도록 두 경로도 한동안 같이 유지했습니다.
Render 배포에도 여러 시행착오가 있었습니다.
처음 두 번은 Render에 전달된 참가자·발표자용 토큰이 서버가 요구하는 형식과 맞지 않았습니다. 서버는 잘못된 인증값으로 외부에 문을 여는 대신 실행 자체를 중단했습니다. 배포 실패이기는 했지만, 보안 장치가 의도대로 작동한 결과였습니다.
토큰을 정상적으로 등록한 뒤에도 인증 검사에서는 401 오류가 났습니다. 이번에는 토큰이 문제가 아니었습니다. 검사 프로그램이 서버가 요구한 token 칸이 아니라 access라는 다른 칸에 값을 넣고 있었습니다. 저는 처음에 이를 토큰 불일치로 생각해 정상 Secret을 다시 등록했습니다. 토큰을 복사하기 위해 만든 임시 HTML에도 실제 값 대신 입력 자리 표시 문구가 남아 있었습니다.
같은 ‘인증 실패’처럼 보여도 원인은 각각 달랐습니다. 서버는 잘못된 값을 계획대로 거절하고 있었습니다. 실제 문제는 토큰을 전달하는 임시 파일과 인증 여부를 확인하는 검사 프로그램에 있었습니다.
이 일을 겪으며 환경변수는 이름만 같다고 같은 설정이 아니라는 것을 알았습니다. 로컬과 Render에 모두 SBIZ_API_KEY라는 항목이 있어도 실제 값은 다를 수 있습니다. 값을 수정한 뒤 새 배포가 그 값을 읽었는지, 서버 코드가 정확한 변수 이름을 찾는지, 검사 프로그램이 서버가 약속한 요청 형식을 사용하는지까지 확인해야 했습니다.
교정 후에는 Render의 Docker 서버 한 대를 대상으로 실제 외부 HTTPS 접속부터 권한 분리까지 다시 확인했습니다. 참가자는 검색과 북마크를 사용할 수 있고, 발표자만 관제 화면과 삭제 기능에 접근할 수 있었습니다. 인증 쿠키가 HTTPS에서만 전송되고 브라우저 JavaScript에는 노출되지 않는지도 확인했습니다. 마지막으로 공단 상가 검색, 북마크 생성, 관제 화면의 실시간 반영, 검증용 데이터 삭제까지 한 흐름으로 실행했습니다. 이 시점의 전체 자동 테스트는 238개였습니다.
하지만 여기까지도 휴대전화에서 실제로 된다는 뜻은 아니었습니다.
4. LTE에서 접속은 성공했는데 검색이 안 됐습니다
실제 휴대전화·LTE 접속을 확인하는 Gate 9에서 있었던 일입니다.
참가자 화면이 열렸고 지도도 떴습니다. 주소에 있던 긴 접근 정보도 로그인 뒤 사라졌습니다. 여기까지만 보면 성공 같았습니다.
상가 검색 버튼을 누르기 전까지는요.
화면에는 검색을 완료하지 못했습니다.가 나타났습니다.
같은 세션을 재현해보니 위치 검색은 200이었지만 공단 상가 검색은 502였습니다. 로컬 Docker에서는 같은 공단 API가 정상 동작했고 Render에서만 반복해서 실패했습니다. 비밀값 자체를 화면에 출력하지 않고 두 환경의 키 지문을 비교한 결과, Render에 등록된 SBIZ_API_KEY가 로컬의 정상 키와 달랐습니다.
키 하나를 바로잡고 다시 배포하자 코엑스 100m 검색이 200으로 돌아왔고, 전체 252개 상가가 조회됐습니다. 북마크도 휴대전화에서 저장됐습니다.
이 사건은 자동 테스트가 쓸모없다는 뜻이 아니었습니다. 자동 테스트는 코드 계약과 많은 실패 경로를 빠르게 확인했습니다. 다만 다음 항목은 서로 다른 증거였습니다.
사이트가 열린다
≠ 지도가 뜬다
≠ 외부 API 검색이 된 다
≠ 북마크가 된다
≠ 실제 휴대전화에서 쓰기 편하다5. 아직 Zoom을 켠 환경에서의 테스트가 남았습니다
현재 Gate 9까지는 통과했습니다.
실제 휴대전화 LTE/5G 접속
QR과 개인 메신저 링크 진입
외부 HTTPS에서 지도·상가 검색·북마크
모바일 결과 목록 탈출과 열기·닫기 상태
전체 테스트 238개
하지만 이 글을 쓰는 지금, 가장 처음의 문제였던 Zoom 동시 실행은 아직 다시 시험하지 않았습니다.
Render에서 서버가 잘 돈다는 사실은 발표 노트북이 Zoom, 화면 공유, 발표자 브라우저를 함께 안정적으로 처리한다는 증거가 아닙니다. 20여명이 같은 네트워크에서 동시에 검색할 때 세션별 제한과 공유 IP 제한이 의도대로 작동하는지도 아직 실제 조건에서 확인해야 합니다.
다음 Gate 10에서 확인할 것은 다음과 같습니다.
Zoom 카메라·마이크·화면 공유를 모두 켠다.
발표자 브라우저와 관제 화면을 실제 순서대로 조작한다.
20명 조건의 검색 부하와 SSE 연결을 함께 발생시킨다.
오류율, 응답 지연, 화면 멈춤, Zoom 음성 경고를 기록한다.
결과에 따라 동시 참여 인원을 줄이거나 녹화 fallback으로 전환한다.
지난 발표에서는 발표 직전에 슬라이드를 고치면서 리허설을 건너뛰었습니다. 이번에는 기능이 완성됐다는 말보다 발표 조건에서 실제로 버티는지를 먼저 보려고 합니다.
Docker를 버리고 Render로 간 이야기도 아니고, 관리형 서버가 정답이었다는 이야기도 아닙니다.
이번에 바뀐 것은 질문이었습니다.
예전에는 “서버가 실행되는가”를 확인했다면, 이제는 “어떤 컴퓨터에서, 어떤 데이터를 얼마나 오래 보존하며, Zoom과 청중이 함께 있는 조건에서도 실행되는가”를 확인하고 있습니다.