edge
더 많은 정보: rubyonrails.org에서 확인하세요: 더 많은 Ruby on Rails

Active Record 콜백

이 가이드는 Active Record 객체의 라이프 사이클에 훅을 걸 수 있는 방법을 가르칩니다.

이 가이드를 읽고 나면 다음을 알게 될 것입니다:

1 객체 라이프 사이클

Rails 애플리케이션의 정상적인 동작 중에는 객체가 생성, 업데이트 및 삭제될 수 있습니다. Active Record는 이러한 객체 라이프 사이클에 훅을 제공하여 애플리케이션과 데이터를 제어할 수 있게 해줍니다.

콜백을 사용하면 객체의 상태 변경 전후에 로직을 트리거할 수 있습니다.

class Baby < ApplicationRecord
  after_create -> { puts "축하합니다!" }
end
irb> @baby = Baby.create
축하합니다!

보시다시피 많은 라이프 사이클 이벤트가 있으며, 이러한 이벤트 중 어느 것에든지 전, 후 또는 주위에 훅을 걸 수 있습니다.

2 콜백 개요

콜백은 객체의 라이프 사이클의 특정 시점에 호출되는 메소드입니다. 콜백을 사용하면 Active Record 객체가 생성, 저장, 업데이트, 삭제, 유효성 검사 또는 데이터베이스에서 로드될 때마다 실행되는 코드를 작성할 수 있습니다.

2.1 콜백 등록

사용 가능한 콜백을 사용하려면 등록해야 합니다. 콜백을 일반적인 메소드로 구현하고 매크로 스타일의 클래스 메소드를 사용하여 콜백으로 등록할 수 있습니다:

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

또는 콜백에 트리거될 proc를 전달할 수도 있습니다.

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으로 선언하는 것이 좋은 관행입니다. public으로 남겨두면 모델 외부에서 호출될 수 있으며 객체 캡슐화 원칙을 위반할 수 있습니다.

경고. 콜백 내에서 객체에 부작용을 일으키는 update, save 또는 다른 메소드 호출을 피하십시오. 예를 들어 콜백 내에서 update(attribute: "value")를 호출하지 마십시오. 이렇게 하면 모델의 상태가 변경되어 커밋 중 예기치 않은 부작용이 발생할 수 있습니다. 대신 before_create / before_update 또는 이전 콜백에서 값을 직접 할당할 수 있습니다.

3 사용 가능한 콜백

다음은 사용 가능한 모든 Active Record 콜백 목록입니다. 해당 작업 중에 호출되는 순서와 동일한 순서로 나열되어 있습니다:

3.1 객체 생성

3.2 객체 업데이트

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

3.3 객체 삭제

참고: before_destroy 콜백은 dependent: :destroy 연관 관계 앞에 배치해야 합니다(prepend: true 옵션을 사용하거나). 이렇게 하면 dependent: :destroy에 의해 레코드가 삭제되기 전에 실행되도록 보장할 수 있습니다.

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

3.4 after_initializeafter_find

Active Record 객체가 인스턴스화될 때마다 after_initialize 콜백이 호출됩니다. new를 직접 사용하거나 데이터베이스에서 레코드를 로드할 때 호출됩니다. Active Record initialize 메소드를 직접 오버라이드할 필요 없이 사용할 수 있습니다.

데이터베이스에서 레코드를 로드할 때 after_find 콜백이 호출됩니다. 둘 다 정의되어 있는 경우 after_findafter_initialize보다 먼저 호출됩니다.

참고: after_initializeafter_find 콜백에는 before_* 상응하는 콜백이 없습니다.

다른 Active Record 콜백과 마찬가지로 등록할 수 있습니다.

class User < ApplicationRecord
  after_initialize do |user|
    puts "객체를 초기화했습니다!"
  end

  after_find do |user|
    puts "객체를 찾았습니다!"
  end
end
irb> User.new
객체를 초기화했습니다!
=> #<User id: nil>

irb> User.first
객체를 찾았습니다!
객체를 초기화했습니다!
=> #<User id: 1>

3.5 after_touch

after_touch 콜백은 Active Record 객체가 터치될 때마다 호출됩니다.

class User < ApplicationRecord
  after_touch do |user|
    puts "객체를 터치했습니다"
  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
객체를 터치했습니다
=> true

belongs_to와 함께 사용할 수 있습니다:

class Book < ApplicationRecord
  belongs_to :library, touch: true
  after_touch do
    puts '책이 터치되었습니다'
  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 '책/도서관이 터치되었습니다'
    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 # @book.library.touch를 트리거합니다
책이 터치되었습니다
책/도서관이 터치되었습니다
=> true

4 콜백 실행

다음 메소드는 콜백을 트리거합니다:

  • create
  • create!
  • destroy
  • destroy!
  • destroy_all
  • destroy_by
  • save
  • save!
  • save(validate: false)
  • toggle!
  • touch
  • update_attribute
  • update
  • update!
  • valid?

또한, after_find 콜백은 다음 검색 메소드에 의해 트리거됩니다:

  • all
  • first
  • find
  • find_by
  • find_by_*
  • find_by_*!
  • find_by_sql
  • last

after_initialize 콜백은 클래스의 새로운 객체가 초기화될 때마다 트리거됩니다.

참고: find_by_*find_by_*! 메소드는 모든 속성에 대해 자동으로 생성되는 동적 검색기입니다. 동적 검색기 섹션에서 자세히 알아보세요.

5 콜백 건너뛰기

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

  • 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

그러나 이러한 메소드는 중요한 비즈니스 규칙과 애플리케이션 로직이 콜백에 저장될 수 있으므로 주의해서 사용해야 합니다. 잠재적인 영향을 이해하지 않고 우회하는 경우 잘못된 데이터가 발생할 수 있습니다.

6 실행 중단

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

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

throw :abort

경고. ActiveRecord::Rollback 또는 ActiveRecord::RecordInvalid가 아닌 예외는 콜백 체인이 중단된 후 Rails에 의해 다시 발생됩니다. 또한, saveupdate와 같이 (true 또는 false를 반환하려고 시도하는) 예외를 발생시키는 메소드를 예상하지 않는 코드를 중단시킬 수 있습니다.

참고: after_destroy, before_destroy 또는 around_destroy 콜백 내에서 ActiveRecord::RecordNotDestroyed가 발생하면 다시 발생되지 않고 destroy 메소드는 false를 반환합니다.

7 관계형 콜백

콜백은 모델 간의 관계를 통해 작동하며, 관계를 통해 정의할 수도 있습니다. 사용자가 여러 개의 게시물을 가지는 예를 가정해 보겠습니다. 사용자가 삭제되면 사용자의 게시물도 삭제되어야 합니다. User 모델에 Article 모델과의 관계를 통해 after_destroy 콜백을 추가해 보겠습니다:

class User < ApplicationRecord
  has_many :articles, dependent: :destroy
end

class Article < ApplicationRecord
  after_destroy :log_destroy_action

  def log_destroy_action
    puts '게시물이 삭제되었습니다'
  end
end
irb> user = User.first
=> #<User id: 1>
irb> user.articles.create!
=> #<Article id: 1, user_id: 1>
irb> user.destroy
게시물이 삭제되었습니다
=> #<User id: 1>

8 조건부 콜백

유효성 검사와 마찬가지로, 콜백 메소드의 호출을 주어진 조건을 만족하는 경우에만 할 수 있습니다. 이를 위해 :if:unless 옵션을 사용할 수 있으며, 심볼, Proc 또는 배열을 사용할 수 있습니다.

콜백이 호출되어야 하는 조건을 지정하려면 :if 옵션을 사용할 수 있습니다. 콜백이 호출되지 않아야 하는 조건을 지정하려면 :unless 옵션을 사용할 수 있습니다.

8.1 심볼을 사용한 :if:unless 사용

심볼:if:unless 옵션과 연결할 수 있으며, 이는 콜백 앞에서 호출되는 예측 메소드의 이름과 일치합니다.

:if 옵션을 사용할 때, 예측 메소드가 false를 반환하면 콜백은 실행되지 않습니다. :unless 옵션을 사용할 때, 예측 메소드가 true를 반환하면 콜백은 실행되지 않습니다. 이것이 가장 일반적인 옵션입니다.

class Order < ApplicationRecord
  before_save :normalize_card_number, if: :paid_with_card?
end

이 형식의 등록을 사용하면 콜백이 실행되어야 하는지 확인하기 위해 여러 다른 예측을 등록할 수도 있습니다. 이에 대해서는 아래에서 다룰 것입니다.

8.2 Proc를 사용한 :if:unless 사용

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

class Order < ApplicationRecord
  before_save :normalize_card_number,
    if: Proc.new { |order| order.paid_with_card? }
end

Proc는 객체의 컨텍스트에서 평가되므로 다음과 같이 작성할 수도 있습니다.

class Order < ApplicationRecord
  before_save :normalize_card_number, if: Proc.new { paid_with_card? }
end

8.3 여러 개의 콜백 조건 사용

: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

8.4 :if:unless 모두 사용하기

콜백은 :if:unless를 동시에 선언할 수 있습니다.

class Comment < ApplicationRecord
  before_save :filter_content,
    if: Proc.new { forum.parental_control? },
    unless: Proc.new { author.trusted? }
end

콜백은 :if 조건이 모두 true이고 :unless 조건이 모두 false일 때만 실행됩니다.

9 콜백 클래스

작성한 콜백 메소드가 다른 모델에서 재사용할만큼 유용할 수도 있습니다. Active Record는 콜백 메소드를 캡슐화하는 클래스를 생성하여 재사용할 수 있도록 합니다.

다음은 파일 시스템에서 폐기된 파일의 정리를 처리하기 위한 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

콜백 클래스 내에 원하는 만큼 많은 콜백을 선언할 수 있습니다.

10 트랜잭션 콜백

10.1 일관성 유지

데이터베이스 트랜잭션이 완료되면 추가적인 두 개의 콜백이 트리거됩니다: 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 옵션을 제공하지 않으면 콜백은 모든 동작에 대해 발생합니다.

10.2 문맥이 중요합니다

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_commit 또는 after_rollback 콜백이 호출됩니다. 그러나 이러한 콜백 중 하나에서 예외가 발생하면 예외가 전달되고 나머지 after_commit 또는 after_rollback 메소드는 실행되지 않습니다. 따라서 콜백 코드가 예외를 발생시킬 수 있는 경우 콜백 내에서 예외를 처리하고 처리하여 다른 콜백이 실행될 수 있도록해야합니다.

경고. after_commit 또는 after_rollback 콜백 내에서 실행되는 코드 자체는 트랜잭션 내에 포함되지 않습니다.

경고. 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

10.3 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

10.4 트랜잭션 콜백 순서

여러 트랜잭션 after_ 콜백(after_commit, after_rollback 등)을 정의할 때 순서는 정의된 순서와 반대로됩니다.

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 변형에도 적용됩니다.

피드백

이 가이드의 품질을 개선하는 데 도움을 주시기를 권장합니다.

오타나 사실적인 오류를 발견하면 기여해주십시오. 시작하려면 문서 기여 섹션을 읽어보세요.

불완전한 내용이나 최신 정보가 아닌 내용을 발견할 수도 있습니다. 주요한 부분에 누락된 문서를 추가해주세요. Edge 가이드에서 이미 문제가 해결되었는지 확인하세요. 스타일과 규칙은 Ruby on Rails 가이드 지침을 확인하세요.

수정할 내용을 발견했지만 직접 수정할 수 없는 경우 이슈를 열어주세요.

마지막으로, Ruby on Rails 문서에 관한 모든 토론은 공식 Ruby on Rails 포럼에서 환영합니다.