
구현의 느림은 의사결정의 게이트였다 ── AI로 쌓이는 비용을 구조로 막기
요약
AI 도입으로 구현 속도가 빨라지면서 발생하는 러닝 코스트 문제를 구조적으로 해결하는 방법을 다룹니다. 비용의 입구에 임계치 기반의 게이트를 설치하고 일일 가시화를 통해 비용 폭증을 방지하는 전략을 제시합니다.
핵심 포인트
- 구현 속도 향상은 의사결정 게이트를 제거하여 러닝 코스트 증가를 초래함
- 사후 비용 안분이 어려운 구조에서는 비용 발생 전 입구에서 차단하는 것이 중요함
- AI 리뷰어는 수량(스캔량, 토큰 등)만 측정하고, 금액 계산은 결정론적 스크립트에 맡겨 신뢰성 확보
- 단순 비용 절감보다 비용의 가시화와 임계치 기반의 인간 리뷰 프로세스가 더 효과적임
여러분 안녕하세요! 에어클로젯(airCloset)에서 CTO를 맡고 있는 츠지입니다.
사내 AI 기반의 운영 비용을 일주일 동안 점검했습니다. 결과를 먼저 말씀드리면:
- Cloud Run의 instance-based 과금은 84% 감소 (카나리아 1개 서비스 단독으로는 44% 감소)
- 컨테이너 이미지의 보존 세대를 재검토하여 스토리지 65% 감소
- 가장 효과적이었던 것은 감소 대책 그 자체가 아니라, 비용의 입구에 배치한 게이트와 일일 가시화였습니다.
다만, 이 글에서 쓰고 싶은 것은 절감액이 아닙니다. 구현 비용이 낮아진 결과, "정말로 그것이 필요한가"를 묻는 시간만이 상대적으로 높아졌다는 이야기입니다.
구현의 느림은 의사결정의 게이트였다
AI 자체에 대한 이야기는 아닙니다. 하지만 AI로 개발하면 이 부분이 확실히 불어난다는 전제부터 시작하겠습니다.
지금까지 무언가를 만들기 위해서는 구현 비용이 들었습니다. 견적을 내고, 설계를 리뷰하고, 공수를 확보합니다. 이 일련의 절차는 번거롭지만, 부작용으로서 "정말로 이것이 필요한가"를 묻는 시간을 강제적으로 만들어 주었습니다. 구현이 느리다는 것 자체가 의사결정의 게이트(Gate)로서 기능하고 있었던 것입니다.
AI로 구현이 빨라지면 이 게이트가 사라집니다. 떠오른 구조를 그날 안에 배포할 수 있습니다. 이는 순수하게 좋은 일이며, "만들어 보지 않으면 모르니까 만든다"라는 판단이 옳은 상황은 확실히 늘어났습니다.
하지만 동시에, 만들기로 하는 판단을 유보할 인센티브도 사라집니다. 하나하나가 작기 때문에 개별적으로는 아무도 신경 쓰지 않습니다. 그리고 남는 것은 구현 공수가 아니라, 만든 것이 계속 돌아가는 러닝 코스트(Running Cost)입니다. 이는 만든 순간에는 보이지 않고, 나중에 조용히 쌓여갑니다.
기점은 "사후에 앱 단위로 안분할 수 없다"는 구조
계기는 특정 변경을 기점으로 종량제 과금이 예상보다 늘어나는 상황이 몇 번 있었기 때문이었습니다. BigQuery의 스캔량과 Vertex AI / Gemini의 실행량입니다.
까다로웠던 점은, 사후에 앱 단위로 안분(Allocation)할 수 없었다는 것입니다. BigQuery의 쿼리 작업에는 라벨을 붙일 수 있고 실행 작업의 이력도 남습니다. 다만 저희 환경에서는 여러 앱이 동일한 서비스 계정(Service Account)으로 동작하고 있었기 때문에, 실행 주체를 보더라도 "어느 앱이 사용했는지"까지 명확히 나눌 수 없었습니다. 라벨을 모든 경로에 철저히 적용하는 방법도 있지만, 그것이 전파될 때까지는 같은 상태가 지속됩니다.
사후에 안분할 수 없다면, 늘어나기 전에 막을 수밖에 없습니다. 여기서부터 "비용의 입구에 게이트를 둔다"라는 발상이 나왔습니다.
모니터링과 가시화는 감소 대책보다 먼저 필요하다
결론을 먼저 쓰자면, 이 일련의 작업에서 가장 효과적이었던 것은 개별적인 감소 대책이 아니라, 비용의 입구에 배치한 게이트와 일일 가시화였습니다. 감소는 한 번 하면 끝나지만, 방치하면 다시 쌓이기 때문입니다.
입구의 게이트: 임계치를 초과하는 변경은 인간 리뷰로
AI 리뷰어의 관점에 비용을 추가하여, 종량제 과금의 증가가 임계치를 초과하는 PR은 인간 리뷰로 인계하도록 했습니다. 임계치는 3종류입니다.
| 구분 | 임계치 |
|---|---|
| 정기적인 일일 비용 증가 | 2,000엔/일 |
| ... |
설계에서 의식한 것은, AI에게 금액 계산을 시키지 않는 것입니다. AI가 하는 일은 변경 사항을 읽고 "수량" (스캔량, 실행 횟수, 토큰 수)을 실측하는 것뿐입니다. 엔(Yen) 환산과 임계치 판정은 결정론적인 스크립트가 수행합니다. AI에게 금액 계산까지 맡기면 산술 실수, 단가 착오, 실행 시마다의 편차로 인해 게이트의 신뢰성이 떨어지기 때문입니다. 판단이 흔들리는 게이트는 없는 것과 마찬가지입니다.
단가는 카탈로그 가격이 아니라, Billing Export의 실측값(지불액 ÷ 사용량)을 사용하고 있습니다. 카탈로그 가격을 하드코딩하면 USD 기준, 환율, 약정 할인(Committed Use Discount)의 3중 오차가 발생하기 때문입니다.
스토리지(Storage)를 별도로 분류한 이유는, 누적되는 비용은 실행 시의 비용과 성질이 다르기 때문입니다. 일일 실행 비용은 멈추면 제로로 돌아가지만, 기록된 데이터는 삭제할 때까지 과금이 계속됩니다. 그래서 "일정 기간이 지나면 삭제되도록 설정되어 있는가", "매월 예상 비용의 증가분이 얼마인가"를 실행 횟수 × 실행량으로부터 미래의 견적으로서 산출하게 하고 있습니다.
지속적인 가시화: 하루 2번, Slack에 게시
앱별 비용을 하루 2번 Slack에 게시하고 있습니다. 여기에도 몇 가지 설계 판단이 있습니다.
임계치 초과가 없는 날도 반드시 게시합니다. "오늘은 문제가 없다"라는 정보 자체에 가치가 있으며, 게시가 멈춘 것인지 정상인지 수신자가 구분할 수 있어야 하기 때문입니다. 무음의 모니터링은 고장 나 있어도 알아차릴 수 없습니다.
비교는 전일 대비가 아니라 전주 동일 요일 대비입니다. 요일에 따라 업무량(배치 실행량)이 달라지기 때문에, 전일 대비로 설정하면 월요일이 매번 스파이크(Spike)로 처리되어 금방 아무도 보지 않게 됩니다.
전주에 데이터가 없는 app (baseline = 0)도 증가로 감지합니다. 이 부분이 가장 위험한 지점이기 때문입니다. 새로 실행되기 시작한 배치야말로 예상치 못한 비용을 발생시킵니다. "전주 대비 ○%"라는 지표는 전주가 0일 때 정의할 수 없어 놓치기 쉬우므로, 명시적으로 케이스를 나누고 있습니다.
마지막 방파제: 종량제에는 천장을 설정한다
게이트와 가시화를 도입해도 우회하는 사례는 발생합니다. 게이트를 통하지 않는 경로(수동 실행, 외부 요인, 애초에 예상하지 못했던 사용 방식)가 있기 때문입니다.
따라서, 사용하면 사용할수록 과금이 늘어나는 서비스에는 운영 실태에 맞춘 상한을 설정해 두는 것이 마지막 방파제가 됩니다. BigQuery나 Vertex AI / Gemini와 같은 종량제(Pay-as-you-go)는 쿼터(Quota)로 천장을 끊을 수 있습니다.
중요한 것은, 상한을 "제도상의 최대치"가 아니라 "통상 시의 실측치"로부터 결정하는 것입니다.
BigQuery를 예로 들면, 온디맨드(On-demand) 과금의 기본 상한은 1 프로젝트당 200 TiB/일입니다 (2025년 9월 이후. 그 이전에는 무제한이 기본이었습니다). 온디맨드 단가는 리전(Region)에 따라 다르지만, 1 TiB당 수 달러 수준이므로, 이 천장은 하루에 천 수백 달러에 해당합니다. 한 달 내내 풀로 사용하면 수백만 엔 규모입니다.
즉, 기본 설정이란 "매달 그만큼 사용해도 아무 말 없이 실행된다"는 의미입니다. 이것은 천장으로서 기능하지 않습니다.
우리는 BigQuery의 스캔량에 대해 통상 시의 1.5배 정도로 설정하고 있습니다. "여유 있게 10배"도 마찬가지로, 폭주했을 때 멈추는 위치가 되어야 비로소 의미가 있습니다.
다만, 이 설정을 할 수 있는 것은 사내용 기반(Infrastructure)이기 때문입니다. 이 점은 전제로 명기해 둡니다. 1.5배에서 끊는다는 것은 "상한에 걸리면 멈춘다"는 것을 받아들인다는 뜻이며, 사내 기반이라면 다음 날 재실행하면 그만이지만, 고객 대상 서비스에서 같은 일을 하면 장애가 됩니다. 멈춰도 되는 것에만 이 천장을 설정할 수 있습니다. 엔드 유저에게 영향을 주는 경로라면, 천장을 더 높게 설정한 뒤 알람(Alert)과 자동 스케일링(Auto-scaling) 설계로 대응해야 합니다.
그 전제하에, 사내 기반에서는 부작용도 허용할 수 있습니다. 정상적인 처리가 상한에 걸려 떨어지는 일은 일어날 수 있지만, 그 경우에는 "왜 통상 시의 1.5배를 사용하는 처리가 실행되었는가"를 확인하는 기회가 되므로, 그 자체로 탐지(Detection)로서 기능합니다. 알람은 사후에 알려주지만, 쿼터는 그 자리에서 멈춥니다. 이 차이는 하루 만에 크게 움직일 수 있는 종량제에서 효과를 발휘합니다.

이러한 "만들기 전에 묻기"를 사람의 마음가짐으로서 운영하면, 바쁜 날에는 반드시 놓치게 됩니다. 시스템(Mechanism)으로 만들어야 비로소 기능한다는 것이 이번에 확실히 알게 된 점이었습니다. 게이트(입구에서 막기), 일간 가시화(지속적으로 보기), 쿼터(폭주를 천장에서 막기)의 3개 층을 갖추어야 비로소 "보이지 않던 것이 보이는" 상태가 됩니다.
실례 1: 기본값이 높은 쪽이라면, 빨리 만들수록 부채가 쌓인다
여기서부터는 재고 조사 과정에서 발견된 내용입니다. 우선 Cloud Run부터 살펴보겠습니다.
Cloud Run에는 두 가지 과금 모델이 있습니다. CPU를 상시 할당하는 instance-based와, 요청 처리 중에만 할당하는 request-based입니다. 조사한 결과, Cloud Run 비용의 97%가 instance-based였습니다.
원인은 구조적이었습니다. 리포지토리 내에서 Cloud Run 서비스를 정의하는 부분을 보면, cpuIdle을 지정하지 않은 것이 다수 존재하는 상태였습니다. 공통 팩토리 함수(Factory function)가 없기 때문에, 새로운 서비스를 추가할 때마다 아무것도 적지 않으면 계속 높은 쪽(비싼 쪽)으로 기울게 됩니다.
여기서 주의할 점은, 콘솔이나 gcloud를 통해 보통 데포이(Deploy)했을 때의 기본값은 request-based(저렴한 쪽)라는 점입니다. 그런데 IaC로 선언적으로 정의하고 리소스량을 명시적으로 적으면라는 역전 현상이 일어납니다. IaC로 관리할수록 이 차이를 알아차리기 어려운 구조가 되어 있는 것입니다.
cpuIdle이 false 취급되어 instance-based로 떨어집니다. 즉, 손으로 만들면 저렴한 쪽, 코드로 만들면 비싼 쪽—이것이 바로 서두의 이야기와 연결됩니다. 기본값이 높은 쪽에 설정되어 있으면, 만드는 속도가 빨라질수록 부채가 자동으로 쌓입니다. 서비스당 차이는 작기 때문에 개별적으로는 아무도 알아차리지 못합니다.
이름이 의미와 반대로 읽히다
여기서 하나의 함정이 있었습니다. 이 설정은 cpuIdle라는 이름을 가지고 있습니다.
| 값 | 의미 | 과금 방식 |
|---|---|---|
true | CPU는 요청 처리 중일 때만 할당 | request-based (저렴) |
false (IaC 시 기본값) | CPU 상시 할당 | instance-based (비쌈) |
true를 '유휴 상태(idle)에도 계속 움직인다'고 읽으면 반대가 됩니다. 저 자신도 작업 중에 한 번 착각했습니다. 게다가 잘못했을 때의 파괴 방식이 까다로워서, 예외를 발생시키지 않고 응답을 보낸 후의 처리만 소리 없이 누락됩니다. fire-and-forget 방식으로 백그라운드 처리를 하는 서비스를 실수로 true로 설정하면, 오류 로그도 남기지 못한 채 처리가 사라집니다.
그래서 판단 기준을 '응답 후에 처리를 계속하는 서비스인가(fire-and-forget)'라는 하나의 축으로 재정리했습니다.
제외 조건은 생각보다 좁다
변경 대상을 고를 때, 처음에는 '상시 CPU가 필요할 것 같은 서비스'를 상당히 넓게 제외하고 있었습니다. 작업을 진행하면서, 그중 상당수가 제외할 필요가 없다는 것을 알게 되었습니다. 이유는 Cloud Run의 동작 방식에 비자명적인 점이 2가지 있기 때문입니다.
첫 번째는 컨테이너의 시작(startup) 단계 중에는 이 설정과 관계없이 CPU가 할당된다는 것입니다. 예를 들어, 시작 시 BigQuery에서 대량의 데이터를 로드하여 웹 서버의 시작 전에 동기적으로 완료시키는 서비스는 언뜻 보면 '상시 CPU가 필요해' 보입니다. 하지만 그 처리는 시작 단계에 국한되므로, 실제로는 변경해도 문제가 없습니다.
두 번째는 타임아웃 길이 자체가 제외 조건이 되지 않는다는 것입니다. 3,600초 동안 동기 처리하는 배치도, 그 시간 내내 요청을 처리하고 있는 상태이므로 CPU가 할당됩니다. '무거운 처리가니 상시 CPU'라는 논리는 성립하지 않습니다.
결국, 정말 제외해야 하는 것은 응답을 보낸 후에도 처리를 계속하는 서비스(fire-and-forget)뿐이었습니다. 판단 축을 이것 하나로 좁히기까지, 제외 목록은 여러 번 다시 작성했습니다.
이런 종류의 설정은 직관적으로 '필요할 것 같다'고 판단하면 반드시 넓게 잡는 경향이 있습니다. 제외는 '필요할 것 같으니까'가 아니라 '이 조건에 해당하니까'로 결정해야 합니다. 조건을 한 줄로 쓸 수 있을 때까지 좁혀나가는 것이 그대로 정확도가 됩니다.
결과와, 재발을 막는 장치
카나리아(Canary)로서 하나의 서비스로 시도했을 때, 해당 서비스 단독으로 44% 감소했습니다. 거기서 전체에 전개하여, instance-based 과금은 84% 감소했습니다. fire-and-forget 방식으로 처리를 계속하는 소수의 서비스를 제외하고 거의 전체에 적용했습니다.
그리고 마지막으로 CI 가드를 추가했습니다. cpuIdle이 미지정인 서비스 정의를 기계적으로 감지하여 차단합니다. 핵심은, cpuIdle: false로 명시한 경우에도 위반(violation) 처리했다는 점입니다. 상시 CPU가 정말 필요한 서비스는 allowlist에 이유를 적어 등록하게 했습니다. '왜 상시 CPU가 필요한지'를 한 줄 쓰게 만드는 것입니다.
그리고 이런 종류의 가드는, 작성한 후에 '위반을 섞어서 실제로 떨어지는지'까지 확인하는 방식으로 진행합니다. 가드는 단순히 작성만 했다고 작동이 보장되지 않으며, 게다가 작동하지 않는 것을 알아차리기 어렵습니다. '보호받고 있을 것이다'라는 전제만이 남아 있으면, 없는 것보다 오히려 위험합니다.
실례 2: 운영 실태를 아는 사람에게만 내릴 수 있는 판단
컨테이너 이미지의 스토리지도 큰 항목이었습니다. 여기는 절감 자체가 단순해서, 이미지 보존 세대를 10개에서 2개로 바꿨을 뿐입니다. 이걸로 스토리지는 65% 줄었습니다.
흥미로운 것은 판단 과정입니다. 데이터만으로는 '10세대가 필요할지도 모른다'고 말할 수 있을 뿐입니다. 몇 세대 전까지 돌아갈 가능성이 있는지는 데이터에 나타나지 않기 때문입니다.
실제 판단은 이랬습니다. 이미지가 없어도, git에서 코드 기록을 복원하여 재배포하면 된다는 것입니다. 즉 이미지 보존은 '과거 모든 시점으로 되돌릴 수 있게 하는 아카이브'가 아니라, 긴급 상황에 한 단계 전으로 즉시 돌아갈 수 있는 수단일 뿐입니다. 그렇게 다시 정의하니, 필요한 세대수는 2개로 충분했습니다.
여기서 효과를 본 것은 데이터가 아니라 그 조직에서 복구가 어떻게 이루어지는지에 대한 이해였습니다. git에서 복원할 수 있다는 전제가 성립하는지, 배포가 몇 분 만에 끝나는지, 긴급 상황에서 '한 단계 전으로 되돌리는' 이상의 조작이 실제로 필요한지가요. 이것은 로그에도 메트릭스에도 나타나지 않습니다.
AI에게 조사를 시키면 "안전을 위해 보존 세대(retention generation)를 넉넉하게"라는 결론에 도달하기 쉽습니다. 데이터만 본다면 그것이 합리적이기 때문입니다. 인간이 가지고 있는 것은, 그 보존이 무엇을 위한 것인가라는 목적의 측면입니다.
실례 3: 리뷰의 왕복 과정에서 CI가 통째로 돌아가고 있었다
가장 효과적이었던 CI 개선은 실행 **타이밍(timing)**을 바꾼 것이었습니다.
리뷰에서 지적이 나오고, 수정을 푸시하고, 다시 확인한다. 이 왕복 과정마다 모든 잡(job) (build / lint / test / knip)이 돌아가고 있었습니다.
구조적으로는 인간의 리뷰도 마찬가지입니다. 다만 AI가 리뷰하면 왕복 횟수와 속도가 올라가기 때문에, 그때까지 보이지 않았던 낭비가 명확하게 드러납니다. 실측해 보니 하나의 PR당 평균 3.4라운드(지적이 나와서 수정을 푸시하는 왕복을 1라운드로 계산), 1라운드당 무거운 잡 그룹에 6.8분이 소요되었습니다. 리뷰가 수렴할 때까지 같은 테스트를 3번 혹은 4번이나 돌리고 있었던 셈입니다. 아직 지적이 남아 있는 단계의 코드에 대해, 매번 풀 테스트(full test) 비용을 지불하고 있었던 것입니다.
그래서 무거운 잡을 리뷰가 통과된 후에만 실행하는 방식으로 변경했습니다. 푸시할 때마다 실행되는 것은 경량 체크 그룹(1.2분)뿐이며, 리뷰가 APPROVE를 낸 시점에 bot이 무거운 CI를 기동합니다.
왕복할 때마다 경량 체크 비용만 지불하고, 무거운 잡은 마지막에 한 번만. 3.4 × 6.8분이 3.4 × 1.2분 + 6.8분이 되어, 약 53% 감소했습니다.
skip과 fail-closed의 비대칭성
여기에는 구현상의 함정이 있었습니다. 처음에는 동일한 워크플로우(workflow) 내에서 조건 분기(if:)를 사용하여 잡을 스킵(skip)하는 방식을 시도했습니다만, 이는 위험한 설계였습니다.
| 상태 | 브랜치 보호(branch protection) 처리 |
|---|---|
잡을 if:로 스킵 | 합격으로 통과 |
| 워크플로우를 기동하지 않음 (체크 자체가 존재하지 않음) | 미보고 상태로 블록(block)됨 |
필수 체크를 스킵하게 되면 브랜치 보호가 실질적으로 무효화됩니다. 테스트가 돌아가지 않았는데도 "합격"의 초록불이 들어오고, 그대로 머지(merge)할 수 있게 됩니다. 이것은 fail-open입니다.
반면, 워크플로우 자체를 기동하지 않으면 체크가 생성되지 않으며, 미보고된 필수 체크가 머지를 블록합니다. 이것은 fail-closed입니다.
이 비대칭성이 설계의 핵심이었습니다. 최종적으로 무거운 잡은 별도의 워크플로우로 분리하여 workflow_dispatch로만 기동하는 형태로 만들었습니다. dispatch에 실패하더라도 체크가 미보고 상태로 남기 때문에 머지는 계속 블록된 상태로 유지됩니다.
비용 절감책이 동시에 세이프티 넷(safety net)의 설계가 됩니다. "싸게 처리하는 것"과 "틀렸을 때 멈추는 것"은 종종 동일한 설계 판단에서 나옵니다.
덤으로: 하지 않기로 하는 판단도 기대값으로 결정한다
또 하나, 문서만 변경한 PR에서 무거운 잡을 스킵하는 최적화도 도입했습니다. 여기서는 "스킵 판정을 전용 잡으로 분리해야 하는가"라는 설계 판단이 있었습니다.
판정 잡을 분리하면 구조는 깔끔해지지만, 모든 PR에 대해 러너(runner) 기동 비용(약 25초)이 추가됩니다. 반면 문서만 변경된 PR은 최근 실적상 전체의 약 4%이며, 여기서 아끼는 무거운 잡의 시간은 약 300초입니다.
기대값으로 보면, 96%의 PR이 지불하는 25초의 손실이 4%의 PR에서 얻는 300초의 이득을 상회합니다. 그래서 판정은 기존 잡 안에서 수행하기로 하고, 전용 잡으로는 분리하지 않았습니다.
"깔끔한 구조로 만든다"는 항상 옳아 보이지만, 비용의 맥락에서는 하지 않기로 하는 판단도 숫자로 결정할 수 있습니다. 감각적으로 "이쪽이 더 아름답다"라고 결정하면 이런 역전을 놓치게 됩니다.
실례 4: 처음에는 공짜처럼 보이지만, 데이터가 쌓이면 효과가 나타난다
마지막으로 하나 더. BigQuery의 MERGE 이야기입니다.
중복을 만들지 않고 데이터를 넣고 싶을 때, MERGE는 자연스러운 선택으로 보입니다. 키(key)가 일치하면 업데이트, 일치하지 않으면 삽입. SQL 한 문장으로 작성할 수 있고 멱등성(idempotency)도 보장할 수 있습니다.
다만 MERGE는, 넣으려는 행의 수와 관계없이 대조 대상을 매번 읽습니다. 단 1행만 넣는 경우라도 중복되지 않았음을 확인하기 위해 타겟(target) 측을 스캔해야 하기 때문입니다.
이것이 비용 관점에서는 까다로운 점입니다. 테이블이 작을 때는 거의 제로(zero)처럼 보입니다. 만들 당시에는 아무런 문제가 없습니다. 하지만 데이터가 쌓이면 1회 실행당 스캔량이 늘어납니다. 실행 빈도는 변하지 않았는데, 비용만 조용히 올라갑니다.
게다가 이는 코드를 변경하지 않았기 때문에 PR(Pull Request) 리뷰에서는 잡아낼 수 없습니다. 진입 게이트(gate)는 "이 변경이 비용을 얼마나 증가시키는가"를 보는 메커니즘이므로, 변경하지 않은 것이 (데이터로서) 성장해가는 부분은 대상에서 제외됩니다. 서두에 썼던 "만든 순간에는 보이지 않고, 나중에 조용히 쌓여간다"의 가장 순수한 형태라고 할 수 있습니다.
중복 판정을 어디서 할 것인가
그래서 중복 판정을 Firestore 쪽으로 옮기고, BigQuery에는 insert만 하는 방식으로 전환하고 있습니다.
키(key)의 존재 여부 체크는 Firestore에서 끝내고, 신규라고 판정된 것만 BigQuery에 insert합니다. BigQuery 측은 추가(append)만 하면 되므로, 대조를 위한 스캔(scan)이 사라집니다.
생각하는 방식은 실례 2와 같습니다. "MERGE를 어떻게 최적화할 것인가"가 아니라, 애초에 중복 판정을 BigQuery에게 시킬 필요가 있는가를 다시 질문한 결과입니다. BigQuery는 대량 데이터의 집계(aggregation)에는 강하지만, 키 1건의 존재 확인을 시키기에는 너무 무겁습니다. Firestore는 그 반대로 키 조회(lookup)는 잘하지만 집계는 서툽니다. 각각 잘하는 쪽으로 몰아주면, 양쪽 모두 자연스러운 사용법이 됩니다.
인지할 수 있는 메커니즘이 필요한 이유
이 건에 대해 말하고 싶은 것은 "MERGE를 쓰지 마라"가 아닙니다. 적합한 상황은 있으며, 저희도 전부를 교체하고 있는 것은 아닙니다.
하고 싶은 말은, 비용이 증가하는 방식에는 "만든 순간부터 높은 것"과 "성장한 후에 높아지는 것"이 있다는 점입니다. 전자는 리뷰에서 잡아낼 수 있습니다. 후자는 잡아낼 수 없습니다.
그렇기에 일간 가시화(visualization)가 필요합니다. 전주 동일 요일 대비로 보고 있다면, "아무것도 변경하지 않았는데 이 app의 비용이 계속 늘어나고 있다"는 사실이 반드시 눈에 들어옵니다. 게이트(gate)만으로는 부족한 이유가 바로 여기에 있습니다.
요약: 확보된 시간을 어디에 배분할 것인가
- 구현 비용이 낮아지면, 과거에 구현의 느림이 담당했던 "정말 필요한가"를 묻는 게이트가 해제된다
- 남는 것은 만든 것이 계속 돌아가는 러닝 코스트(running cost)이며, 이는 만든 순간에는 보이지 않는다
- 게다가 증가 방식에는 두 종류가 있다.
만든 순간부터 높은 것과, 데이터가 성장한 후에 높아지는 것. 후자는 리뷰에서 잡아낼 수 없다. 따라서 확보된 시간은 만들기 전에 다시 질문하는 것에 배분한다 - 단, 마음가짐만으로는 희석되므로,
진입 게이트 · 일간 가시화 · 종량제 쿼터(quota) · CI 가드(guard)로서 메커니즘화한다 - 한편으로, 기계화할 수 없는 판단은 확실히 남는다. 운영 실태, 롤백(rollback) 빈도, 애초에 이것이 필요한가에 대한 감각
- 메커니즘으로 잡을 수 있는 것은 메커니즘에 맡기고, 인간은 메커니즘으로 잡을 수 없는 판단에 집중한다. 시간의 재배분이란 그런 의미입니다.
구현이 빨라진 것 자체는 의심할 여지 없이 좋은 일입니다. 다만 빨라진 만큼을 그대로 "더 많이 만들기"에 전부 써버리면, 만든 것의 무게가 나중에 영향을 미칩니다. 저희는 일주일 분의 시간을 이 재고 조사(inventory)에 사용했지만, 다음에 해야 할 일은 똑같은 재고 조사를 반복하는 것이 아니라, 애초에 쌓이지 않는 메커니즘을 늘리는 것이라고 생각합니다.
제가 CTO를 맡고 있는 주식회사 에어클로젯(AirCloset)에서는, AI와 함께 새로운 개발 경험을 만들어가는 엔지니어를 모집하고 있습니다. 관심 있는 분은 꼭 엔지니어 채용 사이트 에어클로퀘스트(AirCloset Quest)를 확인해 주세요!
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기