완벽함은 너무 비싸다: 데이터베이스의 유효하지 않은 데이터와 Recheck 젬

XO Ruby Chicago 2025 - Perfect is too expensive by Peter Bhat Harkins

작성자
jeff
발행일
2025년 12월 21일

핵심 요약

  • 1 데이터베이스에 유효하지 않은 데이터가 저장되는 9가지 주요 원인과 이들이 발생할 수밖에 없는 트레이드오프를 설명합니다.
  • 2 SQL 제약 조건이나 트리거만으로는 복잡한 데이터 무결성 문제를 완벽하게 해결할 수 없으며, 이는 비용과 효율성 측면에서 비현실적임을 강조합니다.
  • 3 유효하지 않은 데이터의 발생을 완벽히 막을 수 없으므로, 이를 지속적으로 확인하고 재확인하는 `Recheck` 젬을 소개하며, 이는 문제 조기 발견 및 시스템 개선에 기여합니다.

도입

발표자는 Rails 애플리케이션에서 발생한 유효하지 않은 쿠폰 데이터(사용됨과 만료됨 상태가 동시에 설정된) 버그 사례를 제시하며 강연을 시작합니다. 이는 단순한 유효성 검사를 통과했음에도 불구하고 데이터베이스에 잘못된 데이터가 저장된 '밀실 살인 미스터리'와 같다고 비유합니다. 이 문제는 개발자들이 흔히 겪는 현상이며, 지난 9년간 해결되지 않은 광범위한 문제임을 지적하며, 데이터 무결성 문제를 해결하기 위한 새로운 접근 방식의 필요성을 제기합니다. 발표는 '완벽함은 너무 비싸다'는 전제하에, 데이터베이스에 유효하지 않은 데이터가 저장되는 다양한 원인과 그에 대한 실용적인 해결책을 모색합니다.

발표자는 데이터베이스에 유효하지 않은 데이터가 저장되는 9가지 주요 원인을 세 가지 광범위한 범주로 나누어 설명합니다.

1. 유효성 검사 생략

  • Active Record의 validate: false 옵션: save(validate: false) 또는 update(validate: false)와 같이 명시적으로 유효성 검사를 건너뛰는 경우입니다. 이는 주로 마이그레이션 과정에서 복잡하거나 오래된 유효성 검사를 우회하여 신속하게 작업을 완료해야 할 때 발생합니다.

  • 유효성 검사를 건너뛰는 19가지 Active Record 메서드: decrement!, toggle!, update_columns 등 이름만으로는 유효성 검사 생략 여부를 알기 어려운 메서드들이 존재합니다. 이들은 주로 성능상의 이유로 저수준의 속성 변경에 사용되며, 명확한 명명 규칙이 없어 개발자가 일일이 기억하기 어렵습니다.

2. 제약 조건 미적용 및 개발자 실수

  • 미적용된 외래 키(Foreign Key) 및 고아 레코드: Rails는 기본적으로 데이터베이스 수준의 외래 키 제약 조건을 자동 적용하지 않습니다. DHH의 “단일 계층의 영리함(single layer of cleverness)” 철학에 따라 모델에서만 유효성 검사를 수행하려 했으나, 이는 데이터베이스 수준의 무결성을 보장하지 못하여 고아 레코드와 같은 문제를 야기합니다.

  • 새로운 유효성 검사 도입 시 기존 데이터 처리 부재: 새로운 유효성 검사를 추가할 때 기존 데이터에 대한 유효성 검사나 마이그레이션이 이루어지지 않으면, 나중에 해당 데이터를 수정할 때 validate: false를 사용하게 되는 악순환이 발생합니다. 시스템은 인간의 한계를 고려해야 하며, 개발자가 모든 것을 기억하도록 강요해서는 안 됩니다.

3. 동시성 및 외부 요인

  • 경쟁 조건(Race Conditions):
    • 부분 쓰기(Partial Writes): 두 개의 프로세스(예: 백그라운드 작업, 사용자 요청)가 동시에 동일한 레코드의 다른 필드를 업데이트할 때, Active Record는 변경된 필드만 업데이트하는 부분 쓰기를 수행합니다. 이로 인해 두 필드가 동시에 채워져야 할 때 하나만 채워지거나, 불가능한 상태(예: 두 타임스탬프가 동시에 기록되는)의 레코드가 생성될 수 있습니다.
    • 집계 객체(Aggregate Objects) 읽기: 주문 총액과 같이 여러 관련 레코드(품목, 쿠폰)에 의존하는 집계 객체를 읽을 때, 다른 프로세스가 동시에 관련 레코드를 수정하면 일관되지 않거나 불가능한 상태(예: 삭제된 쿠폰이 적용된 총액)를 읽을 수 있습니다. Rails는 성능상의 이유로 읽기 트랜잭션을 자동으로 생성하지 않습니다.
  • 유효성 검사가 완화된 인터페이스: Rails 콘솔, 데이터베이스 콘솔, 관리자 페이지 등에서 사람이 직접 데이터를 수정할 때 오타나 잘못된 복사/붙여넣기 등으로 인해 코드 경로로는 불가능한 상태의 데이터가 생성될 수 있습니다.

  • 다중 코드베이스: 동일한 데이터베이스를 공유하는 여러 애플리케이션(예: Rails 앱과 Java ETL 프로그램)이 서로 다른 유효성 검사 규칙을 가질 때 데이터 불일치가 발생할 수 있습니다. 프로그램 간의 데이터 공유 지점(DB, 잡 큐, 파일 시스템, API)은 항상 동기화 오류의 기회가 됩니다.

  • 서드파티 API 통합 및 비동기 처리: 서드파티 API 호출을 위해 특정 필드를 null로 남겨두고 백그라운드 작업을 예약하는 패턴은 사용자에게 즉각적인 응답을 제공하지만, 백그라운드 작업이 실패하거나 사라지면 영구적으로 null 값이 남는 유효하지 않은 데이터가 생성될 수 있습니다.

발표자는 SQL 제약 조건(Unique, Not Null, Foreign Key)이 기본적인 무결성에는 유용하지만, 복잡한 비즈니스 로직(예: 쿠폰 만료일이 사용 가능일 이후여야 함, 특정 기간 내 유니크 제약, 다른 테이블과의 조인 조건)을 표현하기에는 한계가 있다고 지적합니다. 또한, SQL 트리거는 복잡한 로직을 표현할 수 있지만, SQL 언어의 불편함, 통합(소스 제어, 배포, 테스트)의 어려움, 유지보수 비용 때문에 모든 비즈니스 로직을 여기에 구현하는 것은 비효율적이라고 강조합니다. 결국 ‘완벽한’ 해결책은 없으며, 모든 것은 비용과 효율성의 트레이드오프라는 결론에 도달합니다.

결론

결론적으로, 발표자는 데이터베이스에 유효하지 않은 데이터가 발생하는 것을 완벽하게 예방하는 것은 불가능하며, 이는 비용과 실용성 측면에서 비현실적인 목표라고 역설합니다. 컴파일러나 테스트 스위트처럼 완벽함을 요구하는 프로그래밍 경험과 달리, 엔지니어링은 항상 트레이드오프의 영역임을 강조합니다. 이러한 문제에 대한 해결책으로, 발표자는 `Recheck`라는 새로운 오픈 소스 젬을 소개합니다. `Recheck`는 데이터베이스의 유효하지 않은 데이터를 지속적으로 '확인하고 재확인(check and recheck)'하여 문제를 조기에 발견하고 해결하는 것을 목표로 합니다. 이는 이미 작성해야 하는 문제 해결 쿼리를 '체크(check)'로 캡슐화하여, 버그가 재발했을 때 사용자보다 먼저 인지하고 신속하게 대응할 수 있도록 돕습니다. `Recheck`는 완벽함을 추구하기보다 효과적이고 경제적인 접근 방식을 통해 시스템의 데이터 무결성을 지속적으로 개선하는 데 기여할 것입니다.

댓글 0

로그인이 필요합니다

댓글을 작성하거나 대화에 참여하려면 로그인이 필요합니다.

로그인 하러 가기

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!