Laravel Eloquent Model Event 로 옵저버 패턴(Observe pattern) 사용하기
Observer Pattern 이란
옵서버 패턴(observer pattern)은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다. 주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다. 발행/구독 모델로 알려져 있기도 하다. - 출처 wikipedia
옵저버 패턴은 객체의 상태 변화를 등록한 관찰자에게 알려주는 패턴으로 데이터 변경이 발생할 때 여러 객체에게 통지할 수 있습니다.
라라벨은 Eloquent Model 에 옵저버를 등록하고 변경 사항(생성, 수정, 삭제, 조회등)이 발생할 경우 등록한 옵저버에서 특정 작업을 수행하게 할 수 있으며 다음과 같은 상황에 대해 통지받을 수 있습니다.
- retrieved : DB 에서 레코드를 가져온 후에
- creating : 레코드를 DB 에 insert 하기 전
- created : 레코드를 DB 에 insert 한 후
- updating : 레코드를 DB 에 update 하기 전
- updated : 레코드를 DB 에 update 한 후
- saving : 레코드를 DB 에 저장하기 전(created 와 updated 모두 해당).
- saved : 레코드를 DB 에 저장한 후(created 와 updated 모두 해당).
- deleting : 레코드를 DB 에서 삭제하기 전(soft delete 와 delete 모두 해당).
- deleted : 레코드를 DB 에서 삭제한 후(soft delete 와 delete 모두 해당).
- restoring : Soft delete 한 레코드를 DB 에서 복구하기 전
- restored : Soft delete 한 레코드를 DB 에서 복구한 후
Eloquent Model 에 메서드 작성
개별 모델 클래스에 처리하려는 이벤트 이름의 메서드를 작성하고 boot() 에 등록해 주면 됩니다. 예로 포스트 작성시 글쓴이의 id(writer_id 필드) 를 자동으로 설정하려면 아래와 같이 boot() 메서드에 creating() 을 구현해 주면 됩니다.
class Post { protected static function boot() { parent::boot(); // post 생성시 글쓴 이의 user_id 로 설정 static::creating(function ($model) { $model->writer_id = \Auth::user()->id; }); }
하지만 이 방식은 모델마다 옵저버를 등록하므로 모델이 많아질 경우 어느 모델이 observer 를 사용하는지 알기가 어렵고 관리가 용이하지 않은 단점이 있습니다.
Observer class 생성
laravel 에서는 옵저버 전용 클래스를 제공하며 make:observer artisan 명령으로 옵저버를 생성할 수 있습니다. 예로 다음 명령은 Post 모델의 event 를 받는 PostObserver 를 생성합니다.
php artisan make:observer PostObserver --model=Post
App\Observers namespace 밑에 PostObserver 클래스가 생기며 아래와 같은 skeleton code 를 볼 수 있습니다.
<?php namespace App\Observers; use App\Post; class PostObserver { /** * Handle the post "created" event. * * @param \App\Post $post * @return void */ public function created(Post $post) { // } /** * Handle the post "updated" event. * * @param \App\Post $post * @return void */ public function updated(Post $post) { // } /** * Handle the post "deleted" event. * * @param \App\Post $post * @return void */ public function deleted(Post $post) { // } /** * Handle the post "restored" event. * * @param \App\Post $post * @return void */ public function restored(Post $post) { // } /** * Handle the post "force deleted" event. * * @param \App\Post $post * @return void */ public function forceDeleted(Post $post) { // } }
발생시 특정 처리가 필요한 이벤트 메서드에 처리할 로직을 구현해 주면 되며 사용하지 않는 이벤트 메서드는 삭제하면 됩니다.
예로 restoring 이벤트를 처리하는 로직을 구현할 경우 아래와 같은 로직을 작성해 주면 됩니다.
<?php namespace App\Observers; use App\Post; class PostObserver { /** * Handle the post "restoring" event. * * @param \App\Post $post * @return void */ public function restoring(Post $post) { \Log::info("restoring " . $post->id); }
Observer 등록
Observer Class 를 생성하면 laravel 에 옵저버를 알려주어야 모델 이벤트를 통지 받을 수 있습니다.
Eloquent model 에는 observe() 메서드가 있으므로 AppServiceProvider 에서 관찰 대상 클래스와 관찰자 클래스를 등록해 주면 됩니다.
public function boot() { // post 모델 이벤트 발생시 PostObserver 에 통지 Post ::observe(PostObserver::class); }
사례
작성자가 post 를 삭제하려고 할 경우 해당 post 에 댓글이 있을 경우 삭제 불가가 서비스의 정책이라고 가정해 봅시다.
DBMS 의 foreign key 를 사용해서 Comment 모델이 Post 모델을 참조하게 했을 경우 cascading option 이 옵션을 주지 않았다면 DBMS 차원에서 삭제가 불가능합니다.
하지만 이런 경우 다음과 같은 DBMS 차원의 에러가 발생하므로 삭제를 수행한 사용자는 원인이 무엇인지 혼란스러워 할 수 있습니다.
Illuminate/Database/QueryException with message 'SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails
그래서 삭제전에 댓글이 있는지 확인해서 사용자가 인지할 수 있는 에러 메시지를 전달하는 게 좋습니다.
옵저버 클래스에 deleting() 메서드를 아래와 같이 구현하면 사용자는 댓글있는 게시글 삭제시 이해할 수 있는 에러 메시지를 만날 수 있습니다.
class PostObserver { /** * Handle the post "deleting" event. * * @param \App\Post $post * @return void */ public function deleting(Post $post) { $count = $post->comments()->count(); if ($count > 0) { throw new DataIntegrityException("댓글이 달려있는 게시글은 삭제할 수 없습니다."); } $user = \Auth::user(); if ($user->id != $post_writer_id) { throw new NotPermittedException("본인이 작성한 게시글만 삭제할 수 있습니다."); } }
Ref
- https://laravel.com/docs/7.x/eloquent#observers
- https://www.larashout.com/how-to-use-laravel-model-observers