액티브 레코드 콜백
이 가이드는 액티브 레코드 객체의 생명 주기에 연결하는 방법을 알려줍니다.
이 가이드를 읽고 나면 다음을 알 수 있습니다:
- 액티브 레코드 객체의 생명 주기 동안 발생하는 특정 이벤트
- 객체 생명 주기의 이벤트에 응답하는 콜백 메서드를 만드는 방법
- 콜백에 대한 공통 동작을 캡슐화하는 특수 클래스를 만드는 방법
객체 생명 주기
Rails 애플리케이션의 일반적인 작동 중에 객체가 생성, 업데이트 및 삭제될 수 있습니다. 액티브 레코드는 이 객체 생명 주기에 대한 후크를 제공하므로 애플리케이션과 데이터를 제어할 수 있습니다.
콜백을 사용하면 객체 상태의 변경 전후에 논리를 트리거할 수 있습니다.
class Baby < ApplicationRecord after_create -> { puts "Congratulations!" } end
irb> @baby = Baby.create Congratulations!
보시다시피 많은 생명 주기 이벤트가 있으며 이들 중 어느 것이든 전, 후 또는 주변에서 연결할 수 있습니다.
콜백 개요
콜백은 객체의 생명 주기 중 특정 시점에 호출되는 메서드입니다. 콜백을 사용하면 액티브 레코드 객체가 생성, 저장, 업데이트, 삭제, 유효성 검사 또는 데이터베이스에서 로드될 때마다 실행되는 코드를 작성할 수 있습니다.
콜백 등록
사용 가능한 콜백을 사용하려면 이를 등록해야 합니다. 일반 메서드로 구현하고 매크로 스타일 클래스 메서드를 사용하여 콜백으로 등록할 수 있습니다:
class User < ApplicationRecord validates :login, :email, presence: true before_validation :ensure_login_has_a_value private def ensure_login_has_a_value if login.blank? self.login = email unless email.blank? end end end
매크로 스타일 클래스 메서드에 블록을 전달할 수도 있습니다. 블록 내부의 코드가 한 줄 정도로 짧은 경우 이 스타일을 사용하는 것이 좋습니다:
class User < ApplicationRecord validates :login, :email, presence: true before_create do self.name = login.capitalize if name.blank? end end
또한 트리거될 프로시저를 전달하여 콜백을 등록할 수 있습니다.
class User < ApplicationRecord before_create ->(user) { user.name = user.login.capitalize if user.name.blank? } end
마지막으로 사용자 정의 콜백 객체를 정의할 수 있으며, 이에 대해서는 아래에서 자세히 다룹니다.
class User < ApplicationRecord before_create MaybeAddName end class MaybeAddName def self.before_create(record) if record.name.blank? record.name = record.login.capitalize end end end
콜백은 특정 생명 주기 이벤트에 대해서만 등록할 수 있으며, 이를 통해 콜백이 트리거되는 시기와 컨텍스트를 완전히 제어할 수 있습니다.
class User < ApplicationRecord before_validation :normalize_name, on: :create # :on은 배열도 허용합니다 after_validation :set_location, on: [ :create, :update ] private def normalize_name self.name = name.downcase.titleize end def set_location self.location = LocationService.query(self) end end
콜백 메서드를 private으로 선언하는 것이 좋습니다. 공개된 경우 모델 외부에서 호출될 수 있어 객체 캡슐화 원칙을 위반할 수 있습니다.
경고. 콜백 내에서 update
, save
또는 객체에 부작용을 일으키는 다른 메서드를 호출하지 마세요. 예를 들어 after_create
/ before_update
또는 이른 단계의 콜백 내에서 update(attribute: "value")
를 호출하지 마세요. 이렇게 하면 모델의 상태가 변경되어 예기치 않은 부작용이 발생할 수 있습니다. 대신 값을 직접 할당(예: self.attribute = "value"
)할 수 있습니다.
사용 가능한 콜백
다음은 액티브 레코드 콜백의 전체 목록으로, 해당 작업 중에 호출되는 순서대로 나열되어 있습니다:
객체 생성
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
after_commit
/after_rollback
객체 업데이트
before_validation
after_validation
before_save
around_save
before_update
around_update
after_update
after_save
after_commit
/after_rollback
경고. after_save
는 생성과 업데이트 모두에서 실행되지만 항상 더 구체적인 콜백 after_create
와 after_update
이후에 실행됩니다. 매크로 호출 순서와 상관없습니다.
객체 삭제
참고: before_destroy
콜백은 dependent: :destroy
연관관계 앞에 배치해야 합니다(또는 prepend: true
옵션을 사용해야 합니다). 이렇게 하면 레코드가 dependent: :destroy
에 의해 삭제되기 전에 실행됩니다.
경고. after_commit
은 after_save
, after_update
및 after_destroy
와 매우 다른 보장을 합니다. 예를 들어 after_save
에서 예외가 발생하면 트랜잭션이 롤백되고 데이터가 영구적으로 저장되지 않습니다. 반면 after_commit
에서 발생하는 모든 것은 트랜잭션이 이미 완료되고 데이터가 데이터베이스에 영구적으로 저장되었음을 보장합니다. 트랜잭션 콜백에 대해 자세히 알아보세요.
after_initialize
및 after_find
액티브 레코드 객체가 인스턴스화될 때마다 after_initialize
콜백이 호출됩니다. 이는 new
를 직접 사용하거나 레코드가 데이터베이스에서 로드될 때 발생합니다. 액티브 레코드 initialize
메서드를 직접 재정의할 필요가 없게 해줍니다.
데이터베이스에서 레코드를 로드할 때 after_find
콜백이 호출됩니다. after_find
는 after_initialize
가 정의된 경우 after_initialize
전에 호출됩니다.
참고: after_initialize
및 after_find
콜백에는 before_*
대응 항목이 없습니다.
다른 액티브 레코드 콜백과 마찬가지로 등록할 수 있습니다.
class User < ApplicationRecord after_initialize do |user| puts "You have initialized an object!" end after_find do |user| puts "You have found an object!" end end
irb> User.new You have initialized an object! => #<User id: nil> irb> User.first You have found an object! You have initialized an object! => #<User id: 1>
after_touch
after_touch
콜백은 액티브 레코드 객체가 터치될 때마다 호출됩니다.
class User < ApplicationRecord after_touch do |user| puts "You have touched an object" end end
irb> u = User.create(name: 'Kuldeep') => #<User id: 1, name: "Kuldeep", created_at: "2013-11-25 12:17:49", updated_at: "2013-11-25 12:17:49"> irb> u.touch You have touched an object => true
belongs_to
와 함께 사용할 수 있습니다:
class Book < ApplicationRecord belongs_to :library, touch: true after_touch do puts 'A Book was touched' end end class Library < ApplicationRecord has_many :books after_touch :log_when_books_or_library_touched private def log_when_books_or_library_touched puts 'Book/Library was touched' end end
irb> @book = Book.last => #<Book id: 1, library_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05"> irb> @book.touch # triggers @book.library.touch A Book was touched Book/Library was touched => true
콜백 실행
다음 메서드가 콜백을 트리거합니다:
create
create!
destroy
destroy!
destroy_all
destroy_by
save
save!
save(validate: false)
save!(validate: false)
toggle!
touch
update_attribute
update
update!
valid?
또한 다음 파인더 메서드가 `after_fin네, 계속해서 한국어 번역을 제공하겠습니다.
콜백 실행 (계속)
또한 다음 파인더 메서드가 after_find
콜백을 트리거합니다:
all
first
find
find_by
find_by_*
find_by_*!
find_by_sql
last
after_initialize
콜백은 새 객체가 초기화될 때마다 트리거됩니다.
참고: find_by_*
및 find_by_*!
메서드는 각 속성에 대해 자동으로 생성되는 동적 파인더입니다. 동적 파인더 섹션에서 자세히 알아보세요.
콜백 건너뛰기
유효성 검사와 마찬가지로 다음 메서드를 사용하여 콜백을 건너뛸 수 있습니다:
decrement!
decrement_counter
delete
delete_all
delete_by
increment!
increment_counter
insert
insert!
insert_all
insert_all!
touch_all
update_column
update_columns
update_all
update_counters
upsert
upsert_all
그러나 이러한 메서드는 주의해서 사용해야 합니다. 중요한 비즈니스 규칙과 애플리케이션 로직이 콜백에 포함되어 있을 수 있기 때문입니다. 잠재적인 영향을 이해하지 않고 이를 우회하면 잘못된 데이터가 발생할 수 있습니다. 자세한 내용은 메서드 문서를 참조하세요.
실행 중단
새 콜백을 모델에 등록하면 실행 대기열에 추가됩니다. 이 대기열에는 모든 모델 유효성 검사, 등록된 콜백 및 실행될 데이터베이스 작업이 포함됩니다.
전체 콜백 체인은 트랜잭션으로 래핑됩니다. 콜백 중 하나라도 예외를 발생시키면 실행 체인이 중단되고 ROLLBACK이 발행됩니다. 의도적으로 체인을 중단하려면 다음을 사용하세요:
throw :abort
경고. ActiveRecord::Rollback
또는 ActiveRecord::RecordInvalid
이외의 예외가 발생하면 Rails에 의해 다시 발생됩니다. 또한 save
와 update
(일반적으로 true
또는 false
를 반환하려고 함)가 예외를 발생시킬 것으로 예상하지 않는 코드를 중단할 수 있습니다.
참고: after_destroy
, before_destroy
또는 around_destroy
콜백 내에서 ActiveRecord::RecordNotDestroyed
가 발생하면 다시 발생되지 않으며 destroy
메서드가 false
를 반환합니다.
관계 콜백
콜백은 모델 관계를 통해 작동하며 관계에 의해 정의될 수도 있습니다. 사용자가 많은 기사를 가지고 있는 예를 들어 보겠습니다. 사용자가 삭제되면 사용자의 기사도 삭제되어야 합니다. 사용자 모델의 after_destroy
콜백을 통해 Article
모델과의 관계로 추가해 보겠습니다:
class User < ApplicationRecord has_many :articles, dependent: :destroy end class Article < ApplicationRecord after_destroy :log_destroy_action def log_destroy_action puts 'Article destroyed' end end
irb> user = User.first => #<User id: 1> irb> user.articles.create! => #<Article id: 1, user_id: 1> irb> user.destroy Article destroyed => #<User id: 1>
연관 콜백
연관 콜백은 일반 콜백과 유사하지만 컬렉션의 생명 주기 이벤트에 의해 트리거됩니다. 사용 가능한 연관 콜백은 다음과 같습니다:
before_add
after_add
before_remove
after_remove
연관 콜백은 연관 선언에 옵션을 추가하여 정의합니다. 예를 들어:
class Author < ApplicationRecord has_many :books, before_add: :check_credit_limit def check_credit_limit(book) # ... end end
Rails는 추가 또는 제거되는 객체를 콜백에 전달합니다.
단일 이벤트에 대해 여러 콜백을 쌓을 수 있습니다:
class Author < ApplicationRecord has_many :books, before_add: [:check_credit_limit, :calculate_shipping_charges] def check_credit_limit(book) # ... end def calculate_shipping_charges(book) # ... end end
before_add
콜백이 :abort
를 throw하면 객체가 컬렉션에 추가되지 않습니다. 마찬가지로 before_remove
콜백이 :abort
를 throw하면 객체가 컬렉션에서 제거되지 않습니다:
# 한도에 도달하면 book이 추가되지 않습니다 def check_credit_limit(book) throw(:abort) if limit_reached? end
참고: 이러한 콜백은 연관 객체가 연관 컬렉션을 통해 추가 또는 제거될 때만 호출됩니다:
# `before_add` 콜백 트리거 author.books << book author.books = [book, book2] # `before_add` 콜백 트리거 안 함 book.update(author_id: 1)
조건부 콜백
유효성 검사와 마찬가지로 술어의 만족 여부에 따라 콜백 메서드 호출을 조건부로 만들 수 있습니다. :if
및 :unless
옵션을 사용하여 이를 수행할 수 있으며, 기호, Proc
또는 Array
를 사용할 수 있습니다.
콜백이 호출되어야 하는 조건을 지정하려면 :if
옵션을 사용할 수 있습니다. 콜백이 호출되지 않아야 하는 조건을 지정하려면 :unless
옵션을 사용할 수 있습니다.
:if
및 :unless
를 기호와 함께 사용하기
콜백이 호출되기 직전에 호출되는 술어 메서드의 이름에 해당하는 기호와 :if
및 :unless
옵션을 연결할 수 있습니다.
:if
옵션을 사용할 때 술어 메서드가 false를 반환하면 콜백이 실행되지 않습니다. :unless
옵션을 사용할 때 술어 메서드가 true를 반환하면 콜백이 실행되지 않습니다. 이것이 가장 일반적인 옵션입니다.
class Order < ApplicationRecord before_save :normalize_card_number, if: :paid_with_card? end
이 등록 형식을 사용하면 콜백이 실행되어야 하는지 확인하기 위해 호출되어야 하는 여러 다른 술어를 등록할 수도 있습니다. 이에 대해서는 아래에서 다룹니다.
:if
및 :unless
를 Proc
과 함께 사용하기
:if
및 :unless
를 Proc
객체와 연결할 수 있습니다. 이 옵션은 짧은 유효성 검사 메서드, 일반적으로 한 줄 정도의 메서드를 작성할 때 가장 적합합니다:
class Order < ApplicationRecord before_save :normalize_card_number, if: Proc.new { |order| order.paid_with_card? } end
프로시저가 객체 컨텍스트에서 평가되므로 다음과 같이 작성할 수도 있습니다:
class Order < ApplicationRecord before_save :normalize_card_number, if: Proc.new { paid_with_card? } end
다중 콜백 조건
:if
및 :unless
옵션은 프로시저 또는 기호로 표현된 메서드 이름의 배열도 허용합니다:
class Comment < ApplicationRecord before_save :filter_content, if: [:subject_to_parental_control?, :untrusted_author?] end
프로시저 목록에 포함시킬 수도 있습니다:
class Comment < ApplicationRecord before_save :filter_content, if: [:subject_to_parental_control?, Proc.new { untrusted_author? }] end
:if
와 :unless
함께 사용하기
콜백에 :if
와 :unless
를 함께 사용할 수 있습니다:
class Comment < ApplicationRecord before_save :filter_content, if: Proc.new { forum.parental_control? }, unless: Proc.new { author.trusted? } end
모든 :if
조건이 충족되고 :unless
조건이 하나도 충족되지 않는 경우에만 콜백이 실행됩니다.
콜백 클래스
때로는 작성할 콜백 메서드가 다른 모델에서도 유용할 수 있습니다. 액티브 레코드는 콜백 메서드를 캡슐화하는 클래스를 만들 수 있는 기능을 제공하므로 이를 재사용할 수 있습니다.
여기서는 폐기된 파일의 파일 시스템 정리를 처리하는 after_destroy
콜백이 있는 클래스를 만드는 예를 보여드리겠습니다. 이 동작은 PictureFile
모델에만 고유하지 않을 수 있으며 별도의 클래스로 캡슐화하는 것이 좋습니다. 이렇게 하면 이 동작을 테스트하고 변경네, 계속해서 콜백 클래스에 대한 설명을 제공하겠습니다.
콜백 클래스 (계속)
여기서는 폐기된 파일의 파일 시스템 정리를 처리하는 after_destroy
콜백이 있는 클래스를 만드는 예를 보여드리겠습니다. 이 동작은 PictureFile
모델에만 고유하지 않을 수 있으며 별도의 클래스로 캡슐화하는 것이 좋습니다. 이렇게 하면 이 동작을 테스트하고 변경하기가 훨씬 쉬워집니다.
class FileDestroyerCallback def after_destroy(file) if File.exist?(file.filepath) File.delete(file.filepath) end end end
위와 같이 클래스 내부에 선언된 콜백 메서드는 모델 객체를 매개변수로 받습니다. 이는 다음과 같이 모델에서 사용할 수 있습니다:
class PictureFile < ApplicationRecord after_destroy FileDestroyerCallback.new end
인스턴스 메서드로 선언했기 때문에 FileDestroyerCallback
객체를 새로 인스턴스화해야 한다는 점에 유의하세요. 콜백이 인스턴스화된 객체의 상태를 사용하는 경우 특히 유용합니다. 그러나 종종 콜백을 클래스 메서드로 선언하는 것이 더 적절할 것입니다:
class FileDestroyerCallback def self.after_destroy(file) if File.exist?(file.filepath) File.delete(file.filepath) end end end
콜백 메서드가 이와 같이 선언되면 FileDestroyerCallback
객체를 모델에 인스턴스화할 필요가 없습니다.
class PictureFile < ApplicationRecord after_destroy FileDestroyerCallback end
원하는 만큼 많은 콜백을 콜백 클래스 내부에 선언할 수 있습니다.
트랜잭션 콜백
after_commit
및 after_rollback
데이터베이스 트랜잭션 완료 시 트리거되는 두 가지 추가 콜백이 있습니다: after_commit
및 after_rollback
. 이 콜백은 after_save
콜백과 매우 유사하지만 데이터베이스 변경 사항이 커밋되거나 롤백된 후에만 실행됩니다. 액티브 레코드 모델이 데이터베이스 트랜잭션의 일부가 아닌 외부 시스템과 상호 작용해야 할 때 가장 유용합니다.
예를 들어, PictureFile
모델이 해당 레코드가 삭제된 후 파일을 삭제해야 한다고 가정해 보겠습니다. after_destroy
콜백이 호출된 후 예외가 발생하고 트랜잭션이 롤백되면 파일이 이미 삭제되어 모델이 일관되지 않은 상태가 됩니다. 예를 들어 아래 코드에서 picture_file_2
가 유효하지 않아 save!
메서드가 오류를 발생시키는 경우입니다.
PictureFile.transaction do picture_file_1.destroy picture_file_2.save! end
after_commit
콜백을 사용하면 이 경우를 처리할 수 있습니다.
class PictureFile < ApplicationRecord after_commit :delete_picture_file_from_disk, on: :destroy def delete_picture_file_from_disk if File.exist?(filepath) File.delete(filepath) end end end
참고: :on
옵션은 콜백이 실행되는 시기를 지정합니다. :on
옵션을 제공하지 않으면 콜백이 모든 작업에 대해 실행됩니다.
경고. 트랜잭션이 완료되면 해당 트랜잭션 내에서 생성, 업데이트 또는 삭제된 모든 모델에 대해 after_commit
또는 after_rollback
콜백이 호출됩니다. 그러나 이러한 콜백 중 하나에서 예외가 발생하면 예외가 버블링되어 실행되지 않은 다른 after_commit
또는 after_rollback
메서드가 있을 수 있습니다. 따라서 콜백 코드에서 예외가 발생할 수 있는 경우 콜백 내에서 이를 처리하고 처리해야 다른 콜백이 실행될 수 있습니다.
경고. 단일 트랜잭션 컨텍스트에서 데이터베이스의 동일한 레코드를 나타내는 여러 로드된 객체와 상호 작용하는 경우 after_commit
및 after_rollback
콜백의 중요한 동작을 유의해야 합니다. 이러한 콜백은 트랜잭션 내에서 특정 레코드에 대한 변경이 발생한 첫 번째 객체에 대해서만 트리거됩니다. 동일한 데이터베이스 레코드를 나타내는 다른 로드된 객체의 경우 해당 after_commit
또는 after_rollback
콜백이 트리거되지 않습니다. 이러한 미묘한 동작은 동일한 데이터베이스 레코드와 연결된 각 객체에 대해 독립적인 콜백 실행을 기대하는 시나리오에서 특히 중요합니다. 이는 트랜잭션 후 콜백 시퀀스의 흐름과 예측 가능성에 영향을 미칠 수 있으며, 애플리케이션 로직의 잠재적인 불일치로 이어질 수 있습니다.
after_commit
의 별칭
생성, 업데이트 또는 삭제 시에만 after_commit
콜백을 사용하는 것이 일반적이므로 해당 작업에 대한 별칭이 있습니다:
class PictureFile < ApplicationRecord after_destroy_commit :delete_picture_file_from_disk def delete_picture_file_from_disk if File.exist?(filepath) File.delete(filepath) end end end
경고. after_create_commit
과 after_update_commit
을 동일한 메서드 이름으로 사용하면 마지막으로 정의된 콜백만 적용됩니다. 내부적으로 after_commit
을 별칭으로 사용하기 때문입니다.
class User < ApplicationRecord after_create_commit :log_user_saved_to_db after_update_commit :log_user_saved_to_db private def log_user_saved_to_db puts 'User was saved to database' end end
irb> @user = User.create # 아무것도 출력되지 않음 irb> @user.save # @user 업데이트 User was saved to database
after_save_commit
또한 after_save_commit
이라는 별칭이 있는데, 이는 생성과 업데이트 모두에 대한 after_commit
콜백을 의미합니다:
class User < ApplicationRecord after_save_commit :log_user_saved_to_db private def log_user_saved_to_db puts 'User was saved to database' end end
irb> @user = User.create # User 생성 User was saved to database irb> @user.save # @user 업데이트 User was saved to database
트랜잭션 콜백 순서
기본적으로 콜백은 정의된 순서대로 실행됩니다. 그러나 after_commit
, after_rollback
등의 다중 트랜잭션 after_
콜백을 정의할 때는 정의된 순서와 반대로 실행될 수 있습니다.
class User < ActiveRecord::Base after_commit { puts("this actually gets called second") } after_commit { puts("this actually gets called first") } end
참고: 이는 after_destroy_commit
등의 모든 after_*_commit
변형에도 적용됩니다.
이 순서는 구성을 통해 설정할 수 있습니다:
config.active_record.run_after_transaction_callbacks_in_order_defined = false
true
(Rails 7.1부터 기본값)로 설정하면 콜백이 정의된 순서대로 실행됩니다. false
로 설정하면 위의 예제와 같이 역순으로 실행됩니다.