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 Pack과 Action 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_with
와 render
를 사용하는 방법에 대해서는 각각 Action View Form Helpers와 Layouts 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_attributes
와 attributes=
는 모두 메서드 호출이며, 할당할 속성의 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를 정의하고 객체의 어떤 메서드가 이를 사용할지 정의할 수 있습니다:
- 클래스에
ActiveModel::AttributeMethods
를 include합니다. - 추가하고자 하는 각 메서드를 호출합니다(
attribute_method_suffix
,attribute_method_prefix
,attribute_method_affix
등). - 다른 메서드들 이후에
define_attribute_methods
를 호출하여 prefix와 suffix가 적용되어야 할 속성을 선언합니다. - 선언한 다양한 일반
_attribute
메서드들을 정의합니다. 이러한 메서드들의attribute
매개변수는define_attribute_methods
에 전달된 인자로 대체됩니다. 아래 예시에서는name
입니다.
attribute_method_prefix
와 attribute_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::AttributeMethods
는 alias_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_update
와 after_create
와 같은 모델 생명주기 이벤트에 연결할 수 있으며, 모델의 생명주기의 특정 시점에서 실행될 사용자 정의 로직을 정의할 수 있습니다.
아래 단계를 따라 ActiveModel::Callbacks
를 구현할 수 있습니다:
- 클래스 내에서
ActiveModel::Callbacks
를 extend 합니다. define_model_callbacks
를 사용하여 callbacks가 연결되어야 하는 메서드 목록을 설정합니다.:update
와 같은 메서드를 지정하면,:update
이벤트에 대한 세 가지 기본 callbacks(before
,around
,after
)이 자동으로 포함됩니다.- 정의된 메서드 내에서
run_callbacks
를 사용하면, 특정 이벤트가 발생할 때 callback 체인이 실행됩니다. - 클래스에서 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_create
와 before_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
를 사용하기 위해서는 다음이 필요합니다:
- 클래스에 모듈을 포함시킵니다.
define_attribute_methods
를 사용하여 변경사항을 추적하고자 하는 attribute 메소드를 정의합니다.- 추적 대상 attribute를 변경하기 전에
[attr_name]_will_change!
를 호출합니다. - 변경사항이 저장된 후
changes_applied
를 호출합니다. - 변경사항 정보를 초기화하고 싶을 때
clear_changes_information
을 호출합니다. - 이전 데이터를 복원하고 싶을 때
restore_attributes
를 호출합니다.
그러면 ActiveModel::Dirty
가 제공하는 메소드를 사용하여 변경된 모든 attribute 목록, 변경된 attribute의 원래 값, attribute에 대한 변경사항을 객체에 쿼리할 수 있습니다.
first_name
과 last_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 valueschanged
- Returns an array of the names of changed attributeschanged?
- Returns a boolean indicating if any attributes have changedchanged_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::SecurePassword
는 bcrypt
에 의존하므로, 이를 사용하기 위해서는 Gemfile
에 이 gem을 포함해야 합니다.
gem "bcrypt"
ActiveModel::SecurePassword
는 password_digest
속성이 필요합니다.
다음 유효성 검사들이 자동으로 추가됩니다:
- 생성시 비밀번호가 존재해야 합니다.
- 비밀번호 확인(
password_confirmation
속성 사용). - 비밀번호의 최대 길이는 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_model
이 self
를 반환하는 것은 완전히 괜찮습니다.
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회 건너뜀