rubyonrails.org에서 더 보기:

GitHub에서 이 파일을 읽지 마세요. 가이드는 https://guides.rubyonrails.org 에서 제공됩니다.

Active Model 기초

이 가이드는 Active Model을 시작하는 데 필요한 내용을 제공합니다. Active Model은 Action Pack과 Action View helper가 일반 Ruby 객체와 상호작용할 수 있는 방법을 제공합니다. 또한 Rails 프레임워크 외부에서 사용할 수 있는 커스텀 ORM을 구축하는 데도 도움이 됩니다.

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

  • Active Model이 무엇이고 Active Record와 어떤 관계가 있는지
  • Active Model에 포함된 다양한 모듈들
  • 클래스에서 Active Model을 사용하는 방법

1 Active Model이란 무엇인가?

Active Model을 이해하려면 Active Record에 대해 조금 알아야 합니다. Active Record는 지속적인 저장이 필요한 객체의 데이터를 관계형 데이터베이스에 연결하는 ORM(Object Relational Mapper)입니다. 하지만 ORM 외부에서도 유용한 기능들이 있는데, 이에는 validation, callback, translation, 커스텀 속성 생성 기능 등이 포함됩니다.

이러한 기능들 중 일부가 Active Record에서 추출되어 Active Model을 형성했습니다. Active Model은 모델과 같은 기능이 필요하지만 데이터베이스 테이블과 연결될 필요가 없는 일반 Ruby 객체에서 사용할 수 있는 다양한 모듈을 포함하는 라이브러리입니다.

요약하면, Active Record가 데이터베이스 테이블에 대응하는 모델을 정의하는 인터페이스를 제공하는 반면, Active Model은 데이터베이스 지원이 반드시 필요하지 않은 모델과 같은 Ruby 클래스를 구축하기 위한 기능을 제공합니다. Active Model은 Active Record와 독립적으로 사용될 수 있습니다.

이러한 모듈 중 일부는 아래에서 설명합니다.

1.1 API

ActiveModel::API는 클래스가 Action PackAction View를 즉시 사용할 수 있는 기능을 추가합니다.

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

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

class EmailContact
  include ActiveModel::API

  attr_accessor :name, :email, :message
  validates :name, :email, :message, presence: true # name, email, message가 존재하는지 검증합니다

  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") # locale이 설정된 경우 번역
=> "Name"

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

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

ActiveModel::API를 포함하는 어떤 클래스든 Active Record 객체와 마찬가지로 form_with, render 및 다른 Action View helper methods와 함께 사용할 수 있습니다.

예를 들어, form_with는 다음과 같이 EmailContact 객체의 form을 생성하는 데 사용할 수 있습니다:

<%= 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는 object와 함께 partial을 렌더링하는데 사용될 수 있습니다.

<%= render @email_contact %>

이는 render @article처럼, render partial: "email_contacts/email_contact", locals: { email_contact: @email_contact }의 간단한 표현입니다. Rails는 객체의 클래스를 확인하여 올바른 partial을 결정합니다.

ActiveModel::API 호환 객체와 함께 form_withrender를 사용하는 방법에 대해서는 각각 Action View Form HelpersLayouts and Rendering 가이드에서 더 자세히 알아볼 수 있습니다.

1.2 Model

ActiveModel::Model은 기본적으로 Action Pack 및 Action View와 상호작용하기 위해 ActiveModel::API를 포함하며, model과 유사한 Ruby 클래스를 구현하는 데 권장되는 방법입니다. 향후 더 많은 기능을 추가하도록 확장될 예정입니다.

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"

1.3 Attributes

ActiveModel::Attributes를 사용하면 일반 Ruby 객체에서 데이터 타입을 정의하고, 기본값을 설정하며, 캐스팅과 직렬화를 처리할 수 있습니다. 이는 폼 데이터에 유용할 수 있으며, 일반 객체에서 날짜와 불리언과 같은 것들에 대해 Active Record와 유사한 변환을 제공합니다.

Attributes를 사용하려면 모듈을 모델 클래스에 포함시키고 attribute 매크로를 사용하여 속성을 정의하세요. 이는 이름, 캐스트 타입, 기본값, 그리고 속성 타입이 지원하는 기타 옵션들을 허용합니다.

class Person
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :date_of_birth, :date
  attribute :active, :boolean, default: true
end

위 코드는 ActiveModel::Attributes 모듈을 포함하는 Person 클래스를 정의합니다. 이는 문자열 타입의 name, date 타입의 date_of_birth, 기본값이 true인 boolean 타입의 active 속성을 가집니다.

irb> person = Person.new

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

# 문자열을 attribute에 설정된 date로 형변환
irb> person.date_of_birth = "2020-01-01"
irb> person.date_of_birth
=> Wed, 01 Jan 2020
irb> person.date_of_birth.class
=> Date

# attribute에 설정된 기본값 사용
irb> person.active
=> true

# 정수를 attribute에 설정된 boolean으로 형변환
irb> person.active = 0
irb> person.active
=> false

아래에 설명된 추가적인 메서드들은 ActiveModel::Attributes를 사용할 때 이용할 수 있습니다.

1.3.1 Method: attribute_names

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

irb> Person.attribute_names
=> ["name", "date_of_birth", "active"] # model의 속성명들을 반환합니다

1.3.2 Method: attributes

attributes 메소드는 모든 attribute들의 이름을 key로, attribute들의 값을 value로 가지는 hash를 반환합니다.

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

1.4 Attribute Assignment

ActiveModel::AttributeAssignment를 사용하면 attribute 이름과 일치하는 key를 가진 attribute hash를 전달하여 객체의 attribute들을 설정할 수 있습니다. 이는 여러 attribute들을 한 번에 설정하고 싶을 때 유용합니다.

다음 클래스를 살펴보세요:

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

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

permitted?는 request로부터 params 속성을 할당할 때 strong params 통합을 위해 사용됩니다.

irb> person = Person.new

# strong parameters 체크를 사용해서 요청의 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

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

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

1.4.1 메서드 별칭: 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=는 모두 메서드 호출이며, 할당할 속성의 hash를 인수로 받습니다. 많은 경우 Ruby는 메서드 호출에서 괄호 ()와 hash 정의에서 중괄호 {}를 생략할 수 있습니다.

attributes=와 같은 "Setter" 메서드는 괄호를 포함해도 동일하게 작동하지만, 일반적으로 ()없이 작성되며 hash에는 항상 {}를 포함해야 합니다. person.attributes=({ name: "John" })는 괜찮지만, person.attributes = name: "John"SyntaxError가 발생합니다.

assign_attributes와 같은 다른 메서드 호출은 hash 인수에 대해 괄호 (){}를 모두 포함하거나 포함하지 않을 수 있습니다. 예를 들어, assign_attributes name: "John"assign_attributes({ name: "John" })는 모두 완벽하게 유효한 Ruby 코드입니다. 하지만 assign_attributes { name: "John" }는 Ruby가 해당 hash 인수를 block과 구분할 수 없어 SyntaxError가 발생하므로 유효하지 않습니다.

1.5 Attribute Methods

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

  1. 클래스에 ActiveModel::AttributeMethods를 include합니다.
  2. 추가하고자 하는 각 메서드를 호출합니다(attribute_method_suffix, attribute_method_prefix, attribute_method_affix 등).
  3. 다른 메서드들 이후에 define_attribute_methods를 호출하여 prefix와 suffix가 적용되어야 할 속성을 선언합니다.
  4. 선언한 다양한 일반 _attribute 메서드들을 정의합니다. 이러한 메서드들의 attribute 매개변수는 define_attribute_methods에 전달된 인자로 대체됩니다. 아래 예시에서는 name입니다.

attribute_method_prefixattribute_method_suffix는 메서드를 생성하는 데 사용될 prefix와 suffix를 정의하는 데 사용됩니다. attribute_method_affix는 prefix와 suffix를 동시에 정의하는 데 사용됩니다.

class Person
  include ActiveModel::AttributeMethods

  # reset_와 _to_default! 접두사/접미사 메서드 정의
  attribute_method_affix prefix: "reset_", suffix: "_to_default!"
  # first_, last_ 접두사 메서드 정의
  attribute_method_prefix "first_", "last_"
  # _short? 접미사 메서드 정의 
  attribute_method_suffix "_short?"

  # name 속성에 대한 메서드 정의
  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 에러가 발생합니다.

1.5.1 Method: alias_attribute

ActiveModel::AttributeMethodsalias_attribute를 사용하여 attribute 메서드의 별칭을 제공합니다.

아래 예시는 name에 대해 full_name이라는 별칭 attribute를 생성합니다. 둘은 같은 값을 반환하지만, full_name 별칭이 이 attribute가 이름과 성을 포함한다는 것을 더 잘 나타냅니다.

class Person
  include ActiveModel::AttributeMethods

  attribute_method_suffix "_short?"
  define_attribute_methods :name

  attr_accessor :name

  alias_attribute :full_name, :name

  private
    # attribute의 길이가 5보다 작은지 확인하는 메서드
    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`의 alias이며, 같은 값을 반환합니다
irb> person.full_name  
=> "Joe Doe"
irb> person.name_short?
=> false

# `full_name_short?`는 `name_short?`의 alias이며, 같은 값을 반환합니다
irb> person.full_name_short?
=> false

1.6 Callbacks

ActiveModel::Callbacks는 일반 Ruby 객체에 Active Record 스타일의 callbacks을 제공합니다. 이 callbacks를 사용하면 before_updateafter_create와 같은 모델 생명주기 이벤트에 연결할 수 있으며, 모델의 생명주기의 특정 시점에서 실행될 사용자 정의 로직을 정의할 수 있습니다.

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

  1. 클래스 내에서 ActiveModel::Callbacks를 extend 합니다.
  2. define_model_callbacks를 사용하여 callbacks가 연결되어야 하는 메서드 목록을 설정합니다. :update와 같은 메서드를 지정하면, :update 이벤트에 대한 세 가지 기본 callbacks(before, around, after)이 자동으로 포함됩니다.
  3. 정의된 메서드 내에서 run_callbacks를 사용하면, 특정 이벤트가 발생할 때 callback 체인이 실행됩니다.
  4. 클래스에서 Active Record 모델에서처럼 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 메서드가 호출됨"
    end
  end

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

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

    # 객체에서 update가 호출되면 `around_update` 콜백에 의해 이 메서드가 호출됩니다
    def log_me
      puts "log_me 메서드: update 메서드 주변에서 호출됨"
      yield
      puts "log_me 메서드: 블록이 성공적으로 호출됨"
    end
end

위의 클래스는 다음과 같이 callback이 호출되는 순서를 보여줄 것입니다:

irb> person = Person.new
irb> person.update
reset_me method: update method 이전에 호출됨 
log_me method: update method 주변에서 호출됨
update method 호출됨
log_me method: block이 성공적으로 호출됨
finalize_me method: update method 이후에 호출됨
=> nil

위 예시처럼, 'around' 콜백을 정의할 때는 블록을 실행하기 위해 반드시 yield를 해야 한다는 것을 기억하세요. 그렇지 않으면 실행되지 않습니다.

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

1.6.1 특정 콜백 정의하기

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

설정한 모델 콜백 :update, :create:after:before 이벤트만 트리거합니다.

이는 before_create / after_createbefore_update / after_update callback들만 생성하고, around_* callback들은 건너뛸 것입니다. 이 옵션은 해당 메서드 호출에 정의된 모든 callback에 적용됩니다. 서로 다른 lifecycle 이벤트를 지정하기 위해 define_model_callbacks를 여러 번 호출하는 것이 가능합니다:

define_model_callbacks :create, only: :after   # create에 대해 after callback만 정의
define_model_callbacks :update, only: :before  # update에 대해 before callback만 정의
define_model_callbacks :destroy, only: :around # destroy에 대해 around callback만 정의

이는 after_create, before_update, around_destroy 메서드만 생성합니다.

1.6.2 클래스를 사용한 Callback 정의

callback이 언제, 어떤 컨텍스트에서 트리거되는지를 더 잘 제어하려면 before_<type>, after_<type>, around_<type>에 클래스를 전달할 수 있습니다. callback은 해당 클래스의 <action>_<type> 메서드를 트리거하며, 클래스의 인스턴스를 인자로 전달합니다.

class Person
  extend ActiveModel::Callbacks

  define_model_callbacks :create
  before_create PersonCallbacks
end

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

1.6.3 Callback 중단하기

:abort를 throw하면 언제든지 callback 체인을 중단할 수 있습니다. 이는 Active Record callback의 동작 방식과 유사합니다.

아래 예시에서, reset_me 메서드에서 update 전에 :abort를 throw하기 때문에, before_update를 포함한 나머지 callback 체인이 중단되고 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 메서드가 호출됨"
    end
  end

  private
    def reset_me
      puts "reset_me 메서드: update 메서드 이전에 호출됨"
      throw :abort
      puts "reset_me 메서드: abort 이후의 코드"
    end

    def finalize_me
      puts "finalize_me 메서드: update 메서드 이후에 호출됨"
    end

    def log_me
      puts "log_me 메서드: update 메서드 주변에서 호출됨" 
      yield
      puts "log_me 메서드: 블록이 성공적으로 호출됨"
    end
end
irb> person = Person.new

irb> person.update
reset_me 메서드: update 메서드 호출 전에 실행됨
=> false

1.7 변환

ActiveModel::Conversion는 객체를 다양한 목적에 맞게 다른 형태로 변환할 수 있게 해주는 메서드들의 모음입니다. 일반적인 사용 사례로는 URL, form 필드 등을 만들기 위해 객체를 문자열이나 정수로 변환하는 것이 있습니다.

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

  # persisted? 메서드는 object가 데이터베이스에 존재하는지를 나타냅니다
  def persisted?
    id.present? 
  end
end

1.7.1 to_model

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

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

만약 모델이 Active Model 객체처럼 동작하지 않는다면, :to_model을 직접 정의하여 Active Model 규격을 준수하는 메서드로 객체를 래핑하는 proxy 객체를 반환해야 합니다.

class Person
  def to_model
    # 당신의 object를 Active Model 호환 메소드로 감싸는 proxy object입니다.
    PersonModel.new(self)
  end
end

1.7.2 to_key

to_key 메서드는 객체가 persist되었는지 여부와 관계없이, 설정된 key 속성이 있다면 객체의 key 속성 배열을 반환합니다. key 속성이 없으면 nil을 반환합니다.

irb> person.to_key
=> [1]

key 속성은 객체를 식별하는데 사용되는 속성입니다. 예를 들어, 데이터베이스 기반 모델에서 key 속성은 primary key입니다.

1.7.3 to_param

to_param 메소드는 URL에 사용하기 적합한 객체의 key를 string 형태로 반환하거나, persisted?false인 경우 nil을 반환합니다.

irb> person.to_param
=> "1"

1.7.4 to_partial_path

to_partial_path 메서드는 객체와 연관된 경로를 나타내는 string을 반환합니다. Action Pack은 이를 사용하여 객체를 표현하는 데 적합한 partial을 찾습니다.

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

1.8 Dirty

ActiveModel::Dirty는 모델 attribute들이 저장되기 전에 변경사항을 추적하는 데 유용합니다. 이 기능을 사용하면 어떤 attribute가 수정되었는지, 이전 값과 현재 값이 무엇인지 확인하고, 이러한 변경사항을 기반으로 작업을 수행할 수 있습니다. 이는 애플리케이션의 감사(auditing), 유효성 검사, 조건부 로직에 특히 유용합니다. Active Record와 동일한 방식으로 객체의 변경사항을 추적하는 방법을 제공합니다.

객체는 attribute에 하나 이상의 변경사항이 있고 저장되지 않았을 때 dirty 상태가 됩니다. 이는 attribute 기반의 접근자 메소드를 가지고 있습니다.

ActiveModel::Dirty를 사용하기 위해서는 다음이 필요합니다:

  1. 클래스에 모듈을 포함시킵니다.
  2. define_attribute_methods를 사용하여 변경사항을 추적하고자 하는 attribute 메소드를 정의합니다.
  3. 추적 대상 attribute를 변경하기 전에 [attr_name]_will_change!를 호출합니다.
  4. 변경사항이 저장된 후 changes_applied를 호출합니다.
  5. 변경사항 정보를 초기화하고 싶을 때 clear_changes_information을 호출합니다.
  6. 이전 데이터를 복원하고 싶을 때 restore_attributes를 호출합니다.

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

first_namelast_name attribute를 가진 Person 클래스를 예로 들어, 이러한 attribute의 변경사항을 추적하기 위해 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
    # 데이터 유지 - dirty 데이터를 지우고 `changes`를 `previous_changes`로 이동합니다.
    changes_applied
  end

  def reload!
    # 모든 dirty 데이터를 지웁니다: 현재 변경사항과 이전 변경사항.
    clear_changes_information
  end

  def rollback!
    # 제공된 속성들의 모든 이전 데이터를 복원합니다.
    restore_attributes
  end
end

1.8.1 객체에게 직접 변경된 속성 목록 조회하기

You can query an object directly for its changed attributes using the following methods:

객체에게 변경된 속성을 직접 조회하기 위해 다음 method들을 사용할 수 있습니다:

  • changed_attributes - Returns a hash of the attributes that have changed with their original values
  • changed - Returns an array of the names of changed attributes
  • changed? - Returns a boolean indicating if any attributes have changed

  • changed_attributes - 변경된 속성들과 그들의 원래 값이 담긴 hash를 반환합니다

  • changed - 변경된 속성들의 이름이 담긴 array를 반환합니다

  • changed? - 어떤 속성이든 변경되었는지 여부를 나타내는 boolean을 반환합니다

person = Person.new
person.changed? # => false

person.name = 'Bob'
person.changed? # => true
person.changed  # => ["name"]
person.changed_attributes # => {"name" => nil}

person.name = 'Bill'
person.changed? # => true
person.changed  # => ["name"]
person.changed_attributes # => {"name" => nil}

person.name = 'Bob'
person.changed? # => true
person.changed  # => ["name"]
person.changed_attributes # => {"name" => nil}
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는 저장되지 않은 변경사항이 있는 attribute 이름들의 array를 반환합니다.

irb> person.changed
=> ["first_name"] # first_name이 변경되었음을 나타냄

changed_attributes는 저장되지 않은 변경사항이 있는 attribute들을 attr => original value 형식으로 원래 값을 나타내는 hash를 반환합니다.

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

참고: 위 내용은 변경될 속성들의 이전 값을 보여줍니다.

changes는 변경사항을 hash로 반환합니다. 속성 이름이 key가 되고, value는 attr => [이전 값, 새 값] 형태로 원본 값과 새로운 값의 배열로 구성됩니다.

irb> person.changes
=> {"first_name" => [nil, "Jane Doe"]} 
# 값이 [이전 값, 새로운 값] 형태로 표시됨

previous_changes는 모델이 저장되기 전에 변경된 속성들의 hash를 반환합니다(즉, changes_applied가 호출되기 전의 상태).

irb> person.previous_changes
=> {}

irb> person.save
irb> person.previous_changes
=> {"first_name" => [nil, "Jane Doe"]} # 이전 변경사항 [변경 전 값, 변경 후 값]

1.8.2 Attribute 기반 접근자 메서드

irb> person = Person.new

irb> person.changed? 
=> false

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

[attr_name]_changed? 는 특정 attribute가 변경되었는지 여부를 확인합니다.

irb> person.first_name_changed?
=> true

[attr_name]_was는 해당 attribute의 이전 값을 추적합니다.

irb> person.first_name_was
=> nil

[attr_name]_change는 변경된 attribute의 이전 값과 현재 값을 모두 추적합니다. 변경된 경우 [원래 값, 새로운 값] 형태의 배열을 반환하고, 변경되지 않은 경우 nil을 반환합니다.

irb> person.first_name_change
=> [변경  값은 nil, "John Doe" 변경됨]
irb> person.last_name_change
=> nil

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

irb> person.first_name_previously_changed? # first_name이 이전에 변경되었는지 확인
=> false
irb> person.save
irb> person.first_name_previously_changed? # 저장 후 first_name이 이전에 변경되었는지 확인
=> true

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

irb> person.first_name_previous_change
=> [nil, "John Doe"]
# 이전 변경 내용인 [변경 전 값, 변경 후 값]을 반환합니다

1.9 Naming

ActiveModel::Naming은 naming과 routing을 더 쉽게 관리할 수 있도록 클래스 메서드와 헬퍼 메서드를 추가합니다. 이 모듈은 ActiveSupport::Inflector 메서드를 사용하여 여러 accessor를 정의하는 model_name 클래스 메서드를 정의합니다.

class Person
  extend ActiveModel::Naming
end

이는 class에 Active Model naming과 관련된 여러 helper method를 주입하는 module을 extend 합니다.

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

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

singular는 record나 class의 단수형 class name을 반환합니다.

irb> Person.model_name.singular # Person model의 단수형 이름을 반환
=> "person"

plural은 record나 class의 복수형 클래스 이름을 반환합니다.

irb> Person.model_name.plural # model의 복수형 이름을 가져옴
=> "people"

element는 namespace를 제거하고 단수형의 snake_case 이름을 반환합니다. 일반적으로 Action Pack이나 Action View helper에서 partial/form의 이름을 렌더링하는 것을 돕는 데 사용됩니다.

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

human은 I18n을 사용하여 모델 이름을 더 사람이 읽기 쉬운 형식으로 변환합니다. 기본적으로 클래스 이름을 underscore 처리한 다음 humanize 처리합니다.

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

collection은 namespace를 제거하고 복수형의 snake_case 이름을 반환합니다. 일반적으로 Action Pack 및/또는 Action View helper에서 partial/form의 이름을 렌더링하는 데 도움을 주기 위해 사용됩니다.

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

param_key는 params 이름으로 사용할 문자열을 반환합니다.

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

i18n_key는 i18n key의 이름을 반환합니다. model 이름을 underscore로 변환한 다음 symbol로 반환합니다.

irb> Person.model_name.i18n_key
=> :person
# person Symbol을 리턴합니다

route_key 는 route 이름을 생성할 때 사용할 문자열을 반환합니다.

irb> Person.model_name.route_key
=> "people" # 라우트에서 사용되는 복수형 모델 이름

singular_route_key는 route 이름을 생성할 때 사용할 문자열을 반환합니다.

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

uncountable? 은 record나 class의 class name이 불가산(uncountable)인지 여부를 확인합니다.

irb> Person.model_name.uncountable?
=> false

param_key, route_key, singular_route_key와 같은 일부 Naming 메서드는 Engine 내부에 있는지 여부에 따라 네임스페이스가 있는 모델에 대해 다르게 동작합니다.

1.9.1 모델의 이름 커스터마이즈하기

때로는 form helper와 URL 생성에 사용되는 모델의 이름을 커스터마이즈하고 싶을 수 있습니다. 이는 전체 네임스페이스를 사용하여 참조하면서도 모델에 대해 더 사용자 친화적인 이름을 사용하고 싶을 때 유용할 수 있습니다.

예를 들어, Rails 애플리케이션에 Person 네임스페이스가 있고 새로운 Person::Profile을 위한 폼을 만들고 싶다고 가정해봅시다.

기본적으로 Rails는 네임스페이스 person을 포함하는 URL /person/profiles로 폼을 생성합니다. 하지만 네임스페이스 없이 단순히 profiles를 가리키는 URL을 원한다면, model_name 메서드를 다음과 같이 커스터마이즈할 수 있습니다:

module Person
  class Profile
    include ActiveModel::Model

    # model_name은 기본적으로 module namespace를 class 이름의 일부로 포함시킵니다.
    # 이 예제에서 namespace를 제거하고 model name을 "Profile"로만 설정하려면
    # ActiveModel::Name.new의 두 번째 인자로 nil을 전달하면 됩니다.
    def self.model_name
      ActiveModel::Name.new(self, nil, "Profile")
    end
  end
end

이러한 설정으로, form_with 헬퍼를 사용하여 새로운 Person::Profile을 생성하는 폼을 만들 때, Rails는 /person/profiles 대신 /profiles URL로 폼을 생성합니다. 이는 model_name 메서드가 Profile을 반환하도록 오버라이드되었기 때문입니다.

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

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

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

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" # 라우팅에서 사용되는 복수형 키

1.10 SecurePassword

ActiveModel::SecurePassword는 모든 비밀번호를 암호화된 형태로 안전하게 저장하는 방법을 제공합니다. 이 module을 include하면, has_secure_password 클래스 메서드를 사용할 수 있게 되며, 이는 기본적으로 특정 유효성 검사가 포함된 password accessor를 정의합니다.

ActiveModel::SecurePasswordbcrypt에 의존하므로, 이를 사용하기 위해서는 Gemfile에 이 gem을 포함해야 합니다.

gem "bcrypt"

ActiveModel::SecurePasswordpassword_digest 속성이 필요합니다.

다음 유효성 검사들이 자동으로 추가됩니다:

  1. 생성시 비밀번호가 존재해야 합니다.
  2. 비밀번호 확인(password_confirmation 속성 사용).
  3. 비밀번호의 최대 길이는 72바이트입니다(bcrypt가 암호화하기 전에 문자열을 이 크기로 잘라내기 때문에 필요).

비밀번호 확인 유효성 검사가 필요하지 않다면, 간단히 password_confirmation 값을 생략하면 됩니다(즉, 이를 위한 form 필드를 제공하지 않습니다). 이 속성이 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

위 예제는 두 가지 password 필드를 선언합니다: 유효성 검사가 있는 기본 password와 유효성 검사가 비활성화된 recovery_password입니다.

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"

1.11 Serialization

ActiveModel::Serialization은 객체에 대한 기본적인 serialization을 제공합니다. serialization하고자 하는 속성들이 포함된 attributes hash를 선언해야 합니다. 속성들은 반드시 symbol이 아닌 string이어야 합니다.

class Person
  include ActiveModel::Serialization

  attr_accessor :name, :age

  def attributes
    # serialization될 attributes 선언
    { "name" => nil, "age" => nil }
  end

  def capitalized_name
    # 선언된 메서드는 나중에 serialized hash에 포함될 수 있음
    name&.capitalize
  end
end

이제 serializable_hash 메서드를 사용하여 객체의 serialized 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"}]}

1.11.1 ActiveModel::Serializers::JSON

Active Model은 JSON serializing / deserializing을 위한 ActiveModel::Serializers::JSON 모듈도 제공합니다.

JSON serialization을 사용하기 위해서는 포함하고 있는 모듈을 ActiveModel::Serialization에서 ActiveModel::Serializers::JSON로 변경하세요. 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

위 클래스는 JSON 직렬화를 위한 attributes 메서드를 정의하고 있습니다. attributes 메서드는 serializable 속성들의 기본값을 해시로 반환합니다. 이 해시의 키는 직렬화할 속성의 이름이고 값은 기본값입니다.

이제 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 internationalization (i18n) framework 간의 통합을 제공합니다.

class Person
  extend ActiveModel::Translation
end

Person class에 ActiveModel::Translation을 extend하면 번역과 지역화 기능을 추가할 수 있습니다.

human_attribute_name 메서드를 사용하면 attribute 이름을 사람이 더 읽기 쉬운 형식으로 변환할 수 있습니다. 사람이 읽기 쉬운 형식은 locale 파일에서 정의됩니다.

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

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

1.12 Validations

ActiveModel::Validations는 객체를 검증하는 기능을 추가하며, 애플리케이션의 데이터 무결성과 일관성을 보장하는 데 중요합니다. 모델에 validation을 통합함으로써 속성 값의 정확성을 관리하는 규칙을 정의하고 유효하지 않은 데이터를 방지할 수 있습니다.

class Person
  include ActiveModel::Validations

  attr_accessor :name, :email, :token

  # name 속성이 비어있지 않은지 검증합니다
  validates :name, presence: true
  # email 속성이 올바른 이메일 형식인지 검증합니다  
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
  # token 속성이 비어있지 않은지 엄격하게 검증합니다
  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)"

1.12.1 Validation Methods와 Options

다음과 같은 메서드를 사용하여 validation을 추가할 수 있습니다:

  • validate: 메서드나 block을 통해 클래스에 validation을 추가합니다.

  • validates: validates 메서드에 attribute를 전달하면 모든 기본 validator에 대한 단축 방법을 제공합니다.

  • validates! 또는 strict: true 설정: 최종 사용자가 수정할 수 없고 예외적인 것으로 간주되는 validation을 정의하는 데 사용됩니다. 느낌표가 있거나 :strict 옵션이 true로 설정된 각 validator는 validation이 실패할 때 errors에 추가하는 대신 항상 ActiveModel::StrictValidationFailed를 발생시킵니다.

  • validates_with: record를 지정된 클래스로 전달하여 더 복잡한 조건을 기반으로 에러를 추가할 수 있도록 합니다.

  • validates_each: 각 attribute를 block에 대해 검증합니다.

아래의 일부 옵션은 특정 validator와 함께 사용할 수 있습니다. 사용 중인 옵션이 특정 validator와 함께 사용될 수 있는지 확인하려면 validation 문서를 참조하세요.

  • :on: validation을 추가할 context를 지정합니다. symbol이나 symbol 배열을 전달할 수 있습니다. (예: on: :create 또는 on: :custom_validation_context 또는 on: [:create, :custom_validation_context]). :on 옵션이 없는 validation은 context에 관계없이 실행됩니다. :on 옵션이 있는 validation은 지정된 context에서만 실행됩니다. context는 valid?(:context)를 통해 validation할 때 전달할 수 있습니다.

  • :if: validation이 발생해야 하는지 결정하기 위해 호출할 메서드, proc 또는 문자열을 지정합니다 (예: if: :allow_validation, 또는 if: -> { signup_step > 2 }). 메서드, proc 또는 문자열은 true 또는 false 값을 반환하거나 평가해야 합니다.

  • :unless: validation이 발생하지 말아야 하는지 결정하기 위해 호출할 메서드, proc 또는 문자열을 지정합니다 (예: unless: :skip_validation, 또는 unless: Proc.new { |user| user.signup_step <= 2 }). 메서드, proc 또는 문자열은 true 또는 false 값을 반환하거나 평가해야 합니다.

  • :allow_nil: attribute가 nil인 경우 validation을 건너뜁니다.

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

  • :strict: :strict 옵션이 true로 설정되면, 에러를 추가하는 대신 ActiveModel::StrictValidationFailed를 발생시킵니다. :strict 옵션은 다른 예외로도 설정할 수 있습니다.

동일한 메서드에서 validate를 여러 번 호출하면 이전 정의를 덮어씁니다.

1.12.2 Errors

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

객체가 유효한지 확인하려면 객체에서 valid?를 실행하세요. 객체가 유효하지 않으면 false를 반환하고 에러가 errors 객체에 추가됩니다.

irb> person = Person.new

irb> person.email = "me" 
irb> person.valid?
=> # Token이 비어있을 수 없다는 오류 발생 (ActiveModel::StrictValidationFailed)

irb> person.errors.to_hash
=> {:name => ["비어있을 수 없습니다"], :email => ["유효하지 않습니다"]}

irb> person.errors.full_messages
=> ["Name은 비어있을 수 없습니다", "Email이 유효하지 않습니다"]

1.13 Lint 테스트

ActiveModel::Lint::Tests를 사용하면 객체가 Active Model 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

실행 옵션: --seed 14596

# 실행 중:

......

0.024899초 만에 완료, 240.9735회/초 실행, 1204.8677회/초 검증.

6회 실행, 30회 검증, 0회 실패, 0회 에러, 0회 건너뜀


맨 위로