이번 챕터의 코드는 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
를 입력받도록 메소드를 수정합니다.
이제 PostEntity
의 to_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.) 엔티티들의 실행 순서와 무결성을 제어하는 비즈니스 흐름 레이어입니다.
$_POST
등의 슈퍼 글로벌 변수에 직접 접근하면 안됩니다.단어로만 설명하면 어렵게 느껴질 수도 있으니 코드로 확인하는 편이 더 확실할 것 같습니다.
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_data
는 HTTP POST
에서 입력받은 글 정보를 나타냅니다.
$member_id
는 현재 로그인한 사용자의 주 키입니다. 서비스의 메소드는 오로지 비즈니스 흐름의 제어에 그 목적이 있으므로 로그인 여부 등의 HTTP 수준 유효성 검사는 컨트롤러에서 하게 됩니다. 따라서 create
메소드가 실행되었다는 것은 반드시 로그인한 사람이라는 전제가 있다는 뜻입니다.
동일한 이유로 직접 LoginHelper::memberId()
로 메소드 안에서 값을 가져오는 것이 아니라 파라미터로 사용자 주 키를 입력받았습니다.
(3) 글 엔티티 객체를 선언합니다.
(4) 글 엔티티 객체에 글 정보를 채웁(fill
)니다.
(5) HTTP에서 전달받은 글 정보 $post_data
에는 사용자에 대한 정보가 없으므로 파라미터로 입력받은 $memberId
를 $postEntity
의 author
속성에 대입합니다.
(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)
)이 보장됩니다. 곧바로 사용할 수 있도록 리턴합니다.
서비스 레이어! 저는 만들다보니까 위와 같이 비슷한게 필요해서 리사이클이라고 폴더 만들어 쓰긴했는데요
엔티티를 재활용해서 쓴다는 의미로 한건데~ 서비스레이어로 해야겠군요~ 참고할게요!