발표자는 데이터베이스에 유효하지 않은 데이터가 저장되는 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 언어의 불편함, 통합(소스 제어, 배포, 테스트)의 어려움, 유지보수 비용 때문에 모든 비즈니스 로직을 여기에 구현하는 것은 비효율적이라고 강조합니다. 결국 ‘완벽한’ 해결책은 없으며, 모든 것은 비용과 효율성의 트레이드오프라는 결론에 도달합니다.