코드이그나이터4 데이터베이스 다루기 - 7 - 엔티티 다루기

koeunyeon·2021년 4월 2일
0

엔티티 다루기

엔티티의 정의

엔티티는 비즈니스 로직을 다루는 방법 중 하나입니다. 데이터베이스 테이블 한개의 행에 해당하는 객체로 행의 정보 중 비즈니스 로직을 처리하는 부분을 가져와서 분리하는 것입니다.
코드이그나이터4에서 엔티티는 필수는 아닙니다. 반드시 사용할 필요는 없지만, 개발 패턴을 익히는 것도 나쁘지 않으리라 생각합니다.

이번 예제에서는 회원 엔티티를 만들어서 사용하는 법을 연습해 보겠습니다.

코드는 https://github.com/koeunyeon/ci4/tree/model-entity 에서 볼 수 있습니다.

마이그레이션 실행하기

터미널에서 sampleEntity 마이그레이션 파일을 생성합니다.

php spark migrate:create sampleEntity

app/Database/Migrations 디렉토리 아래 sampleEntity 마이그레이션 파일을 아래와 같이 수정합니다.
사용하는 열은 주 키인 sample_entity_id, 이름을 뜻하는 person_name, 나이가 저장되는 age, 비밀번호를 저장하는 login_pw입니다. 마지막의 login_pw 열은 암호화되어 저장되기 때문에 길이를 256으로 저장했습니다.

<?php namespace App\Database\Migrations;

use CodeIgniter\Database\Migration;

class SampleEntity extends Migration
{
    public function up()
    {
        $this->forge->addField([
            'sample_entity_id'          => [
                'type'           => 'BIGINT',
                'unsigned'       => true,
                'auto_increment' => true,
            ],
            'person_name'       => [
                'type'       => 'VARCHAR',
                'constraint' => '40',
            ],
            'age' => [
                'type' => 'INT',
                'unsigned'       => true,
            ],
            'login_pw' => [
                'type' => 'VARCHAR',
                'constraint' => '256'
            ]

        ]);
        $this->forge->addKey('sample_entity_id', true);
        $this->forge->createTable('sample_entity');
    }

    //--------------------------------------------------------------------

    public function down()
    {
        $this->forge->dropTable('sample_entity');
    }
}

터미널에서 마이그레이션 파일로 데이터베이스를 생성합니다.

php spark migrate

엔티티 만들기

엔티티는 필수가 아니므로 기본 프로젝트 구조에 Entities 디렉토리가 없습니다. 따라서 apps 디렉토리 아래에 Entities 디렉토리를 생성합니다. 그리고 apps/Entities/SampleEntity.php 클래스를 만듭니다.

<?php

namespace App\Entities;

class SampleEntity extends \CodeIgniter\Entity // (1)
{    
    protected $datamap = [ // (2)
        'name' => 'person_name'
    ];
    
    public function setLoginPw($login_pw) // (3)
    {
        $this->attributes['login_pw'] = password_hash($login_pw, PASSWORD_BCRYPT); // (4)
        $this->attributes['origin_login_pw'] = $login_pw; // (5)
    }
    
    public function getAge(){ // (6)
        if ($this->attributes['age'] >= 20){
            return "adult";
        }
        if ($this->attributes['age'] >= 10){
            return "student";
        }
        if ($this->attributes['age'] >= 5){
            return "kids";
        }
        return "baby";
    }
    
    public function checkUser($origin_password): bool // (7)
    {
        $hashed_password = $this->attributes['login_pw']; // (8)
        return password_verify($origin_password, $hashed_password);
    }
    
    public function getFullData(){ // (9)
        return $this->attributes['person_name'] . " " . $this->attributes['age'] . " "; // (10)
    }
}

코드를 확인해 보겠습니다.
(1) 코드이그나이터4의 엔티티는 \CodeIgniter\Entity 클래스를 상속합니다.

(2) $dataMap 멤버 변수는 "PHP에서 사용할 이름과 데이터베이스 열의 이름이 다를 경우 서로를 매핑시키는 역할"을 합니다. 예제에서 보면 'name' => 'person_name' 연관배열이 있는데, 이것은 PHP에서는 name이라고 사용하지만 데이터베이스의 열 이름은 person_name이라는 뜻입니다.
개발이 한참 진행되었는데 데이터베이스의 열 이름이 바뀌었을 경우나 오라클처럼 컬럼 길이의 제한이 있는 DBMS를 사용할 때 의미를 더 명확하게 쓰기 위해서 사용할 수 있습니다.

(3) 속성 자동변환을 위한 뮤테이터(mutator) 입니다.
만약 $sampleEntity->login_pw = 'asdf' 처럼 엔티티의 login_pw 속성을 변경하는 구문이 실행되면 엔티티 클래스는 자동으로 setLoginPw 함수를 호출합니다. 이를 이용해서 엔티티를 데이터베이스에 저장하기 전이나, 엔티티에 특정 값을 설정해야 할 때 사용할 수 있습니다.
뮤테이터는 set + PascalCase로 작성되어야 합니다.
혹시나 자바를 사용해 본 적이 있으시다면 자바 빈즈 규칙 중 setter를 떠올리기 쉬울 텐데, setter와 비슷하지만 조금 다릅니다. 일반적으로 자바 빈즈의 setterprotected로 선언된 멤버 변수를 외부에서 설정하기 위해 public으로 바꾸는 역할을 하는데 반해, 코드이그나이터의 뮤테이터는 내부 속성을 다른 형태로 바꾸기 위해 사용되는 경우가 많습니다. 코드이그나이터4 엔티티의 경우 멤버 변수를 직접 외부에서 접근이 가능하기 때문에 접근제한자를 변경하는 역할은 하지 않기 때문입니다. 물론 자바 빈즈의 setter도 내부 값을 변경하거나 혹은 가상의 속성을 만드는 역할로도 쓰이므로 코드이그나이터4의 엔티티 뮤테이터와 완전히 다른 것은 아닙니다.

(4) 비밀번호를 BCRYPT로 암호화합니다. 엔티티 내부의 데이터는 attributes라는 연관배열로 관리되므로 암호화된 $login_pw 값을 $this->attributes['login_pw']에 설정하면 클래스 외부에서는 암호화된 비밀번호를 반환받게 됩니다.

(5) 원본 비밀번호를 따로 저장할 수 있습니다. attributes 멤버변수는 미리 설정되어 있지 않은 키도 받아들이므로 유연하게 사용 가능합니다.

(6) 속성의 원본값과 무관하게 가공된 값을 리턴하고 싶을 경우 액세서(accessor)를 사용할 수 있습니다. 예시에서는 입력한 나이 대신 나이 기준에 따른 문자열을 리턴하도록 작성해 보았습니다.
액세서는set + PascalCase로 작성되어야 합니다.

(7) 엔티티의 가장 큰 목적은 비즈니스 로직을 관리하는 것입니다. 회원 비밀번호 검증 여부 비즈니스 로직을 생각해 보겠습니다. 회원 한 명을 뜻하는 데이터베이스 한 행, 즉 엔티티의 입장에서 보면 특정 문자열을 입력받아서 엔티티가 가지고 있는 비밀번호와 검증함으로써 비밀번호가 올바른지 확인할 수 있습니다.

(8) 비즈니스 로직을 시행하기 위해 내부의 속성에 접근해 봅니다. 데이터베이스에서 가져온 login_pw 항목의 경우 이미 (3) 의 뮤테이터에 의해 암호화된 상태로 저장되어 있을 것입니다. 이를 파라미터로 입력받은 원본 문자열과 비교해서 비밀번호가 일치하는지 검증합니다.

(9) 가상 프로퍼티를 만드는 것도 엔티티의 역할 중 하나입니다. getFullData는 서비스에서 사용할 수 있도록 특정 정보를 리턴할 수 있습니다.

(10) 이름의 경우 name이 아니라 person_name으로 접근한 것에 주목해 주세요. $dataMap키=>값'name' => 'person_name' 이었습니다. 즉, 내부 프로퍼티 이름은 person_name이지만 "외부에서 접근할 때"는 name으로 쓴다는 뜻입니다. 반대로 말하면 "내부에서 접근할 때"는 person_name으로 사용해야 합니다.

모델 만들기

엔티티를 사용한 모델을 만들어 보겠습니다. /app/Models/SampleEntityModel.php 클래스를 생성합니다.
/app/Models/SampleEntityModel.php

<?php
namespace App\Models;
use CodeIgniter\Model;

class SampleEntityModel extends Model
{
    protected $table = 'sample_entity';
    protected $allowedFields = ['person_name', 'age', 'login_pw']; // (1)
    protected $primaryKey = "sample_entity_id";

    protected $returnType = "App\Entities\SampleEntity"; // (2)
}

코드를 확인해 보겠습니다.
(1) $allowdFields 변수는 엔티티와 무관하게 실제 데이터베이스의 열 이름 으로 설정합니다. 모델은 이제 저장소의 역할을 하기 때문에 저장소의 정보를 가지고 있어야 합니다.

(2) 코드이그나이터4에서 모델은 반환 타입을 설정할 수 있습니다. 문자열로 설정되기 때문에 네임스페이스를 포함한 전체 경로를 다 적어주어야 합니다.
만약 반환 타입이 설정되지 않으면 부모 클래스의 정의에 따라 배열 타입이 됩니다.

컨트롤러에서 엔티티 사용하기

엔티티를 만들었으니 컨트롤러에서 사용해 보겠습니다.
/app/Controllers/Entity.php 클래스를 생성합니다.
/app/Controllers/Entity.php

<?php
namespace App\Controllers;

use App\Entities\SampleEntity; // (1)
use App\Models\SampleEntityModel;


class Entity extends BaseController
{
    public function regist()
    {
        $sampleEntity = new SampleEntity(); // (2)
        $sampleEntity->fill( // (3)
            $this->request->getPost()
        );

        $sampleEntityModel = new SampleEntityModel(); // (4)
        $last_insert_id = $sampleEntityModel->insert($sampleEntity); // (5)
        $resultEntity = $sampleEntityModel->find($last_insert_id); // (6)

        $check_user_result = $resultEntity->checkUser($this->request->getPost('login_pw')); // (7)

        $full_data = $resultEntity->getFullData(); // (8)

        return $this->response->setJSON([
            'post_data' => $this->request->getPost(),
            'insert_entity' => $sampleEntity,
            'result_entity' => $resultEntity,
            'check_user_result' => $check_user_result,
            'full_data' => $full_data
        ]);
    }
}

코드를 확인해 보겠습니다.
(1) 우리가 만든 SampleEntity를 사용하기 위해 불러옵니다.

(2) 엔티티를 선언합니다. 기존 예제와 다르게 배열이 아닌 엔티티를 사용한다고 생각하면 쉽습니다.

(3) 엔티티는 배열을 파라미터로 전달받아서 내부 프로퍼티로 변경해주는 fill이라는 메소드를 제공합니다. 따라서 요청값인 request->getPost() 를 곧바로 엔티티로 바꿀 수 있습니다.

(4) 사용할 모델을 선언합니다.

(5) 모델을 이용해 데이터베이스에 데이터를 입력합니다. 사용법은 배열과 동일하고 연관배열 데이터 대신 엔티티를 파라미터로 전달하면 됩니다.

(6) 입력한 엔티티와 저장된 엔티티를 비교하기 위해 저장된 결과를 데이터베이스에서 가져오겠습니다. 이 때 반환값 타입은 모델에서 설정한 App\Entities\SampleEntity 타입입니다.
만일 모델에 리턴 타입이 지정되어 있더라도 asArray() 메소드를 사용해 배열과 객체간 캐스팅도 가능합니다. 예를 들어 객체 타입 반환값을 배열로 바꾸는 코드는 아래와 같습니다.

$sampleEntityModel->asArray()->find($last_insert_id);

반대로 배열로 설정되어 있어도 엔티티로 변경하는 것도 asObject 메소드를 통해 가능합니다. 만일 SampleEntityModel->$returnType = 'array' 라면 아래와 같이 사용해서 SampleEntity 타입으로 변경할 수 있습니다.

$sampleEntityModel->asObject("App\Entities\SampleEntity")->find($last_insert_id);

(7) 엔티티의 비즈니스 로직을 호출합니다. 이 때 파라미터는 사용자가 입력한 데이터입니다.

(8) 엔티티의 가상 프로퍼티를 호출합니다.

결과 확인하기

결과는 http client에서 확인하겠습니다.
sample/rest_test 디렉토리 아래에 entity.http 파일을 생성합니다.

# regist
POST http://localhost:8080/entity/regist
Content-Type: application/x-www-form-urlencoded

name=ci4user&age=10&login_pw=asdf

http client를 실행시켜보면 아래와 같은 결과를 보실 수 있습니다.

{
  "post_data": {
    "name": "ci4user",
    "age": "10",
    "login_pw": "asdf"
  },
  "insert_entity": {
    "person_name": "ci4user",
    "age": "student",
    "login_pw": "$2y$10$v3yTnEWw4czfMs4rRkzsiegtYBcINs7I0TtXzDlyfDqXVlB8pw9XK",
    "origin_login_pw": "asdf",
    "name": "ci4user"
  },
  "result_entity": {
    "sample_entity_id": "11",
    "person_name": "ci4user",
    "age": "student",
    "login_pw": "$2y$10$v3yTnEWw4czfMs4rRkzsiegtYBcINs7I0TtXzDlyfDqXVlB8pw9XK",
    "name": "ci4user"
  },
  "check_user_result": true,
  "full_data": "ci4user 10 "
}

post_data 항목은 사용자가 입력한 데이터입니다.

insert_entity 항목은 사용자가 입력한 post_data가 엔티티를 거쳐 데이터베이스에 저장되기 전 데이터입니다. age 항목이 student라는 것에 주목하세요. 이 값은 실제로는 student로 저장되지 않고 10으로 저장되지만, 액세서에 의해서 student로 보여지는 것입니다.

result_entity 항목은 데이터가 저장된 후 다시 한번 조회한 데이터입니다. insert_entity에 비교했을 때 sample_entity_id가 추가된 것을 볼 수 있습니다.

check_user_resulttrue이므로 비즈니스 로직이 잘 실행된다는 것을 알 수 있습니다.

full_data로 가상 프로퍼티를 읽는 것도 가능합니다. 결과값이 ci4user 10이므로 age 항목은 사실 10이지만 보여질 때만 액세서에 의해 student로 보여진다는 것을 확인할 수 있습니다.


만약 http client 없이 리눅스에서 테스트하고 싶으면 curl을 이용하면 됩니다. 대부분의 배포판에는 기본으로 설치되어 있습니다만, 설치되어 있지 않다면 아래의 커맨드로 설치합니다.

apt-get install curl

이제 커맨드를 통해 API를 테스트해봅니다.

curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'name=ci4user&age=10&login_pw=asdf' http://localhost:8080/entity/regist
  • -X 옵션은 http 메소드를 지정합니다.
  • -H 옵션은 헤더입니다.
  • -d 옵션은 본문 파라미터입니다. 만약에 URL의 파라미터를 전달하고 싶다면 --data-urlencode 옵션을 사용할 수 있습니다.

curl을 통한 테스트는 개발 환경이 아닌 스테이지 혹은 운영 서버에서 데이터를 확인해야 할 때 유용하게 사용됩니다.

profile
스타트업에 관심이 많은 10 + n년차 웹 개발자. 자바 스프링 (혹은 부트), 파이썬 플라스크, PHP를 주로 다룹니다.

0개의 댓글