사례 4주차 2026-08-13 · AI 개인비서 텔레그램봇 만들기 연재

돌아가는 코드를 갈아엎는 법 — 출력을 얼려놓고 구조만 바꾸기

3주간 기능을 붙이다 보니 한 함수가 읽기·계산·전송을 다 하고 있었습니다. 클린 아키텍처의 축소판으로 정리하되, "바꾸기 전 출력"을 저장해두고 바이트 단위로 대조하는 골든 테스트로 안전하게 갈아탔습니다.

브리핑 봇을 3주 동안 키우다 보니 코드가 뒤엉켰습니다. 예산 구분을 추가하려고 함수를 열었더니, 그 함수가 시트를 읽고 → 계산하고 → 문장을 만들고 → 텔레그램으로 보내는 것까지 혼자 다 하고 있더군요. 어디를 고쳐야 할지 찾는 시간이 고치는 시간보다 길어졌습니다.

그래서 AI에게 물었습니다. “이거 클린 아키텍처로 바꾸고 싶어.”

교과서 그대로는 답이 아니었다

돌아온 첫 대답이 뜻밖이었어요. “교과서식 클린 아키텍처는 여기엔 안 맞습니다.”

정식 클린 아키텍처는 계층 4개에 의존성 규칙까지 갖춘 구조인데, 제가 쓰는 앱스크립트에는 그걸 지탱할 기능(모듈 분리 같은 것)이 없답니다. 억지로 흉내 내면 이득 없이 코드만 세 배가 된다고요.

대신 핵심 아이디어 하나만 가져왔습니다.

계산하는 코드와 바깥세상(시트·API·메신저)을 만지는 코드를 섞지 않는다.

식당에 비유하면 이렇습니다. 예전 코드는 한 사람이 주문받고 장 보고 요리하고 서빙까지 하는 가게였어요. 바뀐 구조는 역할을 나눈 가게입니다.

① 설정    = 레시피 수첩   (예산 금액, 분류 키워드 — 숫자만 고치는 곳)
② 코어    = 주방          (재료를 받아 요리만. 바깥 접촉 금지)
③ 어댑터  = 장보기·배달   (시트 읽기, API 호출, 메시지 전송)
④ 진입점  = 홀 매니저     ("장 봐와 → 주방에 → 배달 보내" 지시만)

실제 흐름으로 그리면 이렇습니다. 밤 브리핑 하나가 도는 순서예요.

시간 트리거 아침 8시 · 밤 10시 웹훅 요청 결제 알림 · 완료 처리 ④ 진입점 — 조립만 한다 밤브리핑() · doPost() · route() ③ 어댑터 — 읽기 시트 · 캘린더 · 날씨 API ① 설정 예산 · 키워드 카드 혜택표 ② 코어 — 순수 계산 집계 · 분류 판정 · 문장 만들기 시트·인터넷 접촉 금지 ③ 어댑터 — 쓰기·전송 sendTG() · 시트 기록 · 응답 텔레그램 도착 다섯 파일 모두 같은 구조 — 카드추적 · 밤브리핑 · 카드파트 · 모닝브리핑 · 업무일정 보라 = 순수 계산 · 청록 = 바깥 접근 · 회색 = 구조

이득은 명확합니다. 고장 나면 볼 곳이 반으로 줄어요. 숫자가 틀렸다 → 주방 문제. 메시지가 안 왔다 → 배달 문제. 예전엔 한 함수 안에서 둘 다 의심해야 했습니다.

진짜 문제: “고치다가 망가뜨리면?”

구조를 바꾼다는 건 결국 잘 돌아가는 코드를 갈아엎는다는 뜻입니다. 그리고 이 봇은 매일 아침저녁으로 실제로 쓰는 물건이에요.

더 무서운 건, 이런 개편에서 생기는 버그는 에러 없이 조용히 틀리는 종류라는 겁니다. 코드를 옮기다 한 줄이 미묘하게 달라져도 브리핑은 멀쩡히 도착합니다. 숫자만 슬쩍 다를 뿐이죠. 며칠씩 모를 수 있습니다.

여기서 배운 게 이번 사례의 핵심입니다. 골든 테스트라는 방법이에요.

골든 테스트: 출력을 얼려놓고 시작한다

순서가 중요합니다. 코드를 고치기 전에:

  1. 가짜 입력(거래 기록 배열, 가짜 캘린더 일정)을 준비하고
  2. 지금 코드가 만드는 출력(브리핑 전문)을 파일로 저장합니다 — 이게 “정답지”
  3. 그다음에 구조를 갈아엎고
  4. 같은 입력을 넣어 글자 하나까지 같은지 대조합니다
바꾸기 전:  입력 → [옛 코드] → 출력 저장 (정답지)
바꾼 후:    입력 → [새 코드] → 출력 = 정답지 ?
                                ├─ 같다  → 구조만 바뀐 것 ✅
                                └─ 다르다 → 어딘가 실수함, 몇 번째 줄인지 표시

실제로 대조는 “대충 비슷”이 아니라 바이트 단위로 했습니다. 공백 하나, 쉼표 하나 달라도 실패로 처리했어요. 리팩터링의 성공 기준은 “더 좋아졌다”가 아니라 “달라진 게 없다” 이니까요.

파일 다섯 개를 이 방식으로 하나씩 갈아탔고, 다섯 번 모두 정답지와 일치했습니다.

부수입: 테스트가 정직해졌다

구조를 나누고 나니 예상 못 한 이득이 따라왔습니다.

예전에는 계산 코드가 시트 읽기와 붙어 있어서, 테스트하려면 구글 시트 전체를 흉내(mock) 내야 했습니다. 그 흉내 장치가 진짜 코드보다 자주 깨졌어요.

지금은 주방(코어)이 배열만 받으니, 배열을 넣고 답을 확인하면 끝입니다. 흉내 낼 게 없어요. 심지어 이런 덫도 놓을 수 있게 됐습니다:

// 코어를 테스트할 때 시트 접근을 '지뢰'로 바꿔둔다
SpreadsheetApp = { getActiveSpreadsheet: () => { throw new Error('코어가 시트를 만지면 안 됨!'); } };

코어가 규칙을 어기고 몰래 시트를 만지면 테스트가 그 자리에서 터집니다. “분리했다”는 말을 믿는 게 아니라 어기면 터지게 만든 거예요.

남는 교훈

1. 리팩터링은 “정답지 저장”부터 시작하세요.

바꾸기 전 출력을 얼려두지 않으면, 바꾼 후에 “잘 된 것 같다”는 느낌만 남습니다. 느낌은 조용한 버그를 못 잡습니다.

2. 교과서 구조는 번역해서 쓰는 겁니다.

제 환경(앱스크립트, 혼자 쓰는 자동화)에 4계층은 과했습니다. 핵심 원리 하나만 가져와 축소판을 만드는 게, 형식을 다 갖추는 것보다 나았습니다. AI에게 “이 교과서 개념, 내 상황에는 어디까지가 적당해?”라고 묻는 게 유효했어요.

3. 한 번에 갈아엎지 마세요.

파일 다섯 개를 한꺼번에 바꿨다면 뭐가 잘못됐는지 못 찾았을 겁니다. 하나 바꾸고 → 정답지 대조 → 통과하면 다음. 지루하지만 이게 제일 빨랐습니다.

한계도 적어둡니다

골든 테스트는 정답지를 만들 때 밟은 길만 보증합니다. 가짜 입력이 안 지나간 구석의 실수는 못 잡아요. 그래서 자주 쓰는 기능 위주로 가짜 입력을 짰고, 나머지는 실제 브리핑을 받아보며 확인했습니다. “전부 검증했다”와 “주요 경로를 검증했다”를 구분해서 말하는 것도 이번에 배운 것 중 하나입니다.