팬텀 스키마 문제: 테스트가 실패하기 전에 데이터베이스 계약이 깨지는 이유
요약
데이터베이스의 명시적 스키마와 애플리케이션의 암묵적 가정 사이의 간극인 '팬텀 스키마' 문제를 다룹니다. 표준적인 테스트로는 포착하기 어려운 운영 환경의 데이터 불일치 원인과 그 위험성을 분석합니다.
핵심 포인트
- 팬텀 스키마는 DB 제약 조건과 앱의 암묵적 가정 간의 차이에서 발생함
- 명시적 계약(Explicit contract)과 달리 암묵적 가정은 테스트하기 매우 어려움
- 테스트 데이터는 지나치게 모범적이어서 실제 운영 환경의 위반을 포착 못함
- 운영 환경의 예외적인 데이터 상태가 시스템 붕괴의 주요 원인이 됨
표준적인 테스트 관행으로는 거의 포착하기 불가능한 유형의 운영 장애가 있습니다. 왜냐하면 이들은 어떤 테스트도 위반하지 않기 때문입니다. 코드는 실행됩니다. 쿼리(Queries)도 실행됩니다. 애플리케이션은 지금까지 접해온 모든 데이터셋에 대해 올바르게 동작합니다. 그러다 새로운 환경, 새로운 통합, 또는 약간 다른 데이터 상태가 나타나면, 어디에도 명시적으로 기록되지 않았던 계약 가정(Contract assumption)이 드러나게 되고, 전체 시스템은 진단하는 데 수 시간이 걸리는 방식으로 무너져 버립니다.
이를 팬텀 스키마(Phantom schema) 문제라고 부릅시다. 이는 데이터베이스가 강제하는 스키마(Schema)와 애플리케이션이 실제로 의존하는 스키마 사이의 간극입니다.
강제된 제약 조건과 가정된 제약 조건의 차이
현대의 관계형 데이터베이스(Relational databases)는 실제 애플리케이션이 의존하는 제약 조건(Constraints) 중 놀라울 정도로 작은 부분집합만을 강제합니다. 외래 키(Foreign keys), NOT NULL 선언, 유니크 인덱스(Unique indexes), 데이터 타입(Data types) — 이것들은 데이터베이스가 쓰기 시점에 실제로 거부할 항목들입니다. 이것들이 명시적인 계약(Explicit contract)입니다.
하지만 애플리케이션은 시간이 지나면서 훨씬 더 큰 규모의 암묵적인 가정(Implicit assumptions)들을 쌓아 올립니다. 타입상으로는 더 많은 길이를 허용하더라도, 실제로는 특정 컬럼이 결코 일정 길이를 초과하지 않을 것이라는 가정. 외래 키가 이를 강제하지 않더라도 두 테이블이 항상 일치하는 행을 가질 것이라는 가정. CHECK 제약 조건이 없는 VARCHAR 타입임에도 불구하고 상태(Status) 필드가 알려진 네 가지 값 중 하나를 포함할 것이라는 가정. 데이터베이스 수준에서 일관성을 강제하는 것이 없음에도 불구하고 연결된 레코드 간의 날짜 범위가 항상 논리적으로 일관될 것이라는 가정 등이 있습니다.
이러한 가정들은 스키마 정의(Schema definition)가 아닌 애플리케이션 코드 내에 존재합니다. 이러한 가정들이 만들어졌을 당시에는 당시의 데이터가 이를 뒷받침했기 때문에 합리적이었습니다. 하지만 데이터가 코드가 전혀 예상하지 못한 방식으로 진화할 때, 이 가정들은 팬텀(Phantoms)이 됩니다.
이것이 생각보다 테스트하기 어려운 이유는 무엇일까요?
이러한 종류의 문제에 대한 표준적인 대응은 "테스트를 더 많이 작성하라"는 것입니다. 하지만 테스트는 여러분이 이미 명시적으로 만든 가정(assumptions)만을 검증할 수 있습니다. 정의상 팬텀 스키마 가정(phantom schema assumption)은 아무도 기록하지 않은 가정이며, 이는 곧 아무도 그에 대한 테스트를 작성하지 않았음을 의미합니다.
더 구체적으로 말하면, 테스트 환경에서 팬텀 스키마 위반(phantom schema violations)을 포착하기 어려운 이유는 테스트 데이터가 이러한 위반을 유발하기에는 거의 항상 너무 '모범적(well-behaved)'이기 때문입니다. 수동으로 작성된 피스처(fixtures)는 작성자가 생각한 시나리오만을 반영합니다. 통제된 분포(controlled distributions)가 없는 생성된 데이터는 평균적인 사례만을 반영합니다. 두 방법 모두 암시적인 제약 조건 위반(implicit constraint violation)을 드러내는 특정 값의 조합을 신뢰성 있게 생성해내지 못합니다. 왜냐하면 그 조합은 본질적으로, 무시해도 안전하다고 느껴졌던 가정의 결과물이기 때문입니다.
위반은 실제 운영 환경(production)의 사용자가 개발 과정에서 모델링되지 않은 데이터 상태를 생성할 때 표면화됩니다. UI가 설계되지 않은 순서로 프로필을 업데이트하는 사용자, 애플리케이션이 가정하는 것과 약간 다른 순서로 레코드를 생성하는 배치 작업(batch job), 혹은 코드가 열거형(enum)으로 선언하지 않은 필드에 유효하지만 예상치 못한 값을 보내는 제3자 통합(third-party integration) 등이 그 예입니다.
JOIN 로직에 살아있는 계약
가장 위험한 팬텀 스키마 가정은 JOIN 로직에 내장된 것들입니다.
JOIN을 작성할 때, 여러분은 두 테이블 간의 관계에 대해 암시적인 주장(implicit claim)을 하는 것입니다. 단순히 외래 키(foreign key)가 존재한다는 사실뿐만, 조인(join) 양측의 카디널리티(cardinality), Null 허용 여부(nullability), 그리고 데이터 분포(data distribution)가 쿼리 결과가 의미를 갖도록 행동할 것이라는 주장 말입니다.
오른쪽 테이블에 매칭되는 행이 "거의 항상" 존재할 것이라고 가정하고 작성된 LEFT JOIN은, 운영 환경(production) 레코드의 30%가 매칭되는 행이 없을 때 매우 다르게 동작합니다. 개발 단계에서 완벽하게 작동했던 INNER JOIN은 엣지 케이스(edge case) 사용자에 대해 조인 조건(join condition)이 충족되지 않을 때 운영 환경에서 조용히 레코드를 누락시킵니다. 이러한 조인들을 기반으로 구축된 집계(Aggregations)는 미묘하게 틀린 수치를 생성하며, 엣지 케이스 인구 집단에 대해 무엇이 "정확한" 것인지 정의된 바가 없기 때문에 모든 검증(validation) 체크를 통과해 버립니다.
이것들은 전통적인 의미의 버그(bugs)가 아닙니다. 쿼리는 구문론적으로 유효(syntactically valid)합니다. 결과는 주어진 데이터 측면에서 기술적으로 정확합니다. 문제는 쿼리가 설계된 데이터 상태(data state)와 운영 환경이 만들어내는 데이터 상태가 서로 다르다는 것이며, 그 사이의 간극이 테스트 과정에서 모델링(modelled)되지 않았다는 점입니다.
팬텀 스키마(Phantom Schema)와 합성 데이터(Synthetic Data)
이 지점이 바로 제어된 분포(controlled distributions)를 가진 합성 데이터 생성(synthetic data generation)이 문제를 의미 있게 변화시키는 부분입니다.
예시(examples)가 아닌 인구 집단(populations)을 지정하여 테스트 데이터를 생성할 때, 팬텀 스키마 가정(phantom schema assumptions)을 드러내는 데이터 상태를 의도적으로 모델링할 수 있습니다. JOIN이 항상 존재할 것이라고 가정하는 테이블에 매칭되는 행이 없는 사용자가 25%인 데이터셋을 생성할 수 있습니다. 코드가 의존하는 암시적 열거형(implicit enum) 값에 예상치 못했지만 기술적으로는 유효한 변형(variant)이 포함된 레코드를 생성할 수 있습니다. 또한, 개발 중에 가정했던 부모 레코드와 자식 레코드의 비율 범위를 벗어날 때만 깨지는 집계 로직(aggregation logic)을 압박하는 카디널리티 분포(cardinality distributions)를 만들 수도 있습니다.
팬텀 가정(phantom assumption)은 그것을 위반하는 데이터가 존재하기 전까지는 가시화되지 않습니다. 제어된 엣지 케이스 분포를 활용한 합성 생성(Synthetic generation)은 실제 운영 환경의 사용자들이 발생시키기 전에 그러한 데이터를 만들어낼 수 있는 가장 빠른 방법입니다.
여기서 중요한 특정 능력은 대규모에서의 관계적 일관성 (relational consistency)입니다. 즉, 단순히 편리한 분포가 아니라 사용자가 지정한 분포를 반영하는 레코드 간의 관계를 가진 연결된 테이블을 생성하는 것입니다. 평면적인 테이블 데이터 (flat tabular data)를 생성하는 생성기는 JOIN 계층의 팬텀 가정 (phantom assumptions)을 드러내지 못할 것입니다. 반면, 정의된 카디널리티 (cardinality) 파라미터를 준수하면서 전체 관계형 스키마 (relational schema)에 걸쳐 참조 무결성 (referential integrity)을 유지하는 생성기는 이를 드러낼 수 있습니다.
그것이 바로 SyntheholDB가 메우기 위해 구축된 격차입니다. 여러분의 스키마와 스트레스 테스트를 원하는 분포를 설명하십시오. 여기에는 암묵적인 계약 가정 (implicit contract assumptions)을 드러내는 엣지 케이스 (edge case) 인구 집단이 포함됩니다. 그리고 여러분의 애플리케이션을 단순히 확인해 주는 것이 아니라, 도전 과제를 던지는 관계적으로 일관된 데이터셋을 생성하십시오. 카드 등록 없이 db.synthehol.ai에서 무료 티어를 사용할 수 있습니다.
채택할 가치가 있는 규율
가장 강력한 엔지니어링 팀은 팬텀 스키마 가정 (phantom schema assumptions)을 사후 고려 사항이 아닌 일급 사항 (first-class concern)으로 취급합니다. 그들은 명시적인 제약 조건과 함께 암묵적인 제약 조건을 문서화합니다. 그들은 해당 제약 조건을 위반할 가능성이 가장 높은 인구 집단을 포함하는 테스트 데이터를 생성합니다. 그리고 커버리지 (coverage) 지표가 무엇이라고 말하든 상관없이, 오직 잘 정돈된 데이터에 대해서만 실행되는 테스트 스위트는 불완전한 것으로 간주합니다.
데이터베이스가 강제하는 스키마는 바닥 (floor)입니다. 여러분의 애플리케이션이 실제로 의존하는 스키마는 천장 (ceiling)입니다. 그 둘 사이의 거리가 바로 가장 흥미로운 운영 환경의 버그 (production bugs)가 존재하는 곳입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기