작은 데이터에서 통과한 테스트가 실제 규모에서 같은 결과를 보장하지는 않아. 데이터의 크기와 모양을 함께 시험해야 성능 병목이 드러나.
한 줄로 말하면
현실 크기 테스트 데이터는 기능이 맞는지만 보는 작은 샘플이 아니라, 실제 운영에서 터질 성능 병목까지 드러낼 만큼 크기와 모양을 닮은 데이터야.
데이터베이스를 바꾸거나 큰 스키마를 옮길 때 이 차이가 중요해. 작은 테스트 데이터에서는 모든 쿼리가 빠르게 지나가도, 운영에 가까운 크기에서는 전체 테이블 스캔이나 N+1 쿼리가 갑자기 시스템 전체를 잡아먹을 수 있다.
비유로 이해하기
이건 이사 전에 빈 방에서 가구 배치를 맞춰보는 것과 비슷해. 빈 방에서는 소파도 들어가고 책상도 들어간다. 하지만 실제 살림살이가 들어오면 문 폭, 엘리베이터, 콘센트 위치, 동선이 한꺼번에 문제가 된다.
테스트 데이터도 그래. 행이 몇 개 없는 데이터는 코드의 문법과 기본 동작을 확인하는 데 좋다. 하지만 인덱스가 빗나갔는지, 검색이 큰 테이블을 끝까지 훑는지, 같은 조회를 수천 번 반복하는지는 데이터가 충분히 커져야 보인다. 여기까지가 쉽게 이해를 돕기 위한 비유야. 실제로는 크기뿐 아니라 값의 분포, 관계의 밀도, 자주 쓰는 접근 패턴까지 닮아야 한다.
정확한 정의
현실 크기 테스트 데이터는 운영 데이터의 원본을 그대로 복사하라는 뜻이 아니야. 핵심은 운영 환경에서 문제가 되는 조건을 테스트 환경에 옮겨 오는 것이다. 데이터 양, 테이블 사이의 관계, 자주 조회되는 값의 치우침, 오래된 레코드와 최근 레코드의 비율 같은 것들이 여기에 들어간다.
그래서 이 개념은 테스트 스위트와 같이 움직인다. 테스트 데이터만 커도 실패 조건이 없으면 문제를 놓친다. 반대로 테스트가 좋아도 데이터가 너무 작으면 느린 쿼리와 마이그레이션 병목을 재현하지 못한다.
왜 중요한가
운영 데이터베이스에 접근하지 못하는 상황에서는 더 중요해진다. lobste.rs의 MariaDB에서 SQLite로의 전환 사례에서, 첫 배포는 체크리스트를 준비했는데도 읽기 전용 트래픽만으로 모든 CPU가 100%에 도달해 롤백됐다.1 프로덕션 DB에 접근할 수 없어 실제 성능 문제를 사전에 재현하기 어려웠다는 점도 함께 드러났다.1
이후에는 실제 lobsters 데이터의 절반 크기를 로컬에 만드는 대량 데이터 생성 스크립트가 추가됐다. 그 데이터를 만드는 데만 일주일이 걸렸고, 첫 배포의 병목은 대형 테이블에서 두 쿼리가 일으킨 전체 테이블 스캔과 별도의 N+1 문제로 확인됐다.1
중요한 대목은 “테스트가 있었느냐”가 아니라 “어떤 실패를 빨리 드러냈느냐”야. lobste.rs 사례는 테스트 스위트가 정확성 검증에는 도움이 됐지만, 현실적인 데이터 규모가 부족하면 성능 병목은 배포 뒤에야 나타날 수 있다는 점을 보여준다.1
실제 예시
데이터베이스 마이그레이션에서는 새 데이터베이스가 기존 쿼리를 받아줄 수 있는지만 보면 부족하다. 타입, 정렬 규칙, 함수, 검색 방식, 인덱스 사용 방식이 바뀌면 작은 데이터에서는 안 보이던 문제가 커진다.
lobste.rs도 SQLite로 옮기면서 regexp, if, stddev 같은 사용자 정의 함수를 구현했고, unsigned bigint 미지원과 ASCII만 처리하는 NOCASE 같은 호환성 제약을 다뤄야 했다.1 이런 차이는 SQLite STRICT 테이블처럼 “데이터베이스의 선언과 실제 값이 어디서 어긋나는가”를 빨리 드러내는 장치와도 이어진다.
현실 크기 테스트 데이터는 이때 세 가지 질문을 던지게 해준다. 같은 기능이 맞게 동작하나. 충분히 큰 데이터에서도 쿼리가 같은 모양으로 실행되나. 문제가 생겼을 때 배포·롤백 체크리스트가 실제로 시간을 벌어주나.
헷갈리지 말아야 할 점
- 운영 데이터 복사본과 같은 말이 아니야. 개인정보와 접근 권한 때문에 운영 데이터를 그대로 쓰면 안 되는 경우가 많다. 필요한 것은 위험한 조건을 재현하는 데이터다.
- 행 개수만 맞추면 끝나는 것도 아니야. 값의 분포와 관계가 다르면 인덱스 선택, 캐시, 조인 비용이 달라진다.
- 단위 테스트를 대체하지 않아. 작은 테스트는 빠른 피드백을 주고, 현실 크기 데이터는 늦게 드러나는 병목을 잡는다. 둘은 역할이 다르다.
- 성공 기준은 “느리지 않다”가 아니라 “나쁘면 실패한다”에 가깝다. lobste.rs 회고에서도 테스트 중 전체 테이블 스캔이 발생하면 실패하도록 설정했다면 첫 배포의 성능 문제를 미리 잡을 수 있었을 것이라고 봤다.1
관련 문서
- SQLite의 타입 경계를 더 빨리 드러내는 장치: SQLite STRICT 테이블
- 개발과 운영의 확인 고리를 짧게 만드는 구조: feedback loop
남은 질문들
- 프로덕션 데이터에 접근하지 않고도 관계 밀도와 값 분포를 닮은 테스트 데이터를 만드는 현실적인 방법은 무엇일까?
- 데이터베이스별로 전체 테이블 스캔과 N+1 쿼리를 테스트 실패로 바꾸는 가장 낮은 비용의 장치는 무엇일까?
- 개인정보를 지키는 익명화 데이터와 처음부터 만든 합성 데이터는 성능 병목 재현에서 어떤 차이를 보일까?
댓글