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;
        });
    }
CODE


하지만 이 방식은 모델마다 옵저버를 등록하므로 모델이 많아질 경우 어느 모델이 observer 를 사용하는지 알기가 어렵고 관리가 용이하지 않은 단점이 있습니다.

Observer class 생성

laravel 에서는 옵저버 전용 클래스를 제공하며 make:observer artisan 명령으로 옵저버를 생성할 수 있습니다. 예로 다음 명령은 Post 모델의 event 를 받는 PostObserver 를 생성합니다.

옵저버 생성

php artisan make:observer PostObserver --model=Post
BASH


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)
    {
        //
    }
}

PHP


발생시 특정 처리가 필요한 이벤트 메서드에 처리할 로직을 구현해 주면 되며 사용하지 않는 이벤트 메서드는 삭제하면 됩니다.


예로 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);
    }
CODE


Observer 등록

Observer Class 를 생성하면 laravel 에 옵저버를 알려주어야 모델 이벤트를 통지 받을 수 있습니다.


Eloquent model 에는 observe() 메서드가 있으므로 AppServiceProvider 에서 관찰 대상 클래스와 관찰자 클래스를 등록해 주면 됩니다.


App\Providers\AppServiceProvider

    public function boot()
    {
        // post 모델 이벤트 발생시 PostObserver 에 통지
        Post ::observe(PostObserver::class);
    }
PHP


사례

작성자가 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 
CODE



그래서 삭제전에 댓글이 있는지 확인해서 사용자가 인지할 수 있는 에러 메시지를 전달하는 게 좋습니다.

옵저버 클래스에 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("본인이 작성한 게시글만 삭제할 수 있습니다.");
		}
    }
CODE


Ref