이 글은 새 기능 이야기가 아니다. AI-Morning-Brief는 개인적으로 쓰려고 만든, 매일 아침 AI·개발 뉴스를 모아 요약해주는 자동화 파이프라인이다. 이번에 한 일은 이 파이프라인에 뭔가를 더 만든 게 아니라, 이미 완성돼서 잘 돌던 걸 어디서 돌릴지 통째로 옮긴 것이다. 계기는 아주 사소했다. 집 공유기가 말썽을 부린 날이었다.
며칠 전, 집 공유기 문제로 AI-Morning-Brief의 자동 실행이 끊긴 적이 있었다. 자동 실행 실패 원인을 점검하고 실행용 repo와 저장용 repo 상태를 정리했고, 공유기가 복구된 뒤에는 일간·주간 자동 리포트가 다시 정상 동작하는지 따로 확인했다. 실제로 서비스가 완전히 죽어버린 건 아니었다 — 이 파이프라인은 원래부터 "어떤 이유로든 실행이 밀리거나 실패해도 다음 실행이 따라잡는다"는 복구 장치(진행 커서 기반의 catch-up 로직)를 이미 갖고 있었고, 공유기가 돌아온 뒤 다시 확인했을 때 리포트는 정상적으로 이어지고 있었다. 다만 정확히 그 복구 장치가 작동한 건지, 그냥 다음 정기 실행이 잘 돈 건지는 따로 확인하지 않았다.
서비스가 알아서 복구된 건 다행이었다. 그런데 며칠 지나고 다시 생각해보니 마음이 편치 않았다. "이번엔 운 좋게 넘어갔는데, 다음에도 그럴까?"라는 질문이 계속 남았다. 왜냐하면 이 파이프라인이 서 있는 바닥 자체가 애초에 개인 홈 네트워크와 개인 노트북이었기 때문이다.
돌아보면 이번 공유기 사건 하나 때문에 이 결정을 내린 게 아니다. 그 전부터 로컬 운영 구조에는 계속 자잘한 균열이 있었다.
macOS는 ~/Desktop 같은 위치를 개인정보 보호 대상 폴더로 취급한다. 그 결과 정기 실행 스케줄러(launchd)가 띄운 파이썬 프로세스가 파일에 접근하지 못해 매번 "권한 없음" 오류로 실패하는 문제가 있었다.
이걸 우회하려고 개발하는 위치와 실제로 정기 실행이 도는 위치를 아예 분리해야 했다. 코드는 한 곳에서 고치고, 정기 실행은 보호 폴더 밖 별도 배포본에서 도는 구조를 계속 유지해왔다.
정기 실행 자체가 집 공유기(네트워크)와 개인 맥북 전원에 의존하는 구조였다. 맥이 꺼져 있거나, 집 네트워크가 끊기거나, 내가 여행이나 외출로 자리를 오래 비우면 그대로 영향을 받는 구조였다.
이 세 가지는 전부 이번 사건 이전부터 이미 알고 있던 문제였다. 이번 공유기 사건은 그중 하나가 실제로 터진 것뿐이었다.
그래서 이번 결정을 "공유기가 고장 나서 클라우드로 옮겼다"고 정리하면 정확하지 않다. 언젠가는 이 개인 인프라 구조를 벗어나 클라우드로 옮겨야 한다는 생각은 이전부터 갖고 있었다. 이번 공유기 사건은 그 생각을 실행으로 옮기는 시점을 앞당긴 계기였을 뿐이다. 원인이 있고 결정이 뒤따른 게 아니라, 이미 방향은 정해져 있었는데 이 사건이 "지금 하자"로 밀어준 셈이다.
방향을 정한 뒤로는 기술을 나열하는 것보다 판단이 훨씬 많은 시간을 잡아먹었다.
왜 코드 저장소의 자동화 기능이 아니라 클라우드 배치 실행 서비스인가. 이 파이프라인은 하루 한 번, 1분 안팎으로 끝나는 짧은 배치 작업이다. 이 정도 규모에서는 두 방식의 비용 차이가 사실상 없었고, 대신 이미 쓰고 있던 클라우드 생태계(상태 저장·비밀값 관리 등)와의 결합도가 더 자연스러운 쪽을 택했다.
왜 매니지드 DB로 옮기지 않고 SQLite를 그대로 뒀는가. 지금 DB는 테이블 하나, 수백 킬로바이트 크기, 쓰는 주체도 하나뿐이다. 매니지드 DB가 진짜 이기는 조건(동시에 여러 곳에서 쓰기, 대용량, 실시간 조회)에 하나도 해당하지 않았다. 지금 옮기면 그냥 과잉 구현이라고 판단해서 보류했다.
왜 상태 동기화를 "다운로드 → 처리 → 업로드 + 버전 불일치 시 실패" 방식으로 했는가. 클라우드 저장소를 로컬 폴더처럼 마운트해서 쓰는 방식은 편하지만, 동시에 두 실행이 겹치면 나중 것이 앞의 것을 조용히 덮어써 버릴 위험이 있다. 그래서 업로드 시점에 "내가 받아갔던 버전 그대로인가"를 검증하고, 아니면 조용히 지워지는 대신 명시적으로 실패하게 만들었다. 겹침 자체를 막는 게 아니라, 겹쳤을 때 눈에 보이게 만드는 것이 목적이었다.
왜 비밀값 관리 체계를 새로 도입했는가. 그동안 API 키와 웹훅 주소는 로컬 파일에 평문으로 있었다. 실행 주체가 클라우드로 넘어가는 김에 이 값들도 전용 비밀값 관리 체계로 옮겼다.
한 가지 흥미로웠던 건, 애초에 로컬 스케줄러를 골랐던 이유(맥이 잠들어 있어도 깨어난 직후 놓친 작업을 실행해준다) 자체가 클라우드의 상시 가동 환경에서는 더 이상 성립하지 않는 전제가 됐다는 점이다. 이게 그때 판단이 틀렸다는 뜻은 아니고, 그 판단이 전제했던 실행 환경(개인 맥) 자체가 바뀐 것뿐이다.
방향과 결정을 정리한 것과, 실제로 붙여서 돌아가게 만드는 것은 역시 다른 일이었다.
첫 번째로, 로컬에서 만든 컨테이너 이미지를 그대로 올렸더니 최초 배포 시도가 실패했다. 로컬 개발 환경의 기본 아키텍처와 클라우드 실행 환경의 기본 아키텍처가 서로 달라서 생긴 문제였고, 다시 빌드해서 해결했다.
두 번째로, 배포 상태를 확인하는 과정에서 헷갈린 적이 있었다. 여러 클라우드 프로젝트를 운영 중인 상태라, 확인 시점에 활성화돼 있던 프로젝트가 실제로 이 파이프라인을 배포한 프로젝트가 아니었다. 처음엔 "혹시 아직 배포가 안 된 건가" 하고 당황했는데, 전체 프로젝트 목록을 다시 훑어보고서야 실제로는 별도로 분리해둔 전용 프로젝트에 정상적으로 배포돼 있는 걸 확인했다. 사실 이 글을 위해 오늘 다시 확인하는 과정에서도 같은 패턴이 반복됐다 — 활성 프로젝트만 보고 "리소스가 없다"고 오판했다가, 전체 목록을 보고서야 진짜 상태를 확인했다.
이 외에도 하나 더 있었다. 작업 중간에 GitHub 웹에서 보이는 오늘 자 리포트 파일과 로컬에 있는 같은 파일의 내용이 서로 다르게 보이는 순간이 있었다. 그런데 이때 정확히 무엇과 무엇을 비교하고 있는지 기준이 흐트러지면서, 문제를 맥 전체에 있는 같은 이름의 파일들을 다 뒤져보는 방향으로 키워버린 적이 있었다. 그럴 때 그냥 넘기지 않고 비교 기준을 다시 못박고 반례를 짚어주자 판단이 바로 제자리를 찾았다. 이걸 "역시 AI는 못 믿겠다"는 식으로 결론 내리고 싶지는 않다. 오히려 작업이 길어질수록 "지금 무엇과 무엇을 비교하고 있는가" 같은 검증 기준을 사람이 계속 손에서 놓지 않고 쥐고 있어야 한다는 걸 다시 확인한 쪽에 가깝다.
이전 전에는 개인 맥북 위에서 정기 실행 스케줄러가 파이썬 파이프라인을 돌리고, 그 결과를 Discord로 보내는 구조였다. 이전 후에는 클라우드 스케줄러가 정해진 시각에 클라우드 배치 실행을 깨우고, 그 실행이 상태 저장용 클라우드 스토리지를 읽고 쓰며 파이프라인을 돌리고, 끝나면 리포트를 코드 저장소에 자동으로 반영하고, 그다음 Discord로 전달되는 구조로 바뀌었다.
오늘 기준으로 일간·주간·월간 세 가지 배치 실행이 모두 배포돼 있고, 대응하는 스케줄러 세 개도 모두 활성 상태로 돌아가고 있다(기존 로컬 스케줄러 3종의 실행 시각을 그대로 옮겼다). 비밀값 관리 체계에는 API 키·웹훅 주소·저장소 접근 토큰 세 가지가 이미 이관돼 있고, 상태 저장용 클라우드 스토리지에는 DB 파일과 원본 수집 데이터가 오늘 날짜로 쌓이고 있는 것도 확인했다. 실행 로그를 보면 오늘 아침 실제 자동 실행도 성공적으로 일어났다. 재밌게도 코드를 정식으로 저장소에 반영하는 커밋보다, 클라우드에서 자동 실행된 결과로 리포트가 먼저 자동 커밋된 순서까지 로그에 남아 있었다 — 코드를 "완성"으로 선언하기 전에 실제로 이미 배포되고 검증까지 끝나 있었다는 뜻이다.
다만 다 끝난 건 아니다. 리포트를 저장소에 자동으로 올리는 방식은 가벼운 API 호출 방식(GitHub Contents API)으로 확정했고, 실제로 오늘 자 리포트 파일이 이 방식으로 자동 커밋되는 것까지 확인했다. 저장소를 통째로 내려받아 커밋하는 방식도 후보에 있었지만 최종적으로는 채택하지 않았다. 다만 기존 로컬 방식을 완전히 걷어내기 전에 클라우드와 로컬을 한동안 나란히 돌려보고 비교하는 절차도 원래 계획에 있었는데, 실제로 그 기간을 다 채웠는지는 확인하지 못했다.
정작 중요한 건, 파이프라인 로직 자체 — 뉴스를 모으고, 분석하고, 리포트를 만드는 코드 — 는 단 한 줄도 바뀌지 않았다는 점이다. 바뀐 건 그 코드가 어디서 실행되고, 중간 상태를 어디에 저장하느냐뿐이다. 그동안 정기 실행을 도맡아온 로컬 배포 폴더는 정리했고, 나중에 필요하면 되돌아볼 수 있도록 백업 압축파일만 남겨뒀다. 기존 로컬 스케줄러 등록도 비활성화하고 격리해뒀다. 이제 정기 실행의 정본은 클라우드 스케줄러와 클라우드 배치 실행이고, 로컬에는 개발과 문서 관리를 위한 저장소만 남아 있다.
이번 작업에서 가장 신경 쓴 건 "무엇을 더 만들까"가 아니라 "이미 만든 걸 어떻게 안전하게 옮길까"였다. 새 CLI 옵션도, 새 분석 로직도 추가하지 않았다. 그래서 이번 기록은 기능 개발기가 아니라 운영 이관기에 가깝다.
돌아보면 이번에 한 일은 단순히 "서버를 옮겼다" 정도로 정리하고 싶지 않다. 예전에는 내 맥북이 켜져 있고 집 네트워크가 살아 있어야만 아침에 리포트가 왔다. 내가 여행을 가거나, 맥북을 닫아두거나, 공유기가 또 한 번 말썽을 부리면 그날 리포트는 그대로 흔들릴 수 있는 구조였다. 이제는 다르다. 내가 자고 있든, 밖에 나가 있든, 집 공유기가 또 말썽을 부리든 상관없이 시스템은 정해진 시각에 알아서 실행되고 알아서 리포트를 만든다. 결국 이번에 배운 건 하나다. 운영은 내 생활 상태에 기대서는 안 되고, 나와 완전히 분리된 시스템으로 서 있어야 한다는 것. 공유기가 말썽을 부린 게 아니었으면 이 결정을 이렇게 빨리 실행에 옮기지는 않았을 것 같다. 그런 의미에서 이번 장애는 성가신 사건이었지만, 동시에 미뤄뒀던 숙제를 꺼내게 만든 계기였다.