리포트용 SQL 빌더 하나를 믿을 수 있게 만들기까지
안녕하세요. 초SSS급 개발자가 되고 싶은 이승재입니다. 이번 글은 광고 리포트를 만드는 SQL 빌더 하나를, 믿을 수 있게 만들기까지의 기록입니다.
제가 다루는 서비스에는 SQL을 이용해 리포트를 만드는 빌더가 있습니다. 사용자가 “지난달 캠페인별 전환수 보여줘”처럼 자연어로 요청하면, AI 에이전트가 그 문장을 해석해 차원·지표·기간·필터를 파라미터로 뽑아냅니다. 빌더는 그 파라미터를 받아 Athena에서 돌릴 SQL을 절 단위로 조립하고, 실행 결과를 리포트로 돌려줍니다. 자연어 한 문장이 SQL이 되어, 곧바로 광고주가 보는 숫자가 되는 셈입니다.
문제는, 이런 코드는 틀려도 티가 안 난다는 점입니다. SQL 문법이 깨지는 게 아니라면 쿼리는 멀쩡히 돌고, 표도 예쁘게 채워집니다. 매출이 중복 집계돼 두 배로 부풀어도 아무도 눈치채지 못하고 숫자만 조용히 틀립니다.
그래서 저는 이 빌더를 믿을 수 있게 만드는 테스트를 고민했습니다. 이 글은 그 고민의 기록입니다.
제가 생각하는 테스트는 크게 둘로 나뉩니다. 유닛 테스트와 통합 테스트입니다. 둘 중 뭐가 더 중요하냐고 묻는다면, 저는 둘 다 중요하다고 답하겠습니다. 목적이 서로 다르기 때문입니다.
유닛 테스트는 계약에 대한 검증입니다. ‘이 입력을 주면 이 출력을 준다’는 계약, 곧 스펙을 확인합니다. 실제 인프라를 끌어오지 않아 비용 부담이 적고 빠릅니다만 그 환경은 실제와 다릅니다. 프로덕션에서는 인프라를 호출하고, 모듈끼리 협응하고, 네트워크를 타고 밖으로 나갑니다. 하지만 유닛 테스트는 그렇지 않습니다. 게다가 유닛 테스트는 대부분 성공하기 마련입니다. 애초에 스펙을 확인하려고 성공 케이스와 실패 케이스를 나눠 그렇게 mock으로 만들어 두니까요. 그래서 유닛 테스트의 역할은 스펙 검증과, 코드 수정 시 변경의 전파를 잡는 데 있습니다.
반면 통합 테스트는 정도의 차이는 있지만 실제 프로덕션 환경과 최대한 유사하게 맞추려 합니다. 실제 동작을 테스트할 수 있지만 그만큼 무겁고, 경우에 따라 과금이 발생하기도 합니다.
실제 코드가 프로덕션으로 나가려면 통합 테스트가 반드시 필요합니다. 그리고 이 둘은 언제나 함께 개발되어야, 비로소 테스트 코드가 제값을 합니다.
막상 테스트를 하려니 문제가 있었습니다. S3·Athena 같은 AWS 인프라는 갖춰져 있었지만, 정작 그 위에서 테스트에 쓸 데이터가 없었습니다.
그래서 mock 데이터로 시작했습니다. 작은 가짜 데이터를 만들어 넣고 그 값을 pandas로 검산해 둔 다음, 같은 입력을 빌더에 넣어 만들어진 SQL의 결과와 맞춰 봤습니다. 이렇게 빌더가 뽑은 숫자를 빌더로 확인하지 않고 독립된 계산과 대조하는 방식을 테스트 오라클 기법이라고 합니다. 두 경로는 완전히 독립적이며 셀 단위로 한 칸이라도 어긋나면 실패입니다.
이 단계에서는 데이터의 정합성도 살폈지만, 주로 확인하려던 건 스펙이었습니다. SQL이 깨지지는 않는지, 빌더가 조건에 따라 분기를 제대로 타는지, 테이블을 옳게 고르는지. 빌더 로직이 뽑아내는 SQL의 모양을 검증하려 했습니다.
실데이터가 적재되는 순간에도 새로운 테스트 오라클을 만들면 됐습니다. 다행히 실데이터 기반으로도 잘 검증해 주었지만 이내 다른 문제를 맞닥뜨리게 됩니다. 요구사항은 언제나 변하고, 버그는 발견됩니다. 그때마다 코드를 리팩토링하고, 테스트를 다시 정비하는 과정은 너무 반복적이고 지루하다는 겁니다.
테스트를 설계하며 세 가지를 중요하게 뒀습니다. 첫째, 언제 돌려도 재현될 것. 둘째, 무엇을 왜 고쳤는지 추적될 것. 셋째, 이 둘을 사람 손이 아니라 자동으로 굴릴 것.
첫 번째 원칙, 재현. 검증에 쓴 데이터셋을 고정해 두고, 그 위에서 케이스 전부를 매번 다시 돌립니다. 빌더 로직을 건드릴 때마다 같은 데이터에 같은 케이스를 던져, 예전에 맞던 것이 지금도 맞는지 확인합니다. 일종의 회귀 테스트입니다. 검증 스크립트도 저장소 안에 영구화해, 파일 이름만으로 그때 그 라운드를 그대로 다시 돌릴 수 있게 했습니다.
두 번째 원칙, 추적. 테스트를 돌릴 때마다 검증 문서를 남겼습니다. 하나는 서사입니다. 목표, 데이터 출처, 산식, 라운드별 결과, 실패했을 때의 원인과 고친 과정. 다른 하나는 테스트셋입니다. 여태 돌린 SQL 전문과 각 결과, 재현 방법. 둘 다 지우지 않고 append만 합니다.
세 번째 원칙, 자동화. 여기까지를 매번 사람이 손으로 할 수는 없습니다. 그래서 SQL 빌더 전용 테스트 에이전트와 스킬을 만들었습니다. 에이전트가 케이스를 펼치고, 스킬이 검증 절차를 매번 같은 순서로 밟습니다. 재현과 추적이 사람 의지가 아니라 도구의 기본 동작이 됩니다.
테스트를 쓰는 일은 구현보다 언제나 더 어려운 것 같습니다. 잘 쓰인 테스트는 그것만 봐도 본래 코드를 유추할 수 있고, 그 자체로 실행 가능한 스펙이 됩니다.
굳이 개발자의 경지를 나눈다면, 유의미한 테스트 코드를 잘 쓰는 개발자야말로 앞서 말한 초SSS급 개발자가 아닐까 싶습니다.
그렇기 때문에 요즘은 어떻게 테스트할 것인가, 어떻게 검증할 것인가에 대해 많은 고민을 하고 있습니다.
읽어주셔서 감사합니다.