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() 을 구현해 주면 됩니다.

model class
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 를 볼 수 있습니다.

App/Observers/PostObserver
<?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 에서 관찰 대상 클래스와 관찰자 클래스를 등록해 주면 됩니다.


App\Providers\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