rubyonrails.org에서 더 보기:

이 파일을 GITHUB에서 읽지 마세요. 가이드는 https://guides.rubyonrails.org 에 게시되어 있습니다.

Active Record Validations

이 가이드는 Active Record의 validation 기능을 사용하여 데이터베이스에 저장하기 전에 Active Record 객체를 검증하는 방법을 설명합니다.

이 가이드를 읽고 나면 다음과 같은 내용을 알 수 있습니다:

  • Active Record의 내장 validation과 옵션을 사용하는 방법
  • 객체의 유효성을 확인하는 방법
  • 조건부 validation과 엄격한 validation을 생성하는 방법
  • 사용자 정의 validation 메서드를 만드는 방법
  • validation 오류 메시지를 처리하고 뷰에 표시하는 방법

1 Validations 개요

다음은 매우 간단한 validation의 예시입니다:

class Person < ApplicationRecord
  validates :name, presence: true
end
irb> Person.new(name: "John Doe").valid? # name에 값 있음
=> true
irb> Person.new(name: nil).valid? # name이 nil
=> false

보시다시피, Personname attribute 없이는 유효하지 않습니다.

더 자세한 내용을 살펴보기 전에, validation이 애플리케이션의 전체적인 그림에서 어떻게 들어맞는지 이야기해보겠습니다.

1.1 왜 Validation을 사용하나요?

Validation은 유효한 데이터만 데이터베이스에 저장되도록 보장하기 위해 사용됩니다. 예를 들어, 애플리케이션에서 모든 사용자가 유효한 이메일 주소와 우편 주소를 제공하도록 보장하는 것이 중요할 수 있습니다. Model-level validation은 유효한 데이터만 데이터베이스에 저장되도록 보장하는 가장 좋은 방법입니다. 이는 모든 데이터베이스에서 사용할 수 있고, 최종 사용자가 우회할 수 없으며, 테스트와 유지보수가 용이합니다. Rails는 일반적인 요구사항에 대한 내장 헬퍼를 제공하고, 사용자가 직접 validation 메서드를 만들 수도 있게 해줍니다.

1.2 데이터 검증의 다른 방법들

데이터베이스에 데이터를 저장하기 전에 검증하는 여러 가지 방법이 있습니다. 여기에는 데이터베이스 자체의 제약 조건, client-side 검증, controller-level 검증이 포함됩니다. 각각의 장단점은 다음과 같습니다:

  • 데이터베이스 제약 조건과 stored procedure는 검증 메커니즘이 데이터베이스에 종속되며 테스트와 유지보수를 더 어렵게 만들 수 있습니다. 하지만 데이터베이스가 다른 애플리케이션에서도 사용된다면 데이터베이스 수준에서 일부 제약 조건을 사용하는 것이 좋을 수 있습니다. 또한 데이터베이스 수준의 검증은 다른 방식으로는 구현하기 어려운 일부 작업(예: 사용량이 많은 테이블에서의 고유성)을 안전하게 처리할 수 있습니다.
  • Client-side 검증은 유용할 수 있지만, 단독으로 사용하면 일반적으로 신뢰성이 떨어집니다. JavaScript로 구현된 경우 사용자의 브라우저에서 JavaScript가 꺼져 있으면 우회될 수 있습니다. 하지만 다른 기술과 결합하면 사이트 사용 중에 사용자에게 즉각적인 피드백을 제공하는 편리한 방법이 될 수 있습니다.
  • Controller-level 검증은 사용하고 싶은 유혹이 있을 수 있지만, 종종 다루기 어렵고 테스트와 유지보수가 어려워집니다. 가능한 한 controller를 단순하게 유지하는 것이 좋습니다. 이는 장기적으로 애플리케이션 작업을 더 쉽게 만들 것입니다.

Rails는 대부분의 경우 model-level 검증을 사용하는 것을 권장하지만, 이를 다른 검증 방법으로 보완하고 싶은 특정 사례가 있을 수 있습니다.

1.3 Validation Triggers

Active Record 객체에는 두 가지 종류가 있습니다 - 데이터베이스 내의 행에 해당하는 것과 그렇지 않은 것입니다. new 메서드를 사용하여 새로운 객체를 인스턴스화할 때, 해당 객체는 아직 데이터베이스에 저장되지 않습니다. 해당 객체에 대해 save를 호출하면 그때 적절한 데이터베이스 테이블에 저장됩니다. Active Record는 객체가 이미 데이터베이스에 있는지 여부를 판단하기 위해 persisted? (그리고 그 반대인 new_record?)라는 인스턴스 메서드를 사용합니다. 다음의 Active Record 클래스를 살펴보세요:

class Person < ApplicationRecord
end

bin/rails console 출력을 살펴보면 어떻게 동작하는지 알 수 있습니다:

irb> p = Person.new(name: "Jane Doe") 
=> #<Person id: nil, name: "Jane Doe", created_at: nil, updated_at: nil>

irb> p.new_record? # 새 레코드인지 확인
=> true

irb> p.persisted? # 저장되어 있는지 확인
=> false

irb> p.save # 저장
=> true

irb> p.new_record? # 이제 더 이상 새 레코드가 아님
=> false

irb> p.persisted? # 이제 저장되어 있음
=> true

새로운 레코드를 저장하면 데이터베이스에 SQL INSERT 연산을 보내고, 기존 레코드를 업데이트하면 SQL UPDATE 연산을 보냅니다. 유효성 검사는 일반적으로 이러한 명령이 데이터베이스로 전송되기 전에 실행됩니다. 유효성 검사에 실패하면 객체는 유효하지 않은 것으로 표시되고 Active Record는 INSERT 또는 UPDATE 연산을 수행하지 않습니다. 이를 통해 유효하지 않은 객체가 데이터베이스에 저장되는 것을 방지할 수 있습니다. 객체가 생성, 저장 또는 업데이트될 때 특정 유효성 검사가 실행되도록 선택할 수 있습니다.

유효성 검사는 일반적으로 유효하지 않은 데이터가 데이터베이스에 저장되는 것을 방지하지만, Rails의 모든 메서드가 유효성 검사를 트리거하지는 않는다는 점을 인지하는 것이 중요합니다. 일부 메서드는 유효성 검사를 수행하지 않고 데이터베이스에 직접 변경을 할 수 있습니다. 결과적으로, 주의하지 않으면 유효성 검사를 우회하여 유효하지 않은 상태의 객체를 저장할 수 있습니다.

다음 메서드들은 유효성 검사를 트리거하며, 객체가 유효한 경우에만 데이터베이스에 객체를 저장합니다:

뱅 버전(느낌표로 끝나는 메서드, 예: save!)은 레코드가 유효하지 않을 경우 예외를 발생시킵니다. 비뱅 버전인 saveupdatefalse를 반환하고, create는 객체를 반환합니다.

1.4 Validation 건너뛰기

다음 메서드들은 validation을 건너뛰고, 유효성과 관계없이 객체를 데이터베이스에 저장합니다. 이들은 주의해서 사용해야 합니다. 자세한 내용은 메서드 문서를 참조하세요.

save는 인자로 validate: false를 전달하면 validation을 건너뛸 수 있습니다. 이 기술은 주의해서 사용해야 합니다.

[touch]:

1.5 Validity 확인하기

Active Record 객체를 저장하기 전에 Rails는 validation을 실행하고, validation에서 오류가 발생하면 Rails는 해당 객체를 저장하지 않습니다.

validation을 직접 실행할 수도 있습니다. valid?는 validation을 실행하고 객체에 오류가 없으면 true를, 그렇지 않으면 false를 반환합니다. 위에서 본 것처럼:

class Person < ApplicationRecord
  validates :name, presence: true
end
irb> Person.new(name: "John Doe").valid?
=> true
irb> Person.new(name: nil).valid?
=> false

Active Record가 유효성 검사를 수행한 후에는 errors 인스턴스 메서드를 통해 실패한 내용에 접근할 수 있으며, 이는 에러 컬렉션을 반환합니다. 정의에 따르면, 유효성 검사를 실행한 후 컬렉션이 비어있다면 해당 객체는 유효한 것입니다.

new로 인스턴스화된 객체는 기술적으로 유효하지 않더라도 에러를 보고하지 않습니다. 이는 유효성 검사가 createsave 메서드와 같이 객체가 저장될 때만 자동으로 실행되기 때문입니다.

class Person < ApplicationRecord
  validates :name, presence: true
end
irb> person = Person.new
=> #<Person id: nil, name: nil, created_at: nil, updated_at: nil>
irb> person.errors.size
=> 0

irb> person.valid?
=> false
irb> person.errors.objects.first.full_message 
=> "Name은 비워둘 수 없습니다"

irb> person.save
=> false

irb> person.save!
ActiveRecord::RecordInvalid: 유효성 검증 실패: Name은 비워둘 수 없습니다

irb> Person.create!
ActiveRecord::RecordInvalid: 유효성 검증 실패: Name은 비워둘 수 없습니다

invalid?valid?의 반대입니다. 검증을 실행하고, 객체에서 오류가 발견되면 true를, 그렇지 않으면 false를 반환합니다.

1.6 에러 검사와 처리

객체의 특정 attribute가 유효한지 여부를 확인하려면 errors[:attribute]를 사용할 수 있습니다. 이는 :attribute에 대한 모든 에러 메시지를 배열로 반환합니다. 지정된 attribute에 에러가 없는 경우 빈 배열이 반환됩니다. 이를 통해 특정 attribute에 유효성 검증 문제가 있는지 쉽게 확인할 수 있습니다.

다음은 attribute의 에러를 확인하는 방법을 보여주는 예시입니다:

class Person < ApplicationRecord
  validates :name, presence: true
end
irb> new_person = Person.new
irb> new_person.errors[:name] 
=> [] # validation이 저장될 때까지 실행되지 않으므로 오류 없음
irb> new_person.errors[:name].any?
=> false

irb> create_person = Person.create
irb> create_person.errors[:name]
=> ["비어 있을 수 없습니다"] # `name`이 필수이므로 validation 오류 발생
irb> create_person.errors[:name].any?  
=> true

추가적으로, errors.add 메서드를 사용하여 특정 attributes에 대한 error message를 수동으로 추가할 수 있습니다. 이는 커스텀 validation 시나리오를 정의할 때 특히 유용합니다.

class Person < ApplicationRecord
  validate do |person|
    errors.add :name, :too_short, message: "길이가 충분하지 않습니다"
  end
end

validation 에러에 대해 더 자세히 알아보려면 Validation 에러 다루기 섹션을 참고하세요.

2 Validations

Active Record는 클래스 정의 내에서 직접 사용할 수 있는 많은 사전 정의된 validation들을 제공합니다. 이러한 사전 정의된 validation들은 일반적인 validation 규칙을 제공합니다. validation이 실패할 때마다 에러 메시지가 객체의 errors 컬렉션에 추가되며, 이 에러는 validation이 수행되는 특정 attribute와 연결됩니다.

validation이 실패하면 에러 메시지는 validation을 트리거한 attribute 이름 아래의 errors 컬렉션에 저장됩니다. 이는 특정 attribute와 관련된 에러에 쉽게 접근할 수 있다는 것을 의미합니다. 예를 들어, :name attribute를 검증하고 validation이 실패하면 errors[:name] 아래에서 에러 메시지를 찾을 수 있습니다.

최신 Rails 애플리케이션에서는 더 간결한 validate 구문이 일반적으로 사용됩니다. 예를 들면:

validate :name, presence: true

하지만 이전 버전의 Rails는 다음과 같은 "helper" 메서드를 사용했습니다:

validates_presence_of :name

:name의 존재 여부를 검증합니다.

두 표기법은 동일한 기능을 수행하지만, 가독성과 Rails 규칙과의 일관성을 위해 새로운 형식을 권장합니다.

각 validation은 임의의 개수의 attribute 이름을 허용하므로, 한 줄의 코드로 여러 attribute에 동일한 유형의 validation을 적용할 수 있습니다.

또한, 모든 validation은 :on:message 옵션을 받습니다. :on 옵션은 validation이 실행될 시점을 지정하며, :create 또는 :update 값을 사용할 수 있습니다. :message 옵션을 사용하면 validation이 실패할 경우 errors 컬렉션에 추가될 사용자 지정 오류 메시지를 정의할 수 있습니다. 메시지를 지정하지 않으면 Rails는 해당 validation에 대한 기본 오류 메시지를 사용합니다.

사용 가능한 기본 헬퍼의 목록을 보려면 ActiveModel::Validations::HelperMethods를 참조하세요. 이 API 섹션에서는 위에서 설명한 이전 표기법을 사용합니다.

아래에서 가장 일반적으로 사용되는 validation들을 설명합니다.

2.1 absence

이 validator는 지정된 속성이 존재하지 않는지를 검증합니다. 값이 nil도 아니고 빈 문자열도 아닌지 (즉, 비어있거나 공백만으로 이루어진 문자열) 확인하기 위해 Object#present? 메서드를 사용합니다.

#absence는 조건부 validation에서 흔히 사용됩니다. 예를 들면:

class Person < ApplicationRecord
  validates :phone_number, :address, absence: true, if: :invited?
end

Person 모델의 phone_number와 address 속성에 대해, invited? 메서드가 true를 반환할 경우 값이 없어야 함을 검증합니다.

irb> person = Person.new(name: "Jane Doe", invitation_sent_at: Time.current)
irb> person.valid?
=> true # absence 유효성 검사 통과

만약 연관관계가 없음을 확실히 하고 싶다면, 연관관계를 매핑하기 위해 사용된 foreign key가 아닌 연관된 객체 자체가 없는지를 테스트해야 합니다.

class LineItem < ApplicationRecord
  belongs_to :order, optional: true 
  validates :order, absence: true # order가 없어야(null)만 한다고 검증합니다
end
irb> line_item = LineItem.new
irb> line_item.valid?
=> true # 부재 검증 통과

order = Order.create
irb> line_item_with_order = LineItem.new(order: order)
irb> line_item_with_order.valid?
=> false # 부재 검증 실패

belongs_to의 경우 association presence는 기본적으로 검증됩니다. Association presence 검증을 원하지 않는다면 optional: true를 사용하세요.

Rails는 보통 inverse association을 자동으로 추론합니다. 커스텀 :foreign_key:through association을 사용하는 경우, association 조회를 최적화하기 위해 :inverse_of 옵션을 명시적으로 설정하는 것이 중요합니다. 이는 검증 중 불필요한 데이터베이스 쿼리를 피하는 데 도움이 됩니다.

자세한 내용은 양방향 Association 문서를 확인하세요.

Association이 존재하면서 유효한지 확인하려면, validates_associated도 함께 사용해야 합니다. 자세한 내용은 validates_associated 섹션을 참조하세요.

has_one이나 has_many 관계를 통해 연결된 객체의 부재를 검증하는 경우, 해당 객체가 present?도 아니고 marked_for_destruction?도 아닌지 확인합니다.

false.present?는 false이기 때문에, boolean 필드의 부재를 검증하고 싶다면 다음을 사용해야 합니다:

validates :field_name, exclusion: { in: [true, false] }

field_name이 true나 false로 설정되지 않도록 검증합니다.

기본 에러 메시지는 "must be blank" 입니다.

2.2 acceptance

이 메서드는 폼이 제출될 때 사용자 인터페이스의 checkbox가 체크되었는지를 검증합니다. 이는 일반적으로 사용자가 애플리케이션의 이용약관에 동의하거나, 특정 텍스트를 읽었음을 확인하거나, 이와 유사한 개념을 확인해야 할 때 사용됩니다.

class Person < ApplicationRecord
  validates :terms_of_service, acceptance: true
end

이것은 person이 웹 응용 프로그램의 이용약관에 동의했는지 검증할 때 유용합니다.

이 검증은 terms_of_servicenil이 아닌 경우에만 수행됩니다. 이 검증의 기본 에러 메시지는 "must be accepted" 입니다. message 옵션을 통해 커스텀 메시지를 전달할 수도 있습니다.

class Person < ApplicationRecord
  validates :terms_of_service, acceptance: { message: "에 동의해야 합니다" }
end

허용 가능한 값을 결정하는 :accept 옵션도 받을 수 있습니다. 기본값은 ['1', true]이며 쉽게 변경할 수 있습니다.

class Person < ApplicationRecord
  validates :terms_of_service, acceptance: { accept: "yes" }
  validates :eula, acceptance: { accept: ["TRUE", "accepted"] }
end

이 예제는 terms_of_service가 "yes"로 제한되고 eula가 "TRUE" 또는 "accepted" 중 하나로 제한되는 것을 보여줍니다.

이 validation은 웹 애플리케이션에 매우 특화되어 있으며 이 'acceptance'는 데이터베이스 어디에도 기록될 필요가 없습니다. 만약 이를 위한 필드가 없다면, validator는 virtual attribute를 생성할 것입니다. 만약 필드가 데이터베이스에 존재한다면, accept 옵션은 반드시 true로 설정되거나 true를 포함해야 합니다. 그렇지 않으면 validation이 실행되지 않습니다.

2.3 confirmation

두 개의 텍스트 필드가 정확히 동일한 내용을 받아야 할 때 이 validator를 사용해야 합니다. 예를 들어, email 주소나 password를 확인하고자 할 때 사용할 수 있습니다. 이 validation은 확인이 필요한 필드의 이름 뒤에 "_confirmation"이 붙은 가상 attribute를 생성합니다.

class Person < ApplicationRecord
  validates :email, confirmation: true
end

confirmation validation helper는 두 개의 text field가 정확히 동일한 내용을 담고 있는지 검증할 때 사용됩니다. 예를 들어, 이메일이나 비밀번호를 확인할 때 사용합니다. 이 validation은 _confirmation이라는 접미사가 붙은 가상의 attribute를 생성합니다.

view template에서 아래와 같이 사용할 수 있습니다

<%= text_field :person, :email %>
<%= text_field :person, :email_confirmation %>

이 검사는 email_confirmationnil이 아닌 경우에만 수행됩니다. confirmation을 필수로 하려면, confirmation 속성에 presence 검사를 추가해야 합니다(이 가이드의 뒷부분에서 presence 검사에 대해 살펴보겠습니다):

class Person < ApplicationRecord
  validates :email, confirmation: true 
  validates :email_confirmation, presence: true
end

또한 confirmation 제약이 대소문자를 구분할지 여부를 정의하는 :case_sensitive 옵션이 있습니다. 이 옵션의 기본값은 true입니다.

class Person < ApplicationRecord
  validates :email, confirmation: { case_sensitive: false }
end

문자열의 대소문자를 구분하지 않고 attribute 확인 검증을 원할 경우 :case_sensitive 옵션을 추가할 수 있습니다.

이 validator의 기본 오류 메시지는 "doesn't match confirmation" 입니다. message 옵션을 통해 사용자 정의 메시지를 전달할 수도 있습니다.

일반적으로 이 validator를 사용할 때는 :if 옵션과 함께 사용하여 레코드를 저장할 때마다가 아닌 초기 필드가 변경되었을 때만 "_confirmation" 필드를 검증하도록 하는 것이 좋습니다. 조건부 검증에 대해서는 나중에 더 자세히 알아보겠습니다.

class Person < ApplicationRecord
  validates :email, confirmation: true
  validates :email_confirmation, presence: true, if: :email_changed?
end

위 코드에서는 두번째 email 필드가 첫번째 email 필드와 정확히 일치하는지 확인합니다. email_confirmation attribute를 모델에 추가하고 (데이터베이스에 저장되지 않더라도) email이 변경될 때만 validation을 실행합니다.

2.4 comparison

이 validator는 비교 가능한 두 값 사이의 비교를 검증합니다.

class Promotion < ApplicationRecord
  validates :end_date, comparison: { greater_than: :start_date }
end

이 validator의 기본 에러 메시지는 "failed comparison" 입니다. message 옵션을 통해 커스텀 메시지를 전달할 수도 있습니다.

다음 옵션들이 지원됩니다:

옵션 설명 기본 에러 메시지
:greater_than 값이 제공된 값보다 커야 함을 지정합니다. "must be greater than %{count}"
:greater_than_or_equal_to 값이 제공된 값보다 크거나 같아야 함을 지정합니다. "must be greater than or equal to %{count}"
:equal_to 값이 제공된 값과 같아야 함을 지정합니다. "must be equal to %{count}"
:less_than 값이 제공된 값보다 작아야 함을 지정합니다. "must be less than %{count}"
:less_than_or_equal_to 값이 제공된 값보다 작거나 같아야 함을 지정합니다. "must be less than or equal to %{count}"
:other_than 값이 제공된 값과 달라야 함을 지정합니다. "must be other than %{count}"

이 validator는 비교 옵션이 제공되어야 합니다. 각 옵션은 값, proc, 또는 symbol을 받습니다. Comparable을 포함하는 모든 클래스는 비교가 가능합니다.

2.5 format

이 validator는 :with 옵션에 지정된 정규표현식과 일치하는지 테스트하여 속성의 값을 검증합니다.

class Product < ApplicationRecord
  validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/,
    message: "문자만 허용됩니다" }
end

반대로, :without 옵션을 사용하면 지정된 속성이 정규 표현식과 일치하지 않도록 요구할 수 있습니다.

어느 경우든, 제공된 :with 또는 :without 옵션은 정규 표현식이거나 정규 표현식을 반환하는 proc 또는 lambda여야 합니다.

기본 에러 메시지는 "is invalid" 입니다.

문자열의 시작과 끝을 매칭하려면 \A\z를 사용하세요. ^$는 행의 시작/끝을 매칭합니다. ^$의 잦은 오용으로 인해, 제공된 정규 표현식에서 이 두 앵커를 사용하는 경우 multiline: true 옵션을 전달해야 합니다. 대부분의 경우 \A\z를 사용해야 합니다.

2.6 inclusionexclusion

이 두 validator는 속성의 값이 주어진 집합에 포함되어 있는지 또는 제외되어 있는지를 검증합니다. 이 집합은 배열, 범위, 또는 proc, lambda, symbol을 사용하여 동적으로 생성된 컬렉션과 같은 모든 열거 가능한 객체가 될 수 있습니다.

  • inclusion은 값이 집합에 존재하는지 확인합니다.
  • exclusion은 값이 집합에 존재하지 않는지 확인합니다.

두 경우 모두, :in 옵션은 값의 집합을 받으며, :within을 별칭으로 사용할 수 있습니다. 오류 메시지 사용자 정의에 대한 전체 옵션은 message 문서를 참조하세요.

열거 가능한 객체가 숫자, 시간, 또는 datetime 범위인 경우, 검증은 Range#cover?를 사용하여 수행되며, 그렇지 않은 경우 include?를 사용합니다. proc이나 lambda를 사용할 때는 검증 중인 인스턴스가 인자로 전달되어 동적 검증이 가능합니다.

2.6.1 예시

inclusion의 경우:

class Coffee < ApplicationRecord
  validates :size, inclusion: { in: %w(small medium large),
    message: "%{value}는 유효한 크기가 아닙니다" }
end

For exclusion:

exclusion 헬퍼는 주어진 집합에 attribute가 포함되지 않는지 검증합니다. 사용법은 inclusion 헬퍼와 동일합니다:

class Account < ApplicationRecord
  validates :subdomain, exclusion: { in: ['www', 'us', 'ca', 'jp'],
    message: "Subdomain %{value} is reserved." }
end

exclusion 헬퍼는 :in 옵션에 대해 별칭으로 :within을 가집니다. 이 헬퍼는 :message 옵션에서 %{value}를 사용하여 실패한 attribute의 값을 받아들일 수 있습니다.

기본 에러 메시지는 "is reserved"입니다.

class Account < ApplicationRecord
  validates :subdomain, exclusion: { in: %w(www us ca jp),
    message: "%{value}는 예약되어 있습니다." }
end

두 validator 모두 enumerable을 반환하는 메서드를 통한 동적 validation을 허용합니다. 다음은 inclusion에 proc을 사용하는 예시입니다:

class Coffee < ApplicationRecord
  validates :size, inclusion: { in: ->(coffee) { coffee.available_sizes } }

  def available_sizes
    %w(small medium large extra_large) # small, medium, large, extra_large 값들을 사용할 수 있습니다
  end
end

exclusion의 경우도 마찬가지입니다:

class Account < ApplicationRecord
  validates :subdomain, exclusion: { in: ->(account) { account.reserved_subdomains } }

  def reserved_subdomains
    %w(www us ca jp admin) # 사용할 수 없는 subdomain 목록
  end
end

2.7 length

이 validator는 속성 값의 길이를 검증합니다. 다양한 옵션을 제공하므로 여러 가지 방식으로 길이 제약조건을 지정할 수 있습니다:

class Person < ApplicationRecord
  validates :name, length: { minimum: 2 }  # 이름은 최소 2자 이상이어야 함
  validates :bio, length: { maximum: 500 } # 자기소개는 최대 500자까지 허용됨 
  validates :password, length: { in: 6..20 } # 비밀번호는 6-20자 사이여야 함
  validates :registration_number, length: { is: 6 } # 등록번호는 6자여야 함
end

가능한 길이 제약 조건 옵션은 다음과 같습니다:

옵션 설명
:minimum 속성은 지정된 길이보다 작을 수 없습니다.
:maximum 속성은 지정된 길이보다 클 수 없습니다.
:in 속성 길이는 주어진 범위 내에 있어야 합니다. 이 옵션의 값은 range여야 합니다.
:is 속성 길이는 주어진 값과 정확히 일치해야 합니다.

기본 에러 메시지는 수행되는 길이 validation의 유형에 따라 다릅니다. :wrong_length, :too_long, :too_short 옵션을 사용하고 사용 중인 길이 제약 조건에 해당하는 숫자의 자리 표시자로 %{count}를 사용하여 이러한 메시지를 커스터마이즈할 수 있습니다. 에러 메시지를 지정하기 위해 :message 옵션을 계속 사용할 수 있습니다.

class Person < ApplicationRecord
  validates :bio, length: { maximum: 1000, 
    too_long: "최대 %{count}자까지 입력 가능합니다" }
end

기본 오류 메시지는 복수형입니다(예: "is too short (minimum is %{count} characters)"). 이러한 이유로, :minimum이 1일 때는 사용자 정의 메시지를 제공하거나 대신 presence: true를 사용해야 합니다. 마찬가지로, :in 또는 :within의 하한값이 1일 때는 사용자 정의 메시지를 제공하거나 length 이전에 presence를 호출해야 합니다. :minimum:maximum 옵션은 함께 조합할 수 있지만, 그 외에는 한 번에 하나의 제약 조건 옵션만 사용할 수 있습니다.

2.8 numericality

이 validator는 attributes가 숫자 값만 가지도록 검증합니다. 기본적으로 선택적 부호가 붙고 그 뒤에 정수나 부동 소수점 숫자가 오는 형태를 허용합니다.

정수만 허용하도록 지정하려면 :only_integer를 true로 설정하세요. 그러면 attribute의 값을 검증하기 위해 다음의 정규 표현식을 사용합니다.

/\A[+-]?\d+\z/

이 패턴은 선행/후행 공백이 없는 정수만 매치합니다. 예를 들어 "+1234"나 "1234"나 "-1234"는 매치되지만 "+1,234", "12.34" 또는 "1234 " 등은 매치되지 않습니다.

그렇지 않으면 Float를 사용하여 값을 숫자로 변환하려고 시도합니다. Float는 column의 precision 값 또는 최대 15자리를 사용하여 BigDecimal로 변환됩니다.

class Player < ApplicationRecord
  validates :points, numericality: true
  validates :games_played, numericality: { only_integer: true }
end

:only_integer의 기본 오류 메시지는 "must be an integer" 입니다.

:only_integer 외에도 이 validator는 :only_numeric 옵션을 받아들입니다. 이는 값이 Numeric 인스턴스여야 하며 값이 String인 경우 파싱을 시도합니다.

기본적으로 numericalitynil 값을 허용하지 않습니다. allow_nil: true 옵션을 사용하여 이를 허용할 수 있습니다. IntegerFloat 컬럼의 경우 빈 문자열은 nil로 변환됩니다.

옵션이 지정되지 않은 경우의 기본 오류 메시지는 "is not a number" 입니다.

허용 가능한 값에 제약을 추가하는 데 사용할 수 있는 많은 옵션들이 있습니다:

옵션 설명 기본 오류 메시지
:greater_than 값이 제공된 값보다 커야 함을 지정합니다. "must be greater than %{count}"
:greater_than_or_equal_to 값이 제공된 값보다 크거나 같아야 함을 지정합니다. "must be greater than or equal to %{count}"
:equal_to 값이 제공된 값과 같아야 함을 지정합니다. "must be equal to %{count}"
:less_than 값이 제공된 값보다 작아야 함을 지정합니다. "must be less than %{count}"
:less_than_or_equal_to 값이 제공된 값보다 작거나 같아야 함을 지정합니다. "must be less than or equal to %{count}"
:other_than 값이 제공된 값과 달라야 함을 지정합니다. "must be other than %{count}"
:in 값이 제공된 범위 내에 있어야 함을 지정합니다. "must be in %{count}"
:odd 값이 홀수여야 함을 지정합니다. "must be odd"
:even 값이 짝수여야 함을 지정합니다. "must be even"

2.9 presence

이 validator는 지정된 attributes가 비어있지 않은지 검증합니다. 값이 nil인지 빈 문자열인지 확인하기 위해 Object#blank? 메서드를 사용합니다 - 즉, 문자열이 비어있거나 공백으로만 이루어져 있는지 확인합니다.

class Person < ApplicationRecord
  validates :name, :login, :email, presence: true
end

위 모델은 name, login, email의 presence(존재여부) validation을 합니다.

person = Person.new(name: "Alice", login: "alice123", email: "alice@example.com")
person.valid?
=> true # presence validation 통과

invalid_person = Person.new(name: "", login: nil, email: "bob@example.com")
invalid_person.valid?
=> false # presence validation 실패

association의 존재여부를 확인하기 위해서는, foreign key 매핑을 사용하는 것이 아닌 연관된 객체가 존재하는지 테스트해야 합니다. association을 테스트하면 foreign key가 비어있지 않은지와 참조된 객체가 존재하는지를 확인하는데 도움이 됩니다.

class Supplier < ApplicationRecord
  has_one :account
  validates :account, presence: true
end
irb> account = Account.create(name: "Account A")

irb> supplier = Supplier.new(account: account) 
irb> supplier.valid?
=> true # presence 유효성 검사 통과

irb> invalid_supplier = Supplier.new
irb> invalid_supplier.valid? 
=> false # presence 유효성 검사 실패

사용자 정의 :foreign_key:through association을 사용하는 경우, association lookup을 최적화하기 위해 :inverse_of 옵션을 명시적으로 설정하는 것이 중요합니다. 이는 유효성 검사 중에 불필요한 데이터베이스 쿼리를 피하는데 도움이 됩니다.

자세한 내용은 양방향 Association 문서를 확인하세요.

association이 존재하고 유효한지 모두 확인하고 싶다면, validates_associated도 함께 사용해야 합니다. 자세한 내용은 아래를 참조하세요.

has_one이나 has_many 관계를 통해 연결된 객체의 존재 여부를 검증하는 경우, 해당 객체가 blank?도 아니고 marked_for_destruction?도 아닌지 확인합니다.

false.blank?가 true이기 때문에, boolean 필드의 존재 여부를 검증하고 싶다면 다음 유효성 검사 중 하나를 사용해야 합니다:

# 값은 _반드시_ true 또는 false여야 합니다
validates :boolean_field_name, inclusion: [true, false]
# 값은 _절대_ nil이 되면 안됩니다, 즉 true 또는 false여야 합니다
validates :boolean_field_name, exclusion: [nil]

이러한 validation들 중 하나를 사용하면 값이 nil이 되지 않도록 보장할 수 있으며, 대부분의 경우 NULL 값이 되는 것을 방지할 수 있습니다.

기본 에러 메시지는 "can't be blank" 입니다.

2.10 uniqueness

이 validator는 객체가 저장되기 직전에 속성 값이 고유한지 검증합니다.

class Account < ApplicationRecord
  validates :email, uniqueness: true
end

validation은 model의 테이블에 SQL 쿼리를 수행하여, 해당 attribute에 동일한 값을 가진 기존 레코드를 검색하여 이루어집니다.

uniqueness 검사를 제한하는데 사용할 하나 이상의 attribute를 지정하는데 사용할 수 있는 :scope 옵션이 있습니다:

class Holiday < ApplicationRecord
  validates :name, uniqueness: { scope: :year,
    message: "은 연간 한 번만 등록할 수 있습니다" }
end

이 validation은 데이터베이스에 uniqueness constraint를 생성하지 않으므로, 서로 다른 두 데이터베이스 연결이 unique하도록 의도한 컬럼에 대해 동일한 값을 가진 두 개의 레코드를 생성하는 시나리오가 발생할 수 있습니다. 이를 방지하려면 데이터베이스의 해당 컬럼에 unique index를 생성해야 합니다.

데이터베이스에 uniqueness database constraint를 추가하기 위해서는 migration에서 add_index 구문을 사용하고 unique: true 옵션을 포함시키세요.

uniqueness validation에서 :scope 옵션을 사용하고 있고, uniqueness validation의 위반 가능성을 방지하기 위한 database constraint를 생성하고자 한다면, 데이터베이스의 두 컬럼 모두에 대해 unique index를 생성해야 합니다. 다중 컬럼 인덱스에 대한 자세한 내용은 the MySQL manualthe MariaDB manual을 참조하거나, 컬럼 그룹을 참조하는 unique constraint의 예시는 the PostgreSQL manual을 참조하세요.

또한 uniqueness constraint가 대소문자를 구분할지, 구분하지 않을지, 또는 기본 데이터베이스 collation을 따를지를 정의하는 데 사용할 수 있는 :case_sensitive 옵션이 있습니다. 이 옵션은 기본적으로 기본 데이터베이스 collation을 따르도록 설정되어 있습니다.

class Person < ApplicationRecord
  validates :name, uniqueness: { case_sensitive: false }
end

위 코드는 name이 대소문자를 구분하지 않고 고유해야 함을 지정합니다.

일부 데이터베이스는 기본적으로 대소문자를 구분하지 않는 검색을 수행하도록 설정되어 있습니다.

:conditions 옵션을 사용하여 고유성 제약 조건 조회를 제한하기 위한 추가 조건을 WHERE SQL 프래그먼트로 지정할 수 있습니다:

validates :name, uniqueness: { conditions: -> { where(status: "active") } }

name이 status가 "active"인 레코드들 사이에서만 유일해야 함을 검증합니다.

기본 에러 메시지는 "has already been taken" 입니다.

자세한 정보는 validates_uniqueness_of를 참고하세요.

2.11 validates_associated

모델이 항상 검증이 필요한 associations을 가지고 있을 때 이 validator를 사용해야 합니다. 객체를 저장하려고 할 때마다 연관된 각 객체들에 대해 valid?가 호출됩니다.

class Library < ApplicationRecord
  has_many :books
  validates_associated :books
end

이 validator는 모델과 연결된 객체들이 저장될 때 모두 유효한지를 확인합니다. 객체가 여러 개인 경우 각각 유효성 검사를 수행합니다. 하나라도 실패하면 에러 메시지가 추가됩니다.

이 유효성 검사는 연결된 객체를 자동으로 저장하지는 않습니다.

이 validation은 모든 association 타입에서 작동합니다.

association의 양쪽 끝에서 validates_associated를 사용하지 마세요. 무한 루프로 서로를 호출하게 될 것입니다.

validates_associated의 기본 에러 메시지는 "is invalid" 입니다. 각각의 연관된 객체가 자신만의 errors 컬렉션을 가지며, 에러는 호출하는 모델로 전파되지 않는다는 점에 유의하세요.

validates_associated는 ActiveRecord 객체에서만 사용할 수 있으며, 지금까지 설명한 나머지는 ActiveModel::Validations를 포함하는 모든 객체에서도 사용할 수 있습니다.

2.12 validates_each

이 validator는 블록을 사용하여 속성을 검증합니다. 미리 정의된 검증 함수는 없습니다. 블록을 사용하여 검증 함수를 직접 만들어야 하며, validates_each에 전달된 모든 속성이 해당 블록으로 테스트됩니다.

다음 예제에서는 소문자로 시작하는 이름과 성을 거부하도록 하겠습니다.

class Person < ApplicationRecord
  validates_each :name, :surname do |record, attr, value|
    record.errors.add(attr, "대문자로 시작해야 합니다") if /\A[[:lower:]]/.match?(value) 
  end
end

block은 레코드, 속성명, 속성값을 받습니다.

block 내에서 유효한 데이터를 확인하기 위해 원하는 작업을 수행할 수 있습니다. 만약 validation이 실패한다면, model에 에러를 추가해야 하며, 이로 인해 invalid 상태가 됩니다.

2.13 validates_with

이 validator는 validation을 위해 별도의 class로 record를 전달합니다.

class AddressValidator < ActiveModel::Validator
  def validate(record)
    if record.house_number.blank?
      record.errors.add :house_number, "필수 필드입니다"
    end

    if record.street.blank?
      record.errors.add :street, "필수 필드입니다"  
    end

    if record.postcode.blank?
      record.errors.add :postcode, "필수 필드입니다"
    end
  end
end

class Invoice < ApplicationRecord
  validates_with AddressValidator
end

validates_with에 대한 기본 에러 메시지는 없습니다. validator 클래스 내에서 record의 errors 컬렉션에 수동으로 에러를 추가해야 합니다.

record.errors[:base]에 추가된 에러는 record 전체 상태와 관련이 있습니다.

validate 메서드를 구현하려면, 메서드 정의에서 검증할 record를 나타내는 record 파라미터를 받아야 합니다.

특정 attribute에 에러를 추가하려면, add 메서드의 첫 번째 인자로 해당 attribute를 전달할 수 있습니다.

def validate(record)
  if record.some_field != "acceptable"
    record.errors.add :some_field, "이 필드는 허용되지 않습니다"
  end
end

우리는 validation error에 대해서 나중에 더 자세히 다룰 것입니다.

validates_with validator는 validation을 위해 사용할 클래스 또는 클래스 목록을 받습니다.

class Person < ApplicationRecord
  validates_with MyValidator, MyOtherValidator, on: :create
end

클래스 사용자 정의 validator들을 차례로 수행하기 위해서는 위와 같이 여러 validator들을 콤마로 구분해서 나열할 수 있습니다.

다른 모든 validation과 마찬가지로 validates_with:if, :unless, :on 옵션을 사용할 수 있습니다. 만약 다른 옵션을 전달하면 그 옵션들은 validator 클래스에 options로 전달됩니다:

class AddressValidator < ActiveModel::Validator 
  def validate(record)
    options[:fields].each do |field|
      if record.send(field).blank?
        record.errors.add field, "가 필요합니다"
      end
    end
  end
end

class Invoice < ApplicationRecord
  validates_with AddressValidator, fields: [:house_number, :street, :postcode, :country]
end

validator는 각각의 validation 실행마다가 아닌 전체 애플리케이션 생명주기에서 단 한 번만 초기화됩니다. 따라서 validator 내에서 인스턴스 변수를 사용할 때는 주의해야 합니다.

만약 validator가 인스턴스 변수가 필요할 만큼 복잡하다면, 일반적인 Ruby 객체를 대신 사용하면 됩니다:

class Invoice < ApplicationRecord
  validate do |invoice|
    AddressValidator.new(invoice).validate
  end
end

class AddressValidator
  def initialize(invoice)
    @invoice = invoice
  end

  def validate 
    validate_field(:house_number)
    validate_field(:street) 
    validate_field(:postcode)
  end

  private
    def validate_field(field)
      if @invoice.send(field).blank?
        @invoice.errors.add field, "#{field.to_s.humanize}는 필수입니다"
      end
    end
end

custom validations에 대해서는 나중에 다루겠습니다.

3 Validation 옵션

validator들이 지원하는 여러 공통 옵션들이 있습니다. 이 옵션들은 다음과 같습니다:

  • :allow_nil: 속성이 nil인 경우 validation을 건너뜁니다.
  • :allow_blank: 속성이 blank인 경우 validation을 건너뜁니다.
  • :message: 사용자 정의 에러 메시지를 지정합니다.
  • :on: 이 validation이 활성화되는 컨텍스트를 지정합니다.
  • :strict: validation이 실패할 때 예외를 발생시킵니다.
  • :if:unless: validation이 실행되어야 하거나 실행되지 말아야 할 때를 지정합니다.

모든 validator가 이러한 옵션들을 전부 지원하지는 않습니다. ActiveModel::Validations API 문서를 참조하시기 바랍니다.

3.1 :allow_nil

:allow_nil 옵션은 검증되는 값이 nil일 때 validation을 건너뜁니다.

class Coffee < ApplicationRecord
  validates :size, inclusion: { in: %w(small medium large),
    message: "%{value}는 유효한 크기가 아닙니다" }, allow_nil: true
end
irb> Coffee.create(size: nil).valid?
=> true
irb> Coffee.create(size: "mega").valid?
=> false

메시지 인자에 대한 모든 옵션은 message 문서를 참조하세요.

3.2 :allow_blank

:allow_blank 옵션은 :allow_nil 옵션과 비슷합니다. 이 옵션은 attribute의 값이 nil이나 빈 문자열과 같이 blank?인 경우에 validation을 통과시킵니다.

class Topic < ApplicationRecord
  validates :title, length: { is: 5 }, allow_blank: true
end

이 예시는 title이 정확히 5 문자라면 blank 값을 허용합니다.

irb> Topic.create(title: "").valid?
=> true
irb> Topic.create(title: nil).valid?
=> true
irb> Topic.create(title: "short").valid?
=> false # 'short'의 길이가 5가 아니므로, blank가 아니더라도 validation이 실패합니다

3.3 :message

이미 보셨듯이, :message 옵션을 사용하면 validation이 실패했을 때 errors 컬렉션에 추가될 메시지를 지정할 수 있습니다. 이 옵션이 사용되지 않으면 Active Record는 각 validation에 대한 기본 오류 메시지를 사용합니다.

:message 옵션은 String 또는 Proc을 값으로 받습니다.

String :message 값은 선택적으로 %{value}, %{attribute}, %{model}을 포함할 수 있으며, validation이 실패했을 때 동적으로 대체됩니다. 이 대체는 i18n gem을 사용하여 수행되며, 플레이스홀더는 정확히 일치해야 하고 공백은 허용되지 않습니다.

class Person < ApplicationRecord
  # 하드코딩된 메시지
  validates :name, presence: { message: "must be given please" }

  # 동적 속성값을 가진 메시지. %{value}는 속성의 실제값으로 대체됩니다. 
  # %{attribute}와 %{model}도 사용 가능합니다.
  validates :age, numericality: { message: "%{value} seems wrong" }
end

Proc :message 값은 두 개의 인자를 받습니다: 검증되는 객체와 :model, :attribute, :value 키-값 쌍을 가진 해시입니다.

class Person < ApplicationRecord
  validates :username,
    uniqueness: {
      # object = 검증되는 person 객체 
      # data = { model: "Person", attribute: "Username", value: <username> }
      message: ->(object, data) do
        "안녕하세요 #{object.name}님, #{data[:value]} 은/는 이미 사용 중입니다."
      end
    }
end

에러 메시지를 번역하려면 I18n 가이드를 참조하세요.

:on 옵션을 사용하면 validation이 실행되어야 하는 시점을 지정할 수 있습니다. 모든 내장 validation의 기본 동작은 저장 시점(새 record 생성과 업데이트 모두)에 실행됩니다. 이를 변경하려면 on: :create를 사용하여 새 record가 생성될 때만 validation을 실행하거나, on: :update를 사용하여 record가 업데이트될 때만 validation이 실행되도록 할 수 있습니다.

class Person < ApplicationRecord
  # 중복된 값으로 email을 업데이트하는 것이 가능합니다
  validates :email, uniqueness: true, on: :create

  # 숫자가 아닌 age로 레코드를 생성하는 것이 가능합니다
  validates :age, numericality: true, on: :update

  # 기본값 (create와 update 모두에서 검증)
  validates :name, presence: true
end

:on을 사용하여 custom context를 정의할 수도 있습니다. Custom context는 valid?, invalid?, 또는 save 메서드에 context 이름을 명시적으로 전달하여 트리거해야 합니다.

class Person < ApplicationRecord
  validates :email, uniqueness: true, on: :account_setup
  validates :age, numericality: true, on: :account_setup
end
irb> person = Person.new(age: 'thirty-three')
irb> person.valid?
=> true
irb> person.valid?(:account_setup) 
=> false
irb> person.errors.messages
=> {:email=>["이미 사용중입니다"], :age=>["숫자가 아닙니다"]}

person.valid?(:account_setup)은 모델을 저장하지 않고 두 개의 validation을 모두 실행합니다. person.save(context: :account_setup)은 저장하기 전에 account_setup context에서 person을 검증합니다.

심볼의 배열을 전달하는 것도 가능합니다.

class Book
  include ActiveModel::Validations

  validates :title, presence: true, on: [:update, :ensure_title]
end
irb> book = Book.new(title: nil)
irb> book.valid?
=> true
irb> book.valid?(:ensure_title)
=> false
irb> book.errors.messages
=> {:title=>["비어있을 수 없습니다"]}

명시적 context에 의해 유발되면 해당 context와 context가 없는 모든 validation이 실행됩니다.

class Person < ApplicationRecord
  validates :email, uniqueness: true, on: :account_setup
  validates :age, numericality: true, on: :account_setup
  validates :name, presence: true 
end
irb> person = Person.new
irb> person.valid?(:account_setup)
=> false
irb> person.errors.messages
=> {:email=>["이미 사용중입니다"], :age=>["숫자가 아닙니다"], :name=>["비워둘 수 없습니다"]}

:on에 대한 추가적인 활용 사례는 Custom Contexts 섹션에서 확인할 수 있습니다.

4 조건부 Validations

때로는 주어진 조건이 충족될 때만 객체를 검증하는 것이 합리적일 수 있습니다. symbol, Proc 또는 Array를 사용할 수 있는 :if:unless 옵션을 사용하여 이를 수행할 수 있습니다. validation이 실행되어야 할 때를 지정하고 싶다면 :if 옵션을 사용할 수 있습니다. 반대로 validation이 실행되지 말아야 할 때를 지정하고 싶다면 :unless 옵션을 사용할 수 있습니다.

4.1 :if:unless에 Symbol 사용하기

:if:unless 옵션을 validation이 발생하기 직전에 호출될 메서드의 이름에 해당하는 symbol과 연결할 수 있습니다. 이것이 가장 일반적으로 사용되는 옵션입니다.

class Order < ApplicationRecord
  validates :card_number, presence: true, if: :paid_with_card?

  def paid_with_card?
    payment_type == "card" 
  end
end

4.2 :if:unless에서 Proc 사용하기

:if:unless에 호출될 Proc 객체를 연결할 수 있습니다. Proc 객체를 사용하면 별도의 메서드 대신 인라인 조건을 작성할 수 있습니다. 이 옵션은 한 줄짜리 코드에 가장 적합합니다.

class Account < ApplicationRecord
  validates :password, confirmation: true,
    unless: Proc.new { |a| a.password.blank? }
end

lambdaProc의 한 종류이므로, 축약된 문법을 활용하여 인라인 조건을 작성하는 데에도 사용될 수 있습니다.

validates :password, confirmation: true, unless: -> { password.blank? }

password가 비어있지 않은 경우에만 password의 confirmation을 검증합니다.

4.3 Conditional Validation 그룹화하기

때로는 여러 validation들이 하나의 조건을 사용하도록 하는 것이 유용할 수 있습니다. 이는 with_options를 사용하여 쉽게 달성할 수 있습니다.

class User < ApplicationRecord
  with_options if: :is_admin? do |admin|
    admin.validates :password, length: { minimum: 10 }
    admin.validates :email, presence: true
  end
end

with_options 블록 내부의 모든 validation은 자동으로 if: :is_admin?가 옵션에 병합됩니다.

4.4 Validation 조건 결합하기

여러 조건들이 validation이 발생해야 하는지 여부를 결정할 때는 Array를 사용할 수 있습니다. 또한 동일한 validation에 :if:unless 모두를 적용할 수 있습니다.

class Computer < ApplicationRecord
  validates :mouse, presence: true,
                    if: [Proc.new { |c| c.market.retail? }, :desktop?],
                    unless: Proc.new { |c| c.trackpad.present? }
end

Computer 클래스는 다음과 같이 유효성 검증을 수행합니다: - mouse의 존재 여부를 검증합니다 - market이 retail인 경우와 desktop?이 참인 경우에만 검증이 이루어집니다 - trackpad가 존재하는 경우에는 검증을 하지 않습니다

validation은 모든 :if 조건이 true로 평가되고 모든 :unless 조건이 false로 평가될 때만 실행됩니다.

5 Strict Validations

객체가 유효하지 않을 때 ActiveModel::StrictValidationFailed를 발생시키는 strict validation을 지정할 수도 있습니다.

class Person < ApplicationRecord
  validates :name, presence: { strict: true }
end

위 validator는 name이 없을 때 ActiveModel::StrictValidationFailed 예외를 발생시킵니다.

irb> Person.new.valid?
=> ActiveModel::StrictValidationFailed: Name은 비어있을  없습니다

엄격한 validation은 validation이 실패할 때 즉시 exception을 발생시켜, 즉각적인 피드백이 필요하거나 유효하지 않은 데이터가 발견될 때 처리를 중단하고 싶은 상황에서 유용할 수 있습니다. 예를 들어, 중요한 트랜잭션을 처리하거나 데이터 무결성 검사를 수행할 때와 같이 유효하지 않은 입력이 추가 작업을 방해해야 하는 시나리오에서 엄격한 validation을 사용할 수 있습니다.

:strict 옵션에 사용자 정의 exception을 전달할 수도 있습니다.

class Person < ApplicationRecord
  validates :token, presence: true, uniqueness: true, strict: TokenGenerationException
end
irb> Person.new.valid?
=> TokenGenerationException: Token은 비워둘  없습니다 

6 Validators 나열하기

특정 객체의 모든 validator를 찾고 싶다면 validators를 사용할 수 있습니다.

예를 들어 커스텀 validator와 내장 validator를 사용하는 다음과 같은 모델이 있다면:

class Person < ApplicationRecord
  validates :name, presence: true, on: :create
  validates :email, format: URI::MailTo::EMAIL_REGEXP
  validates_with MyOtherValidator, strict: true
end

이제 모든 validator들을 나열하거나 validators_on을 사용하여 특정 필드를 확인하기 위해 "Person" 모델에서 validators를 사용할 수 있습니다.

irb> Person.validators
#=> [#<ActiveRecord::Validations::PresenceValidator:0x10b2f2158 
      @attributes=[:name], @options={:on=>:create}>,
     #<MyOtherValidatorValidator:0x10b2f17d0
      @attributes=[:name], @options={:strict=>true}>,
     #<ActiveModel::Validations::FormatValidator:0x10b2f0f10
      @attributes=[:email],
      @options={:with=>URI::MailTo::EMAIL_REGEXP}>]
     #<MyOtherValidator:0x10b2f0948 @options={:strict=>true}>]

irb> Person.validators_on(:name)
#=> [#<ActiveModel::Validations::PresenceValidator:0x10b2f2158
      @attributes=[:name], @options={on: :create}>]

7 Performing Custom Validations

내장된 validation들이 당신의 필요를 충족시키지 못할 때, 선호하는 방식으로 직접 validator나 validation 메서드를 작성할 수 있습니다.

7.1 Custom Validators

Custom validator는 ActiveModel::Validator를 상속하는 클래스입니다. 이러한 클래스들은 record를 인자로 받아 유효성 검사를 수행하는 validate 메서드를 구현해야 합니다. Custom validator는 validates_with 메서드를 사용하여 호출됩니다.

class MyValidator < ActiveModel::Validator 
  def validate(record)
    unless record.name.start_with? "X"
      record.errors.add :name, "X로 시작하는 이름을 입력해주세요!"
    end
  end
end

class Person < ApplicationRecord
  validates_with MyValidator
end

각 속성을 검증하는 커스텀 validator를 추가하는 가장 쉬운 방법은 편리한 ActiveModel::EachValidator를 사용하는 것입니다. 이 경우 커스텀 validator 클래스는 세 가지 인자를 받는 validate_each 메소드를 구현해야 합니다: record, attribute, value입니다. 이것들은 각각 인스턴스, 검증할 속성, 전달된 인스턴스의 속성 값에 대응됩니다.

class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless URI::MailTo::EMAIL_REGEXP.match?(value)
      record.errors.add attribute, (options[:message] || "이메일이 아닙니다")
    end
  end
end

class Person < ApplicationRecord
  validates :email, presence: true, email: true
end

예시에서 볼 수 있듯이, 표준 validation과 사용자 정의 validator를 함께 조합할 수 있습니다.

7.2 커스텀 메서드

모델의 상태를 검증하고 유효하지 않을 때 errors 컬렉션에 에러를 추가하는 메서드를 생성할 수 있습니다. 이때 [validate][] 클래스 메서드를 사용하여 validation 메서드 이름을 심볼로 전달하여 등록해야 합니다.

각 클래스 메서드에 대해 하나 이상의 심볼을 전달할 수 있으며, 해당 validation들은 등록된 순서대로 실행됩니다.

valid? 메서드는 errors 컬렉션이 비어있는지 확인하므로, validation이 실패하기를 원할 때는 커스텀 validation 메서드가 이 컬렉션에 에러를 추가해야 합니다:

class Invoice < ApplicationRecord
  validate :expiration_date_cannot_be_in_the_past,
    :discount_cannot_be_greater_than_total_value

  def expiration_date_cannot_be_in_the_past
    if expiration_date.present? && expiration_date < Date.today
      errors.add(:expiration_date, "과거의 날짜는 사용할 수 없습니다")
    end
  end

  def discount_cannot_be_greater_than_total_value
    if discount > total_value
      errors.add(:discount, "총액보다 클 수 없습니다")
    end
  end
end

기본적으로 이러한 validation은 valid?를 호출하거나 객체를 저장할 때마다 실행됩니다. 하지만 validate 메소드에 :on 옵션을 :create 또는 :update로 지정하여 이러한 custom validation이 실행되는 시점을 제어할 수도 있습니다.

class Invoice < ApplicationRecord
  validate :active_customer, on: :create

  def active_customer
    errors.add(:customer_id, "활성화되어 있지 않습니다") unless customer.active?
  end
end

:on에 대한 자세한 내용은 위 섹션을 참조하세요.

7.3 Custom Contexts

콜백을 위한 사용자 정의 유효성 검사 context를 정의할 수 있습니다. 이는 특정 시나리오에 기반하여 유효성 검사를 수행하거나 특정 콜백들을 함께 그룹화하여 특정 context에서 실행하고 싶을 때 유용합니다. custom context의 일반적인 시나리오는 multi-step form이 있고 각 단계별로 유효성 검사를 수행하고 싶을 때입니다.

예를 들어, form의 각 단계에 대한 custom context를 다음과 같이 정의할 수 있습니다:

class User < ApplicationRecord
  validate :personal_information, on: :personal_info
  validate :contact_information, on: :contact_info 
  validate :location_information, on: :location_info

  private
    def personal_information
      errors.add(:base, "이름을 입력해야 합니다") if first_name.blank?
      errors.add(:base, "나이는 18세 이상이어야 합니다") if age && age < 18
    end

    def contact_information  
      errors.add(:base, "이메일을 입력해야 합니다") if email.blank?
      errors.add(:base, "전화번호를 입력해야 합니다") if phone.blank?
    end

    def location_information
      errors.add(:base, "주소를 입력해야 합니다") if address.blank? 
      errors.add(:base, "도시를 입력해야 합니다") if city.blank?
    end
end

이러한 경우, callback을 건너뛰는 것을 고려할 수 있지만, custom context를 정의하는 것이 더 체계적인 접근 방식이 될 수 있습니다. callback에 대한 custom context를 정의하려면 context와 :on 옵션을 함께 사용해야 합니다.

custom context를 정의하고 나면, 이를 사용하여 validation을 실행할 수 있습니다:

irb> user = User.new(name: "John Doe", age: 17, email: "jane@example.com", phone: "1234567890", address: "123 Main St")
irb> user.valid?(:personal_info) # => false 
irb> user.valid?(:contact_info) # => true
irb> user.valid?(:location_info) # => false

커스텀 context를 사용하여 callback을 지원하는 모든 메서드에서 validation을 트리거할 수도 있습니다. 예를 들어 커스텀 context를 사용하여 save에서 validation을 트리거할 수 있습니다:

irb> user = User.new(name: "John Doe", age: 17, email: "jane@example.com", phone: "1234567890", address: "123 Main St")
irb> user.save(context: :personal_info) # => false 
irb> user.save(context: :contact_info) # => true
irb> user.save(context: :location_info) # => false

8 Validation Error 다루기

valid?invalid? 메서드는 유효성에 대한 요약 상태만을 제공합니다. 하지만 errors 컬렉션의 다양한 메서드를 사용하여 각각의 오류를 더 자세히 파악할 수 있습니다.

다음은 가장 일반적으로 사용되는 메서드들의 목록입니다. 사용 가능한 모든 메서드의 목록은 ActiveModel::Errors 문서를 참조하세요.

8.1 errors

errors 메서드는 각 에러의 다양한 세부 정보를 파악할 수 있는 시작점입니다.

이는 모든 에러를 포함하는 ActiveModel::Errors 클래스의 인스턴스를 반환하며, 각 에러는 ActiveModel::Error 객체로 표현됩니다.

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end
irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors.full_messages
=> ["이름을 입력해주세요", "이름이 너무 짧습니다 (최소 3자 이상)"]  

irb> person = Person.new(name: "John Doe")
irb> person.valid?
=> true
irb> person.errors.full_messages
=> []

irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors.first.details 
=> {:error=>:too_short, :count=>3}

8.2 errors[]

errors[]는 특정 attribute의 에러 메시지를 확인하고 싶을 때 사용됩니다. 이는 주어진 attribute에 대한 모든 에러 메시지를 포함하는 문자열 배열을 반환하며, 각 문자열은 하나의 에러 메시지를 담고 있습니다. 만약 해당 attribute와 관련된 에러가 없다면 빈 배열을 반환합니다.

이 메서드는 validation이 실행된 후에만 유용합니다. 왜냐하면 이는 단순히 errors 컬렉션만을 검사할 뿐 validation 자체를 실행하지는 않기 때문입니다. 이는 위에서 설명한 ActiveRecord::Base#invalid? 메서드와는 다른데, 객체 전체의 유효성을 검증하지 않기 때문입니다. errors[]는 객체의 개별 attribute에서 발견된 에러가 있는지만 확인합니다.

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 } 
end
irb> person = Person.new(name: "John Doe")
irb> person.valid? 
=> true
irb> person.errors[:name]
=> []

irb> person = Person.new(name: "JD")
irb> person.valid?
=> false 
irb> person.errors[:name]
=> ["너무 짧습니다 (최소 3자 이상이어야 합니다)"]

irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors[:name]
=> ["비어있을 수 없습니다", "너무 짧습니다 (최소 3자 이상이어야 합니다)"]

8.3 errors.where와 Error Object

때때로 에러의 메시지 외에도 각 에러에 대한 더 많은 정보가 필요할 수 있습니다. 각 에러는 ActiveModel::Error 객체로 캡슐화되어 있으며, where 메서드가 접근하는 가장 일반적인 방법입니다.

where는 다양한 조건으로 필터링된 error 객체들의 배열을 반환합니다.

다음과 같은 validation이 있다고 가정해봅시다:

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end

errors.where(:attr)에 첫번째 파라미터로 attribute를 전달하여 해당 속성만 필터링할 수 있습니다. 두번째 파라미터는 errors.where(:attr, :type)을 호출하여 우리가 원하는 error의 type을 필터링하는데 사용됩니다.

irb> person = Person.new
irb> person.valid?
=> false

irb> person.errors.where(:name)
=> [ ... ] # :name 속성의 모든 에러

irb> person.errors.where(:name, :too_short)
=> [ ... ] # :name 속성의 :too_short 에러

마지막으로, 주어진 error 객체 타입의 options에 있는 모든 조건으로 필터링할 수 있습니다.

irb> person = Person.new
irb> person.valid?
=> false

irb> person.errors.where(:name, :too_short, minimum: 3)
=> [ ... ] # name이 너무 짧고 최소 길이가 3인 모든 에러

이러한 error 객체들로부터 다양한 정보를 읽을 수 있습니다:

irb> error = person.errors.where(:name).last

irb> error.attribute
=> :name 
irb> error.type
=> :too_short
irb> error.options[:count]
=> 3

다음과 같이 error message를 생성할 수도 있습니다:

irb> error.message
=> "너무 짧습니다 (최소 3글자 이상이어야 합니다)"
irb> error.full_message 
=> "Name이 너무 짧습니다 (최소 3글자 이상이어야 합니다)"

full_message 메서드는 속성 이름을 대문자로 시작하여 앞에 붙인 더 사용자 친화적인 메시지를 생성합니다. (full_message가 사용하는 형식을 커스터마이즈하려면 I18n 가이드를 참조하세요.)

8.4 errors.add

[add][] 메서드는 attribute, 에러 type, 그리고 추가적인 options hash를 받아서 error 객체를 생성합니다. 이는 매우 구체적인 에러 상황을 정의할 수 있게 해주기 때문에 사용자 정의 validator를 작성할 때 유용합니다.

class Person < ApplicationRecord
  validate do |person|
    errors.add :name, :too_plain, message: "충분히 멋지지 않습니다"
  end
end
irb> person = Person.new
irb> person.errors.where(:name).first.type
=> :too_plain
irb> person.errors.where(:name).first.full_message
=> "이름이 충분히 멋지지 않습니다"

다음 Rails 가이드 문서를 한국어로 번역해주세요가 번역할 원본 문서가 보이지 않네요. 번역할 내용을 공유해주시면 도와드리도록 하겠습니다.

8.5 errors[:base]

특정 attribute와 관련된 것이 아니라 객체의 전반적인 상태와 관련된 error를 추가할 수 있습니다. 이를 위해서는 새로운 error를 추가할 때 attribute로 :base를 사용해야 합니다.

class Person < ApplicationRecord
  validate do |person|
    errors.add :base, :invalid, message: "유효하지 않은 사람입니다. 이유는..."
  end
end
irb> person = Person.new
irb> person.errors.where(:base).first.full_message
=> "이 사람은 다음 이유로 유효하지 않습니다 ..."

8.6 errors.size

size 메서드는 객체에 대한 전체 error 개수를 반환합니다.

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end
irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors.size
=> 2

irb> person = Person.new(name: "Andrea", email: "andrea@example.com") 
irb> person.valid?
=> true
irb> person.errors.size
=> 0

8.7 errors.clear

clear 메서드는 의도적으로 errors 컬렉션을 지우고 싶을 때 사용됩니다. 물론, 유효하지 않은 객체에서 errors.clear를 호출한다고 해서 실제로 그 객체가 유효해지는 것은 아닙니다. errors 컬렉션은 비워지겠지만, 다음번에 valid?를 호출하거나 이 객체를 데이터베이스에 저장하려고 시도하는 메서드를 호출하면 validation이 다시 실행됩니다. validation 중 하나라도 실패하면 errors 컬렉션은 다시 채워질 것입니다.

class Person < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3 }
end
irb> person = Person.new
irb> person.valid?
=> false
irb> person.errors.empty?
=> false

irb> person.errors.clear
irb> person.errors.empty? 
=> true

irb> person.save
=> false

irb> person.errors.empty?
=> false

9 Views에서 Validation 오류 표시하기

모델을 정의하고 validation을 추가한 후에는, 웹 폼을 통해 모델을 생성하는 동안 validation이 실패했을 때 오류 메시지를 표시하고 싶을 것입니다.

모든 애플리케이션이 validation 오류를 표시하는 방식이 다르기 때문에, Rails는 이러한 메시지를 생성하기 위한 view 헬퍼를 포함하지 않습니다. 하지만 Rails는 여러분이 직접 만들 수 있도록 validation과 상호 작용하는 많은 메서드를 제공합니다. 또한, scaffold를 생성할 때 Rails는 _form.html.erb에 해당 모델의 전체 오류 목록을 표시하는 ERB를 생성해 넣습니다.

@article이라는 인스턴스 변수에 저장된 모델이 있다고 가정하면, 다음과 같습니다:

<% if @article.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(@article.errors.count, "error") %> 때문에 이 article을 저장할 수 없습니다:</h2>

    <ul>
      <% @article.errors.each do |error| %>
        <li><%= error.full_message %></li>
      <% end %>
    </ul>
  </div>
<% end %>

또한 Rails form helper를 사용하여 form을 생성할 경우, field에서 validation 오류가 발생하면 해당 입력 필드 주변에 추가적인 <div>가 생성됩니다.

<div class="field_with_errors">
  <input id="article_title" name="article[title]" size="30" type="text" value="">
</div>

그런 다음 이 div를 원하는 대로 스타일링할 수 있습니다. 예를 들어 Rails가 생성하는 기본 scaffold는 다음과 같은 CSS 규칙을 추가합니다:

.field_with_errors {
  padding: 2px;
  background-color: red;
  display: table;
}

이는 오류가 있는 모든 field가 2픽셀 빨간색 테두리를 가지게 된다는 것을 의미합니다.

9.1 Error Field Wrapper 커스터마이징

Rails는 field_error_proc 설정 옵션을 사용하여 error가 있는 field를 HTML로 감쌉니다. 기본적으로 이 옵션은 error가 있는 form field를 위 예시에서 볼 수 있듯이 field_with_errors 클래스를 가진 <div>로 감쌉니다:

config.action_view.field_error_proc = Proc.new { |html_tag, instance| content_tag :div, html_tag, class: "field_with_errors" }

유효성 검사에 실패한 form inputs를 래핑하는데 사용되는 HTML generator입니다. field_with_errors div로 감싸는 것이 기본값입니다.

애플리케이션 설정의 field_error_proc 설정을 수정하여 이 동작을 커스터마이징할 수 있으며, 폼에서 에러가 표시되는 방식을 변경할 수 있습니다. 자세한 내용은 field_error_proc 설정 가이드를 참조하세요.



맨 위로