액티브 모델 기본 사항

이 가이드는 액티브 모델 사용을 시작하는 데 필요한 내용을 제공합니다. 액티브 모델은 액션 팩과 액션 뷰 헬퍼가 일반 루비 객체와 상호 작용할 수 있는 방법을 제공합니다. 또한 Rails 프레임워크 외부에서 사용할 수 있는 사용자 정의 ORM을 구축하는 데 도움이 됩니다.

이 가이드를 읽고 나면 다음을 알 수 있습니다:

  • 액티브 모델이 무엇이며 액티브 레코드와 어떤 관계가 있는지
  • 액티브 모델에 포함된 다양한 모듈
  • 클래스에서 액티브 모델을 사용하는 방법

액티브 모델이란 무엇인가?

액티브 모델을 이해하려면 액티브 레코드에 대해 약간 알아야 합니다. 액티브 레코드는 지속적인 저장소가 필요한 데이터를 가진 객체를 관계형 데이터베이스에 연결하는 ORM(Object Relational Mapper)입니다. 그러나 ORM 외에도 유효성 검사, 콜백, 번역, 사용자 정의 속성 생성 등의 기능이 있습니다.

이러한 기능 중 일부는 액티브 레코드에서 분리되어 액티브 모델을 형성했습니다. 액티브 모델은 데이터베이스 테이블에 연결되지 않은 모델 기능이 필요한 일반 루비 객체를 위한 라이브러리입니다.

요약하면, 액티브 레코드는 데이터베이스 테이블에 해당하는 모델을 정의하는 인터페이스를 제공하지만, 액티브 모델은 데이터베이스에 연결되지 않은 모델 기능이 필요한 루비 클래스를 구축하는 데 사용됩니다. 액티브 모델은 액티브 레코드와 독립적으로 사용할 수 있습니다.

아래에서 설명하는 모듈 중 일부는 다음과 같습니다.

API

ActiveModel::API액션 팩액션 뷰와 바로 작동할 수 있는 기능을 클래스에 추가합니다.

ActiveModel::API를 포함하면 기본적으로 다른 모듈이 포함되어 다음과 같은 기능을 사용할 수 있습니다:

다음은 ActiveModel::API를 포함하는 클래스의 예와 사용 방법입니다:

class EmailContact
  include ActiveModel::API

  attr_accessor :name, :email, :message
  validates :name, :email, :message, presence: true

  def deliver
    if valid?
      # 이메일 전송
    end
  end
end
irb> email_contact = EmailContact.new(name: "David", email: "david@example.com", message: "Hello World")

irb> email_contact.name # 속성 할당
=> "David"

irb> email_contact.to_model == email_contact # 변환
=> true

irb> email_contact.model_name.name # 명명
=> "EmailContact"

irb> EmailContact.human_attribute_name("name") # 번역 (로케일이 설정된 경우)
=> "Name"

irb> email_contact.valid? # 유효성 검사
=> true

irb> empty_contact = EmailContact.new
irb> empty_contact.valid?
=> false

ActiveModel::API를 포함하는 모든 클래스는 form_with, render 및 기타 액션 뷰 헬퍼 메서드와 함께 사용할 수 있습니다. 액티브 레코드 객체와 마찬가지로 사용할 수 있습니다.

예를 들어, form_with를 사용하여 EmailContact 객체에 대한 폼을 만들 수 있습니다:

<%= form_with model: EmailContact.new do |form| %>
  <%= form.text_field :name %>
<% end %>

이렇게 하면 다음과 같은 HTML이 생성됩니다:

<form action="/email_contacts" method="post">
  <input type="text" name="email_contact[name]" id="email_contact_name">
</form>

render를 사용하여 객체의 부분을 렌더링할 수 있습니다:

<%= render @email_contact %>

참고: form_withrenderActiveModel::API 호환 객체와 함께 사용하는 방법에 대해서는 액션 뷰 폼 헬퍼레이아웃과 렌더링 가이드에서 자세히 알아볼 수 있습니다.

모델

ActiveModel::Model 은 기본적으로 ActiveModel::API를 포함하여 액션 팩 및 액션 뷰와 상호 작용할 수 있게 해줍니다. 향후 더 많은 기능이 추가될 예정입니다.

class Person
  include ActiveModel::Model

  attr_accessor :name, :age
end
irb> person = Person.new(name: 'bob', age: '18')
irb> person.name # => "bob"
irb> person.age  # => "18"

속성

ActiveModel::Attributes 를 사용하면 일반 루비 객체에서 데이터 유형을 정의하고, 기본값을 설정하며, 캐스팅 및 직렬화를 처리할 수 있습니다. 이는 폼 데이터에 유용할 수 있으며 날짜와 부울과 같은 Active Record 유사한 변환을 일반 객체에 제공할 수 있습니다.

Attributes를 사용하려면 모델 클래스에 모듈을 포함하고 attribute 매크로를 사용하여 속성을 정의합니다. 이름, 캐스트 유형, 기본값 및 속성 유형에 지원되는 기타 옵션을 받습니다.

class Person
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :date_of_birth, :date
  attribute :active, :boolean, default: true
end
irb> person = Person.new

irb> person.name = "Jane"
irb> person.name
=> "Jane"

# 문자열을 속성에 의해 설정된 날짜로 캐스팅
irb> person.date_of_birth = "2020-01-01"
irb> person.date_of_birth
=> Wed, 01 Jan 2020
irb> person.date_of_birth.class
=> Date

# 속성에 의해 설정된 기본값 사용
irb> person.active
=> true

# 정수를 속성에 의해 설정된 부울로 캐스팅
irb> person.active = 0
irb> person.active
=> false

아래에 설명된 추가 메서드를 사용할 수 있습니다.

메서드: attribute_names

attribute_names 메서드는 속성 이름의 배열을 반환합니다.

irb> Person.attribute_names
=> ["name", "date_of_birth", "active"]

메서드: attributes

attributes 메서드는 모든 속성의 이름을 키로, 속성 값을 값으로 하는 해시를 반환합니다.

irb> person.attributes
=> {"name" => "Jane", "date_of_birth" => Wed, 01 Jan 2020, "active" => false}

속성 할당

ActiveModel::AttributeAssignment 를 사용하면 속성 이름과 일치하는 키가 있는 속성 해시를 전달하여 객체의 속성을 설정할 수 있습니다. 여러 속성을 한 번에 설정하려는 경우 유용합니다.

다음 클래스를 고려해 보세요:

class Person
  include ActiveModel::AttributeAssignment

  attr_accessor :name, :date_of_birth, :active
end
irb> person = Person.new

# 한 번에 여러 속성 설정
irb> person.assign_attributes(name: "John", date_of_birth: "1998-01-01", active: false)

irb> person.name
=> "John"
irb> person.date_of_birth
=> Thu, 01 Jan 1998
irb> person.active
=> false

전달된 해시가 permitted? 메서드에 응답하고 이 메서드의 반환 값이 false인 경우 ActiveModel::ForbiddenAttributesError 예외가 발생합니다.

참고: permitted?강력한 매개변수 통합에 사용되며, 요청에서 params 속성을 할당하고 있습니다.

irb> person = Person.new

# 요청에서 params와 유사한 속성 해시 만들기
irb> params = ActionController::Parameters.new(name: "John")
=> #<ActionController::Parameters {"name" => "John"} permitted: false>

irb> person.assign_attributes(params)
=> # ActiveModel::ForbiddenAttributesError 발생
irb> person.name
=> nil

# 허용할 속성을 허가
irb> permitted_params = params.permit(:name)
=> #<ActionController::Parameters {"name" => "John"} permitted: true>

irb> person.assign_attributes(permitted_params)
irb> person.name
=> "John"

메서드 별칭: attributes=

assign_attributes 메서드에는 attributes=라는 별칭이 있습니다.

정보: 메서드 별칭은 다른 이름으로 호출되지만 동일한 작업을 수행하는 메서드입니다. 가독성과 편의성을 위해 별칭이 존재합니다.

다음 예제는 attributes= 메서드를 사용하여 한 번에 여러 속성을 설정하는 방법을 보여줍니다:

irb> person = Person.new

irb> person.attributes = { name: "John", date_of_birth: "1998-01-01", active: false }

irb> person.name
=> "John"
irb> person.date_of_birth
=> "1998-01-01"

정보: assign_attributesattributes=는 모두 메서드 호출이며 할당할 속성 해시를 인수로 받습니다. 많은 경우 Ruby에서는 메서드 호출의 괄호 ()와 해시 정의의 중괄호 {}를 생략할 수 있습니다.

attributes=와 같은 “setter” 메서드는 일반적으로 괄호 ()를 생략하지만, 동일하게 작동하며 해시에는 항상 {}를 포함해야 합니다. person.attributes=({ name: "John" })은 괜찮지만 person.attributes = name: "John"SyntaxError를 발생시킵니다.

assign_attributes와 같은 다른 메서드 호출에서는 괄호 ()와 해시 인수의 중괄호 {}를 모두 포함할 수 있습니다. 예를 들어, assign_attributes name: "John"assign_attributes({ name: "John" })은 모두 유효한 Ruby 코드이지만, assign_attributes { name: "John" }은 Ruby가 해시 인수와 블록을 구분할 수 없어 SyntaxError를 발생시킵니다.

속성 메서드

ActiveModel::AttributeMethods 는 모델의 속성에 대해 동적으로 메서드를 정의하는 방법을 제공합니다. 이 모듈은 특히 속성 액세스 및 조작을 단순화하는 데 유용하며, 클래스의 메서드에 사용자 정의 접두사 및 접미사를 추가할 수 있습니다. 다음과 같이 접두사와 접미사, 그리네, 계속해서 ActiveModel::AttributeMethods에 대해 설명드리겠습니다.

ActiveModel::AttributeMethods 는 모델의 속성에 대해 동적으로 메서드를 정의하는 방법을 제공합니다. 이 모듈은 특히 속성 액세스 및 조작을 단순화하는 데 유용하며, 클래스의 메서드에 사용자 정의 접두사 및 접미사를 추가할 수 있습니다. 다음과 같이 접두사와 접미사, 그리고 어떤 메서드에 적용할지 정의할 수 있습니다:

  1. 클래스에 ActiveModel::AttributeMethods를 포함합니다.
  2. attribute_method_suffix, attribute_method_prefix, attribute_method_affix와 같은 메서드를 호출하여 접두사와 접미사를 정의합니다.
  3. 다른 메서드 호출 후 define_attribute_methods를 호출하여 접두사와 접미사가 적용될 속성을 선언합니다.
  4. 선언한 다양한 _attribute 메서드를 정의합니다. 이 메서드의 attribute 매개변수는 define_attribute_methods에 전달된 인수로 대체됩니다. 아래 예에서는 name입니다.

참고: attribute_method_prefixattribute_method_suffix는 메서드를 생성하는 데 사용할 접두사와 접미사를 정의하는 데 사용됩니다. attribute_method_affix는 접두사와 접미사를 동시에 정의하는 데 사용됩니다.

class Person
  include ActiveModel::AttributeMethods

  attribute_method_affix prefix: "reset_", suffix: "_to_default!"
  attribute_method_prefix "first_", "last_"
  attribute_method_suffix "_short?"

  define_attribute_methods "name"

  attr_accessor :name

  private
    # 'first_name' 메서드 호출
    def first_attribute(attribute)
      public_send(attribute).split.first
    end

    # 'last_name' 메서드 호출
    def last_attribute(attribute)
      public_send(attribute).split.last
    end

    # 'name_short?' 메서드 호출
    def attribute_short?(attribute)
      public_send(attribute).length < 5
    end

    # 'reset_name_to_default!' 메서드 호출
    def reset_attribute_to_default!(attribute)
      public_send("#{attribute}=", "Default Name")
    end
end
irb> person = Person.new
irb> person.name = "Jane Doe"

irb> person.first_name
=> "Jane"
irb> person.last_name
=> "Doe"

irb> person.name_short?
=> false

irb> person.reset_name_to_default!
=> "Default Name"

정의되지 않은 메서드를 호출하면 NoMethodError 오류가 발생합니다.

메서드: alias_attribute

ActiveModel::AttributeMethodsalias_attribute를 사용하여 속성 메서드를 별칭할 수 있습니다.

다음 예에서는 name 속성에 대한 별칭 속성 full_name을 만듭니다. 두 속성은 동일한 값을 반환하지만 full_name 별칭은 속성에 이름과 성이 포함되어 있음을 더 잘 반영합니다.

class Person
  include ActiveModel::AttributeMethods

  attribute_method_suffix "_short?"
  define_attribute_methods :name

  attr_accessor :name

  alias_attribute :full_name, :name

  private
    def attribute_short?(attribute)
      public_send(attribute).length < 5
    end
end
irb> person = Person.new
irb> person.name = "Joe Doe"
irb> person.name
=> "Joe Doe"

# `full_name`은 `name`의 별칭이며 동일한 값을 반환합니다.
irb> person.full_name
=> "Joe Doe"
irb> person.name_short?
=> false

# `full_name_short?`는 `name_short?`의 별칭이며 동일한 값을 반환합니다.
irb> person.full_name_short?
=> false

콜백

ActiveModel::Callbacks 는 일반 루비 객체에 액티브 레코드 스타일 콜백을 제공합니다. 이 콜백을 통해 before_updateafter_create와 같은 모델 수명 주기 이벤트에 연결하고 모델 수명 주기의 특정 시점에 실행되는 사용자 정의 로직을 정의할 수 있습니다.

다음 단계를 따라 ActiveModel::Callbacks를 구현할 수 있습니다:

  1. 클래스 내에서 ActiveModel::Callbacks를 확장합니다.
  2. define_model_callbacks를 사용하여 콜백이 연결될 메서드 목록을 설정합니다. :update와 같은 메서드를 지정하면 :update 이벤트에 대한 기본 콜백(:before, :around, :after)이 자동으로 포함됩니다.
  3. 정의된 메서드 내에서 run_callbacks를 사용하여 특정 이벤트가 트리거될 때 콜백 체인을 실행합니다.
  4. 클래스 내에서 before_update, after_update, around_update 메서드를 사용할 수 있습니다. 이는 액티브 레코드 모델에서 사용하는 것과 동일한 방식입니다.
class Person
  extend ActiveModel::Callbacks

  define_model_callbacks :update

  before_update :reset_me
  after_update :finalize_me
  around_update :log_me

  # `define_model_callbacks` 메서드에 포함된 `run_callbacks`가 주어진 이벤트에 대한 콜백을 실행합니다.
  def update
    run_callbacks(:update) do
      puts "update method called"
    end
  end

  private
    # update가 객체에서 호출되면 `before_update` 콜백에 의해 이 메서드가 호출됩니다.
    def reset_me
      puts "reset_me method: called before the update method"
    end

    # update가 객체에서 호출되면 `after_update` 콜백에 의해 이 메서드가 호출됩니다.
    def finalize_me
      puts "finalize_me method: called after the update method"
    end

    # update가 객체에서 호출되면 `around_update` 콜백에 의해 이 메서드가 호출됩니다.
    def log_me
      puts "log_me method: called around the update method"
      yield
      puts "log_me method: block successfully called"
    end

위의 클래스는 다음과 같은 출력을 생성하여 콜백이 호출되는 순서를 보여줍니다:

irb> person = Person.new
irb> person.update
reset_me method: called before the update method
log_me method: called around the update method
update method called
log_me method: block successfully called
finalize_me method: called after the update method
=> nil

위의 예와 같이 ‘around’ 콜백을 정의할 때는 yield를 해야 블록이 실행됩니다.

참고: define_model_callbacks에 전달된 method_name!, ? 또는 =로 끝나면 안 됩니다. 또한 동일한 콜백을 여러 번 정의하면 이전 콜백 정의가 덮어쓰여집니다.

특정 콜백 정의

define_model_callbacks 메서드에 only 옵션을 전달하여 특정 콜백만 생성할 수 있습니다:

define_model_callbacks :update, :create, only: [:after, :before]

이렇게 하면 before_create / after_createbefore_update / after_update 콜백만 생성되고 around_* 콜백은 생성되지 않습니다. 이 옵션은 해당 호출에 정의된 모든 콜백에 적용됩니다. define_model_callbacks를 여러 번 호출하여 다른 수명 주기 이벤트에 대한 콜백을 지정할 수 있습니다:

define_model_callbacks :create, only: :after
define_model_callbacks :update, only: :before
define_model_callbacks :destroy, only: :around

이렇게 하면 after_create, before_updatearound_destroy 메서드만 생성됩니다.

클래스를 사용하여 콜백 정의

before_<type>, after_<type>around_<type>에 클래스를 전달하여 콜백이 트리거되는 시기와 컨텍스트를 더 잘 제어할 수 있습니다. 콜백은 해당 클래스의 <action>_<type> 메서드를 호출하고 클래스의 인스턴스를 인수로 전달합니다.

class Person
  extend ActiveModel::Callbacks

  define_model_callbacks :create
  before_create PersonCallbacks
end

class PersonCallbacks
  def self.before_create(obj)
    # `obj`는 콜백이 호출되는 Person 인스턴스입니다.
  end
end

콜백 중단

언제든지 :abort를 던져 콜백 체인을 중단할 수 있습니다. 이는 액티브 레코드 콜백에서 작동하는 방식과 유사합니다.

다음 예에서는 reset_me 메서드에서 :abort를 던지므로 before_update 콜백을 포함한 나머지 콜백 체인이 중단되고 update 메서드의 본문이 실행되지 않습니다.

class Person
  extend ActiveModel::Callbacks

  define_model_callbacks :update

  before_update :reset_me
  after_update :finalize_me
  around_update :log_me

  def update
    run_callbacks(:update) do
      puts "update method called"
    end
  end

  private
    def reset_me
      puts "reset_me method: called before the update method"
      throw :abort
      puts "reset_me method: some code after abort"
    end

    def finalize_me
      puts "finalize_me method: called after the update method"
    end

    def log_me
      puts "log_me method: called around the update method"
      yield
      puts "log_me method: block successfully called"
    end
end
irb> person = Person.new

irb> person.update
reset_me method: called before the update method
=> false

변환

ActiveModel::Conversion 은 객체를 다양한 형태로 변환할 수 있는 메서드의 모음입니다. URL, 폼 필드 등을 구축하는 데 일반적으로 사용됩니다.

ActiveModel::Conversion 모듈은 다음 메서드를 클래스에 추가합니다: to_model, to_key, to_param, to_partial_path.

메서드의 반환 값은 persisted?가 정의되어 있는지 여부와 id가 제공되는지 여부에 따라 달라집니다. persisted? 메서드는 객체가 데이터베이스 또는 저장소에 저장되었으면 true를 반환하고, 그렇지 않으면 false를 반환해야 합니다. id는 객체의 ID를 참조해야 하며, 객체가 저장되지 않은 경우 nil이어야 합니다.

class Person
  include ActiveModel::Conversion
  attr_accessor :id

  def initialize(id)
    @id = id
  end

  def persisted?
    id.present?
  end
end

to_model

to_model 메서드는 객체 자체를 반환합니다.

irb> person = Person.new(1)
irb> person.to_model == person
=> true

모델이 액티브 모델 객체처럼 작동하지 않는 경우 직접 :to_model을 정의하여 액티브 모델 호환 메서드가 있는 프록시 객체를 반환할 수 있습니다.

class Person
  def to_model
    # 액티브 모델 호환 메서드가 있는 프록시 객체.
    PersonModel.new(self)
  end
end

to_key

to_key 메서드는 객체의 키 속성(있는 경우)을 배열로 반환합니다. 키 속성이 없는 경우 nil을 반환합니다.

irb> person.to_key
=> [1]

참고: 키 속성은 객체를 식별하는 데 사용되는 속성입니다. 예를 들어, 데이터베이스 지원 모네, 계속해서 ActiveModel::Conversion에 대해 설명드리겠습니다.

to_param

to_param 메서드는 URL에 사용할 수 있는 객체의 키에 대한 문자열 표현을 반환합니다. persisted?false인 경우 nil을 반환합니다.

irb> person.to_param
=> "1"

topartialpath

to_partial_path 메서드는 객체와 연관된 경로를 나타내는 문자열을 반환합니다. 액션 팩은 이를 사용하여 객체를 나타내는 적절한 부분을 찾습니다.

irb> person.to_partial_path
=> "people/person"

Dirty

ActiveModel::Dirty 는 모델 속성이 저장되기 전에 변경되었는지 추적하는 데 유용합니다. 이 기능을 통해 어떤 속성이 수정되었는지, 이전 값과 현재 값이 무엇인지 확인하고 이러한 변경 사항에 따라 작업을 수행할 수 있습니다. 감사, 유효성 검사, 조건부 로직에 특히 유용합니다. 액티브 레코드와 동일한 방식으로 객체의 변경 사항을 추적할 수 있습니다.

객체가 더티(dirty)가 되려면 속성이 하나 이상 변경되었지만 아직 저장되지 않아야 합니다. 속성 기반 접근자 메서드가 있습니다.

ActiveModel::Dirty를 사용하려면 다음을 수행해야 합니다:

  1. 클래스에 모듈을 포함합니다.
  2. define_attribute_methods를 사용하여 변경 사항을 추적할 속성 메서드를 정의합니다.
  3. 추적된 속성을 변경하기 전에 [attr_name]_will_change!를 호출합니다.
  4. 변경 사항이 영구화된 후 changes_applied를 호출합니다.
  5. 변경 사항 정보를 재설정하려면 clear_changes_information를 호출합니다.
  6. 이전 데이터를 복원하려면 restore_attributes를 호출합니다.

그런 다음 ActiveModel::Dirty가 제공하는 메서드를 사용하여 객체의 변경된 모든 속성 목록, 변경된 속성의 원래 값, 속성에 대한 변경 사항을 쿼리할 수 있습니다.

first_namelast_name 속성이 있는 Person 클래스를 고려하고 ActiveModel::Dirty를 사용하여 이러한 속성의 변경 사항을 추적하는 방법을 살펴보겠습니다.

class Person
  include ActiveModel::Dirty

  attr_reader :first_name, :last_name
  define_attribute_methods :first_name, :last_name

  def initialize
    @first_name = nil
    @last_name = nil
  end

  def first_name=(value)
    first_name_will_change! unless value == @first_name
    @first_name = value
  end

  def last_name=(value)
    last_name_will_change! unless value == @last_name
    @last_name = value
  end

  def save
    # 데이터 영구화 - 더티 데이터 지우고 `changes`를 `previous_changes`로 이동합니다.
    changes_applied
  end

  def reload!
    # 모든 더티 데이터(현재 변경 사항 및 이전 변경 사항) 지우기
    clear_changes_information
  end

  def rollback!
    # 제공된 속성의 이전 데이터 복원
    restore_attributes
  end
end

객체 직접 쿼리하여 변경된 모든 속성 목록 얻기

irb> person = Person.new

# 새로 생성된 `Person` 객체는 변경되지 않음:
irb> person.changed?
=> false

irb> person.first_name = "Jane Doe"
irb> person.first_name
=> "Jane Doe"

changed?는 저장되지 않은 변경 사항이 있는 경우 true를, 그렇지 않으면 false를 반환합니다.

irb> person.changed?
=> true

changed는 저장되지 않은 변경 사항이 있는 속성 이름의 배열을 반환합니다.

irb> person.changed
=> ["first_name"]

changed_attributes는 저장되지 않은 변경 사항이 있는 속성과 해당 원래 값을 나타내는 해시를 반환합니다.

irb> person.changed_attributes
=> {"first_name" => nil}

changes는 변경된 속성 이름을 키로, 원래 값과 새 값을 포함하는 배열을 값으로 하는 해시를 반환합니다.

irb> person.changes
=> {"first_name" => [nil, "Jane Doe"]}

previous_changes는 모델이 저장되기 전(즉, changes_applied가 호출되기 전)에 변경된 속성에 대한 해시를 반환합니다.

irb> person.previous_changes
=> {}

irb> person.save
irb> person.previous_changes
=> {"first_name" => [nil, "Jane Doe"]}

속성 기반 접근자 메서드

irb> person = Person.new

irb> person.changed?
=> false

irb> person.first_name = "John Doe"
irb> person.first_name
=> "John Doe"

[attr_name]_changed?는 특정 속성이 변경되었는지 확인합니다.

irb> person.first_name_changed?
=> true

[attr_name]_was는 속성의 이전 값을 추적합니다.

irb> person.first_name_was
=> nil

[attr_name]_change는 변경된 속성의 이전 값과 현재 값을 추적합니다. 변경된 경우 [원래 값, 새 값] 배열을 반환하고, 그렇지 않으면 nil을 반환합니다.

irb> person.first_name_change
=> [nil, "John Doe"]
irb> person.last_name_change
=> nil

[attr_name]_previously_changed?는 모델이 저장되기 전(즉, changes_applied가 호출되기 전)에 특정 속성이 변경되었는지 확인합니다.

irb> person.first_name_previously_changed?
=> false
irb> person.save
irb> person.first_name_previously_changed?
=> true

[attr_name]_previous_change는 모델이 저장되기 전(즉, changes_applied가 호출되기 전)에 변경된 속성의 이전 값과 현재 값을 추적합니다. 변경된 경우 [원래 값, 새 값] 배열을 반환하고, 그렇지 않으면 nil을 반환합니다.

irb> person.first_name_previous_change
=> [nil, "John Doe"]

명명

ActiveModel::Naming 은 명명 및 라우팅을 더 쉽게 관리할 수 있도록 클래스 메서드와 헬퍼 메서드를 추가합니다. 이 모듈은 model_name 클래스 메서드를 정의하며, 이 메서드는 일부 ActiveSupport::Inflector 메서드를 사용하여 여러 접근자를 정의합니다.

class Person
  extend ActiveModel::Naming
end

name은 모델의 이름을 반환합니다.

irb> Person.model_name.name
=> "Person"

singular는 레코드 또는 클래스의 단수 클래스 이름을 반환합니다.

irb> Person.model_name.singular
=> "person"

plural는 레코드 또는 클래스의 복수 클래스 이름을 반환합니다.

irb> Person.model_name.plural
=> "people"

element는 네임스페이스를 제거하고 단수 snake_cased 이름을 반환합니다. 일반적으로 Action Pack 및/또는 Action View 헬퍼가 부분/폼의 이름을 지정하는 데 사용됩니다.

irb> Person.model_name.element
=> "person"

human은 I18n을 사용하여 모델 이름을 더 인간 친화적인 형식으로 변환합니다. 기본적으로 클래스 이름을 언더스코어하고 인간화합니다.

irb> Person.model_name.human
=> "Person"

collection은 네임스페이스를 제거하고 복수 snake_cased 이름을 반환합니다. 일반적으로 Action Pack 및/또는 Action View 헬퍼가 부분/폼의 이름을 지정하는 데 사용됩니다.

irb> Person.model_name.collection
=> "people"

param_key는 매개변수 이름에 사용할 문자열을 반환합니다.

irb> Person.model_name.param_key
=> "person"

i18n_key는 i18n 키의 이름을 반환합니다. 모델 이름을 언더스코어하고 기호로 반환합니다.

irb> Person.model_name.i18n_key
=> :person

route_key는 경로 이름 생성 시 사용할 문자열을 반환합니다.

irb> Person.model_name.route_key
=> "people"

singular_route_key는 경로 이름 생성 시 사용할 문자열을 반환합니다.

irb> Person.model_name.singular_route_key
=> "person"

uncountable?는 레코드 또는 클래스의 클래스 이름이 셀 수 없는지 식별합니다.

irb> Person.model_name.uncountable?
=> false

참고: param_key, route_keysingular_route_key와 같은 일부 Naming 메서드는 Engine에 격리되어 있는지 여부에 따라 네임스페이스가 지정된 모델에 대해 다릅니다.

모델 이름 사용자 정의

때로는 폼 헬퍼와 URL 생성에 사용되는 모델 이름을 사용자 정의하고 싶을 수 있습니다. 이는 모델의 전체 네임스페이스를 참조할 수 있지만 사용자에게 더 친숙한 이름을 사용하려는 경우에 유용할 수 있습니다.

예를 들어, Rails 애플리케이션에 Person 네임스페이스가 있고 새 Person::Profile에 대한 폼을 만들고 싶다고 가정해 보겠습니다.

기본적으로 Rails는 /person/profiles와 같은 URL을 생성하여 person 네임스페이스를 포함합니다. 그러나 URL이 단순히 profiles를 가리키도록 하려면 model_name 메서드를 다음과 같이 사용자 정의할 수 있습니다:

module Person
  class Profile
    include ActiveModel::Model

    def self.model_name
      ActiveModel::Name.new(self, nil, "Profile")
    end
  end
end

이 설정을 사용하면 form_with 헬퍼를 사용하여 새 Person::Profile을 만드는 폼을 만들 때 Rails는 /profiles URL을 생성합니다. 이는 model_name 메서드가 Profile을 반환하도록 재정의되었기 때문입니다.

또한 경로 헬퍼가 네임스페이스 없이 생성되므로 person_profiles_path 대신 profiles_path를 사용하여 profiles 리소스의 URL을 생성할 수 있습니다. profiles_path 헬퍼를 사용하려면 config/routes.rb 파일에서 Person::Profile 모델에 대한 경로를 다음과 같이 정의해야 합니다:

Rails.application.routes.draw do
  resources :profiles
end

따라서 모델은 이전 섹션에서 설명네, 계속해서 ActiveModel::Naming에 대해 설명드리겠습니다.

따라서 모델은 이전 섹션에서 설명한 메서드에 대해 다음과 같은 값을 반환할 것으로 예상할 수 있습니다:

irb> name = ActiveModel::Name.new(Person::Profile, nil, "Profile")
=> #<ActiveModel::Name:0x000000014c5dbae0

irb> name.singular
=> "profile"
irb> name.singular_route_key
=> "profile"
irb> name.route_key
=> "profiles"

SecurePassword

ActiveModel::SecurePassword 는 암호화된 형태로 암호를 안전하게 저장할 수 있는 방법을 제공합니다. 이 모듈을 포함하면 기본적으로 특정 유효성 검사가 포함된 has_secure_password 클래스 메서드가 제공됩니다.

ActiveModel::SecurePasswordbcrypt에 의존하므로 이 gem을 Gemfile에 포함해야 합니다.

gem "bcrypt"

ActiveModel::SecurePassword에는 password_digest 속성이 필요합니다.

다음과 같은 유효성 검사가 자동으로 추가됩니다:

  1. 생성 시 비밀번호가 필수입니다.
  2. 비밀번호 확인(password_confirmation 속성 사용).
  3. 비밀번호의 최대 길이는 72바이트입니다(bcrypt가 이 크기로 암호화하기 때문).

참고: 비밀번호 확인 유효성 검사가 필요하지 않은 경우 password_confirmation 값을 제공하지 않으면 됩니다(즉, 폼 필드를 제공하지 않습니다). 이 속성이 nil 값을 가지면 유효성 검사가 트리거되지 않습니다.

추가 사용자 정의를 위해 validations: false를 인수로 전달하여 기본 유효성 검사를 억제할 수 있습니다.

class Person
  include ActiveModel::SecurePassword

  has_secure_password
  has_secure_password :recovery_password, validations: false

  attr_accessor :password_digest, :recovery_password_digest
end
irb> person = Person.new

# 비밀번호가 비어 있을 때
irb> person.valid?
=> false

# 확인이 비밀번호와 일치하지 않을 때
irb> person.password = "aditya"
irb> person.password_confirmation = "nomatch"
irb> person.valid?
=> false

# 비밀번호 길이가 72를 초과할 때
irb> person.password = person.password_confirmation = "a" * 100
irb> person.valid?
=> false

# 비밀번호만 제공되고 password_confirmation은 없을 때
irb> person.password = "aditya"
irb> person.valid?
=> true

# 모든 유효성 검사를 통과할 때
irb> person.password = person.password_confirmation = "aditya"
irb> person.valid?
=> true

irb> person.recovery_password = "42password"

# `authenticate`는 `authenticate_password`의 별칭
irb> person.authenticate("aditya")
=> #<Person> # == person
irb> person.authenticate("notright")
=> false
irb> person.authenticate_password("aditya")
=> #<Person> # == person
irb> person.authenticate_password("notright")
=> false

irb> person.authenticate_recovery_password("aditya")
=> false
irb> person.authenticate_recovery_password("42password")
=> #<Person> # == person
irb> person.authenticate_recovery_password("notright")
=> false

irb> person.password_digest
=> "$2a$04$gF8RfZdoXHvyTjHhiU4ZsO.kQqV9oonYZu31PRE4hLQn3xM2qkpIy"
irb> person.recovery_password_digest
=> "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"

직렬화

ActiveModel::Serialization 은 객체에 대한 기본 직렬화를 제공합니다. 직렬화할 속성을 포함하는 속성 해시를 선언해야 합니다. 속성은 기호가 아닌 문자열이어야 합니다.

class Person
  include ActiveModel::Serialization

  attr_accessor :name, :age

  def attributes
    # 직렬화할 속성 선언
    { "name" => nil, "age" => nil }
  end

  def capitalized_name
    # 선언된 메서드는 직렬화된 해시에 포함될 수 있습니다.
    name&.capitalize
  end
end

이제 serializable_hash 메서드를 사용하여 객체의 직렬화된 해시에 액세스할 수 있습니다. serializable_hash에 대한 유효한 옵션에는 :only, :except, :methods:include가 포함됩니다.

irb> person = Person.new

irb> person.serializable_hash
=> {"name" => nil, "age" => nil}

# 이름과 나이 속성을 설정하고 객체 직렬화
irb> person.name = "bob"
irb> person.age = 22
irb> person.serializable_hash
=> {"name" => "bob", "age" => 22}

# methods 옵션을 사용하여 capitalized_name 메서드 포함
irb>  person.serializable_hash(methods: :capitalized_name)
=> {"name" => "bob", "age" => 22, "capitalized_name" => "Bob"}

# only 옵션을 사용하여 name 속성만 포함
irb> person.serializable_hash(only: :name)
=> {"name" => "bob"}

# except 옵션을 사용하여 name 속성 제외
irb> person.serializable_hash(except: :name)
=> {"age" => 22}

includes 옵션을 사용하는 예제에는 약간 더 복잡한 시나리오가 필요합니다:

  class Person
    include ActiveModel::Serialization
    attr_accessor :name, :notes # has_many :notes 모방

    def attributes
      { "name" => nil }
    end
  end

  class Note
    include ActiveModel::Serialization
    attr_accessor :title, :text

    def attributes
      { "title" => nil, "text" => nil }
    end
  end
irb> note = Note.new
irb> note.title = "Weekend Plans"
irb> note.text = "Some text here"

irb> person = Person.new
irb> person.name = "Napoleon"
irb> person.notes = [note]

irb> person.serializable_hash
=> {"name" => "Napoleon"}

irb> person.serializable_hash(include: { notes: { only: "title" }})
=> {"name" => "Napoleon", "notes" => [{"title" => "Weekend Plans"}]}

ActiveModel::Serializers::JSON

액티브 모델은 또한 ActiveModel::Serializers::JSON 모듈을 제공하여 JSON 직렬화/역직렬화를 수행할 수 있습니다.

JSON 직렬화를 사용하려면 ActiveModel::Serialization에서 ActiveModel::Serializers::JSON으로 포함하는 모듈을 변경하세요. 전자가 후자를 이미 포함하므로 명시적으로 포함할 필요가 없습니다.

class Person
  include ActiveModel::Serializers::JSON

  attr_accessor :name

  def attributes
    { "name" => nil }
  end
end

as_json 메서드는 serializable_hash와 유사하게 키가 문자열인 모델을 나타내는 해시를 제공합니다. to_json 메서드는 모델을 나타내는 JSON 문자열을 반환합니다.

irb> person = Person.new

# 키가 문자열인 모델을 나타내는 해시
irb> person.as_json
=> {"name" => nil}

# 모델을 나타내는 JSON 문자열
irb> person.to_json
=> "{\"name\":null}"

irb> person.name = "Bob"
irb> person.as_json
=> {"name" => "Bob"}

irb> person.to_json
=> "{\"name\":\"Bob\"}"

JSON 문자열에서 모델 속성을 정의할 수도 있습니다. 이를 위해 먼저 클래스에 attributes= 메서드를 정의해야 합니다:

class Person
  include ActiveModel::Serializers::JSON

  attr_accessor :name

  def attributes=(hash)
    hash.each do |key, value|
      public_send("#{key}=", value)
    end
  end

  def attributes
    { "name" => nil }
  end
end

이제 from_json을 사용하여 Person 인스턴스를 만들고 속성을 설정할 수 있습니다.

irb> json = { name: "Bob" }.to_json
=> "{\"name\":\"Bob\"}"

irb> person = Person.new

irb> person.from_json(json)
=> #<Person:0x00000100c773f0 @name="Bob">

irb> person.name
=> "Bob"

번역

ActiveModel::Translation 은 객체와 Rails 국제화(i18n) 프레임워크 간의 통합을 제공합니다.

class Person
  extend ActiveModel::Translation
end

human_attribute_name 메서드를 사용하면 속성 이름을 더 인간 친화적인 형식으로 변환할 수 있습니다. 인간 친화적인 형식은 로케일 파일에 정의됩니다.

# config/locales/app.pt-BR.yml
pt-BR:
  activemodel:
    attributes:
      person:
        name: "Nome"
irb> Person.human_attribute_name("name")
=> "Name"

irb> I18n.locale = :"pt-BR"
=> :"pt-BR"
irb> Person.human_attribute_name("name")
=> "Nome"

유효성 검사

ActiveModel::Validations 는 객체에 대한 유효성 검사 기능을 추가하며, 애플리케이션 내에서 데이터 무결성과 일관성을 보장하는 데 중요합니다. 모델에 유효성 검사를 포함시키면 속성 값의 정확성을 규제하는 규칙을 정의하고 잘못된 데이터를 방지할 수 있습니다.

class Person
  include ActiveModel::Validations

  attr_accessor :name, :email, :token

  validates :name, presence: true
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates! :token, presence: true
end
irb> person = Person.new
irb> person.token = "2b1f325"
irb> person.valid?
=> false

irb> person.name = "Jane Doe"
irb> person.email = "me"
irb> person.valid?
=> false

irb> person.email = "jane.doe@gmail.com"
irb> person.valid?
=> true

# `token`은 validate!를 사용하므로 설정되지 않으면 예외가 발생합니다.
irb> person.token = nil
irb> person.valid?
=> "Token can't be blank (ActiveModel::StrictValidationFailed)"

유효성 검사 메서드 및 옵션

다음과 같은 메서드를 사용하여 유효성 검사를 추가할 수 있습니다:

  • validate: 클래스에 메서드 또는 블록을 통해 유효성 검사를 추가합니다.

  • validates: 속성을 validates 메서드에 전달하면 기본 유효성 검사기에 대한 단축키가 제공됩니다.

  • validates! 또는 strict: true 설정: 최종 사용자가 수정할 수 없고 예외적으로 간주되는 유효성 검사를 정의하는 데 사용됩니다. 느낌표 또는 :strict 옵션이 true로 설정된 각 유효성 검사기는 유효성 검사에 실패할 때 ActiveModel::StrictValidationFailed를 발생시킵니다.

  • validates_with: 레코드를 지정된 네, 계속해서 ActiveModel::Validations에 대해 설명드리겠습니다.

  • validates_with: 레코드를 지정된 클래스 또는 클래스로 전달하여 더 복잡한 조건에 따라 오류를 추가할 수 있습니다.

  • validates_each: 블록에 대해 각 속성을 검증합니다.

특정 유효성 검사기에 사용할 수 있는 옵션은 다음과 같습니다. 특정 유효성 검사기에서 옵션을 사용할 수 있는지 여부는 여기의 문서를 참조하세요.

  • :on: 유효성 검사를 추가할 컨텍스트를 지정합니다. 기호 또는 기호 배열을 전달할 수 있습니다. (예: on: :create 또는 on: :custom_validation_context 또는 on: [:create, :custom_validation_context]). :on 옵션이 없는 유효성 검사는 컨텍스트에 관계없이 실행됩니다. :on 옵션이 있는 유효성 검사는 지정된 컨텍스트에서만 실행됩니다. valid?(:context)를 통해 컨텍스트를 전달하면서 유효성 검사를 수행할 수 있습니다.

  • :if: 유효성 검사를 수행할지 여부를 결정하기 위해 호출할 메서드, 프로시저 또는 문자열을 지정합니다(예: if: :allow_validation 또는 if: -> { signup_step > 2 }). 메서드, 프로시저 또는 문자열은 true 또는 false 값을 반환하거나 평가해야 합니다.

  • :unless: 유효성 검사를 수행하지 않을지 여부를 결정하기 위해 호출할 메서드, 프로시저 또는 문자열을 지정합니다(예: unless: :skip_validation 또는 unless: Proc.new { |user| user.signup_step <= 2 }). 메서드, 프로시저 또는 문자열은 true 또는 false 값을 반환하거나 평가해야 합니다.

  • :allow_nil: 속성이 nil인 경우 유효성 검사를 건너뜁니다.

  • :allow_blank: 속성이 비어 있는 경우 유효성 검사를 건너뜁니다.

  • :strict: :strict 옵션이 true로 설정되면 오류 대신 ActiveModel::StrictValidationFailed를 발생시킵니다. :strict 옵션은 다른 예외로도 설정할 수 있습니다.

참고: 동일한 메서드에 대해 validate를 여러 번 호출하면 이전 정의가 덮어쓰여집니다.

오류

ActiveModel::Validations는 자동으로 새 ActiveModel::Errors 객체로 초기화된 errors 메서드를 인스턴스에 추가하므로 수동으로 수행할 필요가 없습니다.

valid?를 객체에서 실행하여 객체가 유효한지 확인합니다. 객체가 유효하지 않으면 false를 반환하고 오류가 errors 객체에 추가됩니다.

irb> person = Person.new

irb> person.email = "me"
irb> person.valid?
=> # Token can't be blank (ActiveModel::StrictValidationFailed) 발생

irb> person.errors.to_hash
=> {:name => ["can't be blank"], :email => ["is invalid"]}

irb> person.errors.full_messages
=> ["Name can't be blank", "Email is invalid"]

Lint 테스트

ActiveModel::Lint::Tests 를 사용하면 객체가 액티브 모델 API를 준수하는지 테스트할 수 있습니다. TestCase에 ActiveModel::Lint::Tests를 포함하면 객체가 완전히 준수하는지 또는 API의 어떤 측면이 구현되지 않았는지 알려주는 테스트가 포함됩니다.

이러한 테스트는 반환된 값의 의미론적 정확성을 결정하려고 하지 않습니다. 예를 들어, valid?를 항상 true를 반환하도록 구현할 수 있으며 테스트가 통과될 것입니다. 값이 의미론적으로 의미 있는지 확인하는 것은 사용자의 책임입니다.

전달된 객체는 to_model 호출에서 호환 가능한 객체를 반환해야 합니다. to_modelself를 반환하는 것도 완전히 괜찮습니다.

  • app/models/person.rb

    class Person
      include ActiveModel::API
    end
    
  • test/models/person_test.rb

    require "test_helper"
    
    class PersonTest < ActiveSupport::TestCase
      include ActiveModel::Lint::Tests
    
      setup do
        @model = Person.new
      end
    end
    

테스트 메서드는 여기에서 찾을 수 있습니다.

테스트를 실행하려면 다음 명령을 사용할 수 있습니다:

$ bin/rails test

Run options: --seed 14596

# Running:

......

Finished in 0.024899s, 240.9735 runs/s, 1204.8677 assertions/s.

6 runs, 30 assertions, 0 failures, 0 errors, 0 skips