코드이그나이터4 마크다운 블로그 리팩토링 - 2 - 엔티티와 서비스 분리하기

고은연·2021년 11월 3일
1

엔티티 분리하기

이번 챕터의 코드는 https://github.com/koeunyeon/ci4/tree/refacto-entity-service 에 있습니다.

엔티티 분리의 이유

이제껏 우리는 비즈니스 로직을 "컨트롤러"에서 처리했습니다. 데이터를 가공하고 조회하는 일 등이죠.
하지만 원칙론적인 MVC 에서 데이터를 다루는 일은 모델이 합니다. 조금 더 단단한 구조를 만들기 위해 엔티티를 분리해서 비즈니스 로직을 녹이고, 코드이그나이터4의 모델 클래스는 저장소(Repository)로 사용하도록 수정해 보겠습니다.

글 엔티티 파일 생성

글 하나의 정보를 담을 엔티티를 생성합니다. 만약 Entities 디렉토리가 없다면 직접 생성하면 됩니다.
app/Entities/PostEntity.php

<?php

namespace App\Entities;

use Michelf\Markdown;

class PostEntity extends \CodeIgniter\Entity
{
    private function to_markdown($content) // (1)
    {
        $content = str_replace(PHP_EOL, "  " . PHP_EOL, $content);
        return Markdown::defaultTransform($content);
    }

    public function setContent($content) // (2)
    {
        $this->attributes['content'] = $content; // (3)
        $this->attributes['html_content'] = $this->to_markdown($content); // (4)
    }
}

(1) 글을 마크다운으로 변환하는 코드입니다. 우리는 기존에 컨트롤러에서 직접 HTTP POST 값을 변환했었습니다.
app/Controllers/Post.php

private function add_input_markdown(){
    $data = $this->request->getPost();
    if (array_key_exists("content", $data)){
        $content = $data['content'];
        $content = str_replace(PHP_EOL, "  " . PHP_EOL, $content);
        $data['html_content'] = Markdown ::defaultTransform($content);
    }

    return $data;
}

그런데 마크다운 데이터는 말 그대로 데이터의 영역이기 때문에 엔티티로 옮깁니다.
또한 기존에는 직접 $this->request->getPost()를 통해 데이터를 가져왔었는데요. 엔티티로 옮겨가면서 (컨트롤러가 아니므로) 곧바로 HTTP POST 값을 읽을 수 없기도 하고, 읽을 수 있다고 해도 엔티티는 HTTP 레이어와 분리하는 편이 나으므로 파라미터 $content를 입력받도록 메소드를 수정합니다.
이제 PostEntityto_markdown 메소드는 순수한 함수로 동작합니다.

(2) 자동으로 html_content를 만들어내기 위해 엔티티의 뮤테이터를 이용합니다. $postEntity->fill 혹은 $postEntity->content가 호출되는 순간 뮤테이터가 실행되어, 현재 컨텐츠($this->attributes['content'])를 설정하고, html_content도 설정($this->attributes['html_content'])합니다.

(3) 뮤테이터를 설정할 때 주의해야 할 것은 다른 값을 함께 바꾸더라도 현재 값도 설정해줘야 한다는 것입니다. 만약에 (3) 이 없으면 $this->attributes['content']null 입니다. 반드시 뮤테이터에서는 현재 어트리뷰트도 설정해 주세요.

서비스 레이어 분리하기

서비스 레이어란?

서비스 레이어는 a.) HTTP와 무관하게 b.) 엔티티들의 실행 순서와 무결성을 제어하는 비즈니스 흐름 레이어입니다.

  • a.) 가능하면 서비스 레이어는 HTTP와 분리되어야 합니다. HTTP 와 직접적으로 통신하는 것은 컨트롤러가 할 일이고, 서비스는 HTTP에서 컨트롤러가 추출한 데이터를 이용하기만 합니다. 절대 $_POST등의 슈퍼 글로벌 변수에 직접 접근하면 안됩니다.
  • b.) 서비스는 엔티티를 통해서 비즈니스 로직을 실행하고, 결과를 코드이그나이터4의 모델을 이용해 리포지토리에 저장하는 역할을 합니다. 하나의 비즈니스 로직은 여러개의 엔티티에 걸쳐 이루어지는 일이 많으므로 이를 제어하는 역할을 하는 것입니다.

단어로만 설명하면 어렵게 느껴질 수도 있으니 코드로 확인하는 편이 더 확실할 것 같습니다.

서비스 작성하기

PostService 서비스 클래스를 작성합니다. 만약 Services 디렉토리가 없다면 직접 생성하고 진행하면 됩니다.
app/Services/PostService.php

<?php

namespace App\Services;

use App\Entities\PostEntity;
use App\Models\PostsModel;

class PostService  // (1)
{
    public function create($post_data, $memberId)  // (2)
    {
        $postEntity = new PostEntity(); // (3)
        $postEntity->fill($post_data); // (4)
        $postEntity->author = $memberId; // (5)

        $postModel = new PostsModel(); // (6)
        $post_id = $postModel->insert($postEntity); // (7)

        if ($post_id) {
            return [true, $post_id, []]; // (8)
        }

        return [false, null, $postModel->errors()]; // (9)
    }

    private static $postService = null; // (10)

    public static function factory() // (11)
    {
        if (self::$postService === null) { // (12)
            self::$postService = new PostService();
        }

        return self::$postService; // (13)
    }
}

(1) 서비스 클래스를 선언합니다. 서비스 클래스는 어떤 부모 클래스도 상속하지 않은 순수 클래스입니다.

(2) 글을 생성하는 메소드 create를 작성합니다.
파라미터의 $post_dataHTTP POST에서 입력받은 글 정보를 나타냅니다.
$member_id는 현재 로그인한 사용자의 주 키입니다. 서비스의 메소드는 오로지 비즈니스 흐름의 제어에 그 목적이 있으므로 로그인 여부 등의 HTTP 수준 유효성 검사는 컨트롤러에서 하게 됩니다. 따라서 create 메소드가 실행되었다는 것은 반드시 로그인한 사람이라는 전제가 있다는 뜻입니다.
동일한 이유로 직접 LoginHelper::memberId() 로 메소드 안에서 값을 가져오는 것이 아니라 파라미터로 사용자 주 키를 입력받았습니다.

(3) 글 엔티티 객체를 선언합니다.

(4) 글 엔티티 객체에 글 정보를 채웁(fill)니다.

(5) HTTP에서 전달받은 글 정보 $post_data에는 사용자에 대한 정보가 없으므로 파라미터로 입력받은 $memberId$postEntityauthor 속성에 대입합니다.

(6) 글 모델 객체를 생성합니다. 코드이그나이터4의 모델 객체는 lastInsertId 혹은 where상태를 가지는 객체이기 때문에 사용할 때마다 새로 만들어 주어야 합니다.

(7) 모델에 엔티티를 전달함으로서 데이터를 입력합니다.

(8) 데이터 입력이 성공할 경우 리턴값이 배열임을 알 수 있습니다.

return [true, $post_id, []];

첫번째 파라미터는 성공 / 실패 여부, 두번째 파라미터는 글 번호, 세번째 파라미터는 오류 메세지를 나타냅니다. 즉 성공시에는 [성공, 글번호, 오류없음] 형태겠죠.

(9) 데이터 입력이 실패했을 경우는 [실패, 글번호 없음, 오류 메세지] 형태가 됩니다.

(10) 싱글톤 패턴을 구현하기 위한 정적 변수를 선언합니다.
서비스 객체는 "메소드만 있는" 객체입니다. 바꿔 말하면 "상태가 없는 객체"죠. 어플리케이션을 개발할 때 불변 객체는 상태가 없으므로 언제 메소드를 실행시켜도 늘 동일한 결과가 나온다는 것을 보장할 수 있습니다. 마치 전역 변수를 사용하지 않은 PHP의 함수와 동일합니다.
메소드가 늘 같은 입력 파라미터에 같은 결과를 리턴한다면, 여러번 인스턴스를 만드는 대신 한번만 만들어서 재사용하면 어떨까? 라는 아이디어가 싱글톤 객체입니다. 즉, 어플리케이션 어디에서 몇 번 PostService의 인스턴스가 필요하더라도 미리 만들어놓은 인스턴스를 반환한다면 실행 메모리를 아끼거나 속도가 빨라지는 등의 이점이 있습니다.

(11) 팩토리 패턴을 구현하기 위한 정적 메소드를 선언합니다.
팩토리 패턴은 객체를 만드는 디자인 패턴 중 하나입니다. 인스턴스를 생성할 때 무조건 new 클래스명() 형태로 만드는 것이 아니라 따로 인스턴스를 만드는 메소드를 두는 것입니다.
팩토리 패턴이 싱글톤 패턴과 만나면, 객체를 단 한번만 생성하는 정적 메소드를 가지게 됩니다.
PHP에서 팩토리 패턴을 쓰는 이유는 하나가 더 있습니다. 아래의 코드를 한번 볼께요.

new PostService()->create(null,null);

이 PHP 코드는 동작하지 않습니다. 이유는 객체를 생성한 후 곧바로 인스턴스의 메소드 호출이 불가능하기 때문입니다.
반면 아래의 코드는 동작합니다.

PostService::factory()->create(null,null);

즉, 팩토리를 통해 생성된 객체는 곧바로 메소드 체이닝이 가능해집니다.

(12) 싱글톤 인스턴스를 생성하기 위해 $postService 변수가 비어있을 때만 객체를 만듭니다.

(13) 이 시점에서 $postService 객체는 무조건 존재함(isset($service))이 보장됩니다. 곧바로 사용할 수 있도록 리턴합니다.

profile
중년 아저씨. 10 + n년차 백엔드 개발자. 스타트업과 창업, 솔로프리너와 1인 기업에 관심 많아요.

2개의 댓글

comment-user-thumbnail
2021년 12월 10일

서비스 레이어! 저는 만들다보니까 위와 같이 비슷한게 필요해서 리사이클이라고 폴더 만들어 쓰긴했는데요
엔티티를 재활용해서 쓴다는 의미로 한건데~ 서비스레이어로 해야겠군요~ 참고할게요!

1개의 답글