액티브 레코드 콜백

이 가이드는 액티브 레코드 객체의 생명 주기에 연결하는 방법을 알려줍니다.

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

  • 액티브 레코드 객체의 생명 주기 동안 발생하는 특정 이벤트
  • 객체 생명 주기의 이벤트에 응답하는 콜백 메서드를 만드는 방법
  • 콜백에 대한 공통 동작을 캡슐화하는 특수 클래스를 만드는 방법

객체 생명 주기

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")할 수 있습니다.

사용 가능한 콜백

다음은 액티브 레코드 콜백의 전체 목록으로, 해당 작업 중에 호출되는 순서대로 나열되어 있습니다:

객체 생성

객체 업데이트

경고. after_save는 생성과 업데이트 모두에서 실행되지만 항상 더 구체적인 콜백 after_createafter_update 이후에 실행됩니다. 매크로 호출 순서와 상관없습니다.

객체 삭제

참고: before_destroy 콜백은 dependent: :destroy 연관관계 앞에 배치해야 합니다(또는 prepend: true 옵션을 사용해야 합니다). 이렇게 하면 레코드가 dependent: :destroy에 의해 삭제되기 전에 실행됩니다.

경고. after_commitafter_save, after_updateafter_destroy와 매우 다른 보장을 합니다. 예를 들어 after_save에서 예외가 발생하면 트랜잭션이 롤백되고 데이터가 영구적으로 저장되지 않습니다. 반면 after_commit에서 발생하는 모든 것은 트랜잭션이 이미 완료되고 데이터가 데이터베이스에 영구적으로 저장되었음을 보장합니다. 트랜잭션 콜백에 대해 자세히 알아보세요.

after_initializeafter_find

액티브 레코드 객체가 인스턴스화될 때마다 after_initialize 콜백이 호출됩니다. 이는 new를 직접 사용하거나 레코드가 데이터베이스에서 로드될 때 발생합니다. 액티브 레코드 initialize 메서드를 직접 재정의할 필요가 없게 해줍니다.

데이터베이스에서 레코드를 로드할 때 after_find 콜백이 호출됩니다. after_findafter_initialize가 정의된 경우 after_initialize 전에 호출됩니다.

참고: after_initializeafter_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_*! 메서드는 각 속성에 대해 자동으로 생성되는 동적 파인더입니다. 동적 파인더 섹션에서 자세히 알아보세요.

콜백 건너뛰기

유효성 검사와 마찬가지로 다음 메서드를 사용하여 콜백을 건너뛸 수 있습니다:

그러나 이러한 메서드는 주의해서 사용해야 합니다. 중요한 비즈니스 규칙과 애플리케이션 로직이 콜백에 포함되어 있을 수 있기 때문입니다. 잠재적인 영향을 이해하지 않고 이를 우회하면 잘못된 데이터가 발생할 수 있습니다. 자세한 내용은 메서드 문서를 참조하세요.

실행 중단

새 콜백을 모델에 등록하면 실행 대기열에 추가됩니다. 이 대기열에는 모든 모델 유효성 검사, 등록된 콜백 및 실행될 데이터베이스 작업이 포함됩니다.

전체 콜백 체인은 트랜잭션으로 래핑됩니다. 콜백 중 하나라도 예외를 발생시키면 실행 체인이 중단되고 ROLLBACK이 발행됩니다. 의도적으로 체인을 중단하려면 다음을 사용하세요:

throw :abort

경고. ActiveRecord::Rollback 또는 ActiveRecord::RecordInvalid 이외의 예외가 발생하면 Rails에 의해 다시 발생됩니다. 또한 saveupdate(일반적으로 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:unlessProc과 함께 사용하기

:if:unlessProc 객체와 연결할 수 있습니다. 이 옵션은 짧은 유효성 검사 메서드, 일반적으로 한 줄 정도의 메서드를 작성할 때 가장 적합합니다:

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_commitafter_rollback

데이터베이스 트랜잭션 완료 시 트리거되는 두 가지 추가 콜백이 있습니다: after_commitafter_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_commitafter_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_commitafter_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로 설정하면 위의 예제와 같이 역순으로 실행됩니다.