유효성 검증과 데이터베이스 제약 혹은 둘 다?

Validation, Database Constraint, or Both?

Posted by npmachine on August 1, 2017

Derek Prior의 Validation, Database Constraint, or Both?를 번역한 글입니다.


레일즈가 제공하는 유효성 검사는 광범위하다. 값 존재 검증(presence), 고유성 검증(uniqueness), 형식 검증(format), 숫자 검증(numericality) 등등. 데이터에 대한 모든 제약 조건에 대응하여, “쓰레기” 데이터로부터 당신의 애플리케이션을 보호할 수 있는, 한 줄로 된 유효성 검증 구문을 구성하는 것이 가능하다.

유효성 검증이 충분하지 않은 경우

앞서 살펴본 것처럼(역자주: 고유성 검증의 위험) 애플리케이션 수준의 유효성 검증 일부는 데이터베이스 무결성을 보장하기에 부족하며 이를 위해서는 데이터베이스 제약 조건으로 보완해야 한다. 예를 들어 각 고유성 제약 조건은 경쟁 상태(race condition)를 방지하기 위해 고유한 데이터베이스 인덱스로 뒷받침 되어야 한다. 또한 개발자는 값 존재 검증(presence validations)이 스키마에서 null: false 제약 조건으로 뒷받침 되는지 혹은 포함 검증(inclusion validations)이 체크 제약 조건(check constraint)으로 뒷받침 되는지를 고려해야 한다.

모든 사항을 검토하고 완료한다면, 스키마와 모델에서 유효성 검증의 중복은 흔하다. 예를 들어,

create_table "users", force: :cascade do |t|
  t.name :string, null: false
  t.username :string, null: false, index: true
  t.encrypted_password :string, null: false
  t.belongs_to :organization,
    null: false,
    index: :true,
    foreign_key: { on_delete: :cascade }
  t.timestamps

  t.index :username, unique: true
end
class User < ApplicationRecord
  attr_accessor :password

  belongs_to :organization

  validates :name, presence: true
  validates :username, presence: true, uniqueness: true
  validates :password, presence: true
  validates :encrypted_password, presence: true
end

이 User 모델을 나타낸 스키마를 기반으로 구축한 애플리케이션으로 사용자 등록, 암호 재설정, 이름 변경을 수행할 수 있다. 사용자는 가입시에 라우트 매개 변수를 통해 조직과 관계를 맺는다.

DRY 원칙은?

(역자주: DRY는 Don’t Repeat Yourself의 약자이며, 중복 코드를 가급적 최소한으로 유지하려는 소프트웨어 개발 원칙이다. 레일즈에서 특히 강조한다.)

언뜻보면 여기에 상당한 중복이 있다. 그러나 스키마 제약 조건이 모델 유효성 검증과 다른 용도로 사용된다는 점을 인지해야 한다.

스키마 제약 조건은 원본과 관계없이 데이터베이스에 보관된 데이터가 일관되고 도메인의 컨텍스트에 의미가 있음을 보장한다. 이 애플리케이션의 스키마는 사용자가 도메인에서 유효하려면 이름, 사용자 이름, 암호화된 비밀번호 및 관련 조직이 있어야 함을 알려준다. 스키마는 사용자 이름이 고유하고 관련 조직이 존재하는지 보장하여 일관성을 강화한다. 종속된 외래 키는 조직 행을 삭제하면 관련 사용자 행도 같이 삭제하여 잘못된 외부 키 참조를 방지한다.

그에 반해 모델 검증은 오류에 대한 사용자 인터페이스를 제공한다. 애플리케이션 사용자가 name 필드를 비워두면, 값 존재 검증 구문은 양식을 다시 렌더링 할 때 사용자가 이해할 수 있는 유용한 오류 메시지를 추가한다.

유효성 검증과 제약 조건 사이의 결정

각 유효성 검증을 스키마 제약 조건으로 되돌릴 필요도, 스키마 제약 조건을 모델 유효성 검증으로 반영할 필요도 없다. 각자의 상황에 적합한 것을 결정할 때 생각해 볼만한 가치가 있는 두 가지 질문이 있다.

잘못된 데이터가 데이터베이스에 기록되는 것을 방지하려고 하나? 그렇다면 스키마 제약 조건이 있어야 한다. 불행히도, Omakase Rails는 Postgres가 지원하는 모든 공통 제약 조건의 생성 및 스키마 덤핑을 기본적으로 지원하지 않으므로 의사 결정시에 이를 고려해야 한다. 애플리케이션 사용자가 직접 고칠 수 있는 오류를 방지하려고 하나? 그렇다면 모델 검증을 사용해야 한다.

users 예제 재구성하기

위의 예에서 users 테이블과 User 모델을 돌아보자.

우리는 스키마를 잘 정의했다. 존재하는 각 제약 조건은 불량 데이터가 기록되는 것을 방지하기 위해 존재하며, 필요한 제약 조건도 누락하지 않았다. 사용자 이름 열에 고유한 색인을 추가하기도 했다.

그러나 User의 유효성을 검토해보면 개선할 부분이 보인다. 우리의 애플리케이션은 사용자가 그들의 name , username 및 password 를 입력할 수 있다. User의 다른 필드는 사용자 입력란으로 나타나 있지 않지만 그럼에도 각 입력란에 대한 유효성 검사가 있다.

encrypted_password에 대한 존재 검증을 생각해 보자. 어떤 방법으로든 encrypted_password가 누락된 채로 등록을 시도하면, 애플리케이션에 오류가 발생한다. 이 필드에 대한 유효성 검증으로 인해 컨트롤러는 등록 양식을 다시 렌더링 한다. 최선의 경우 우리의 양식은 모든 오류 메시지를 렌더링하고 “암호화 된 비밀번호가 필요합니다”라는 오류 메시지를 표시한다. 최악의 경우 encypted_password 필드가 양식에 없기 때문에 양식에 오류가 전혀 표시되지 않는다. 두 경우 모두 사용자가 문제를 해결하기 위해 할 수 있는 일은 없지만 후자의 경우 사용자는 당황하게 된다.

모델에서 유효성 검증을 제거하면 동일한 시나리오에서 데이터베이스 스키마의 해당 열에 대해 null: false 제약 조건으로 인해 애플리케이션 오류가 발생한다. 사용자는 여전히 등록 할 수 없지만 애플리케이션 오류로 인해 오류 모니터링 서비스에 대한 보고서가 기록되어 버그를 알려준다.

아마도 놀랍게도 우리는 organization에도 이와 동일한 문제가 있을 수 있다. Rails 5부터는 belongs_to 관계가 새로 생성된 애플리케이션에서 기본적으로 필수로 표시된다. organization은 라우트 데이터를 사용하여 콘트롤러에서 자동으로 설정되며 사용자가 선택하지 않는다. 이 프로세스에 오류가 있는 경우 belongs_to 관계는 “조직이 있어야 함”이라는 유효성 검증 오류를 추가하여 이를 표시한다.

이러한 문제를 염두에두고 User 모델을 다음과 같이 다시 작성할 수 있다.

class User < ApplicationRecord
  attr_accessor :password

  belongs_to :organization, optional: true

  validates :name, presence: true
  validates :username, presence: true, uniqueness: true
  validates :password, presence: true
end

사용자가 수정할 수 있는 오류가 아니므로 encrypted_password 대한 유효성 검증을 제거하였다. 마찬가지로 organization을 필수가 아닌 관계로 선언했다. 후자의 변경 사항이 번거롭다면 생성된 active_record_belongs_to_required_by_default.rb를 편집하여 이 설정을 애플리케이션 전체에 적용할 수 있다.

Rails.application.config.active_record.belongs_to_required_by_default = false

애플리케이션 설계에 미치는 영향

데이터 무결성 제약 조건을 데이터베이스로 옮기면 특화된 양식 객체를 사용하여 액티브 레코드 모델의 부담을 줄이는 것이 더 쉬워진다. 데이터베이스 제약 조건에 따라 안전망을 제공하기 때문이다.

양식 객체는 여러개의 모델을 만드는 데 필요한 사용자 인터페이스를 제공한다. 검증을 양식 오류에 대한 사용자 인터페이스를 제공하는 것으로 바라본다면 중복될 가능성이 있더라도 필요한 검증들을 포함하는게 합리적이다.

양식 객체는 표시하고 있는 상황이 존재하며, 상황에 적합한 유효성 검증 및 메시지를 추가 할 수 있다. 이러한 상황은 액티브 레코드 모델에서 종종 볼 수 있는 조건부 유효성 검증을 제거할 수 있게 해준다. 예를 들어 예제 애플리케이션이 더 커지면 등록 양식, 프로필 양식, 패스워드 재설정 양식에 각각의 상황에 맞는 검증을 작성하고 User 클래스에는 검증을 완전히 제거할 수 있다.


npmachine

a mediocre engineer