php/코드이그나이터4(CI4)

코드이그나이터3(CI3)에서 CI4로! 프로젝트 전환 후기 1 (feat. 서비스 패턴)

현박이 2025. 7. 4. 13:46
반응형

지금까지 CI3 를 사용하다가 CI4 로 바꾸자고 얘기하면서 드디어 CI4로 바꾸게 되었다.

CI3에 익숙했던 팀원들을 설득하는 과정은 쉽지 않았지만, CI4가 가진 현대적인 개발 방식과 구조적 이점을 생각하면 반드시 가야 할 길이라고 생각했다.

새 프로젝트를 진행하면서 CI3와 CI4를 다루면서 느끼는 차이점을 적을 예정이다.

 

1. 개발 환경: PHP 7.4와 CI 4.4

일단 CI4는 PHP8 이상의 서버 환경을 추천하지만 환경상 PHP7.4 로 진행해야했기에

최신버전의 CI4 4.6버전이 아닌 CI4 4.4 버전으로 낮춰서 진행했다.


2. 사소하지만 중요한 변화: 코드 스타일의 현대화

가장 먼저 팀원들과 합의한 것은 코드 스타일의 통일이다.

  • 기존 CI3 방식: 스네이크 케이스 (test_code, get_user_data())
  • 새로운 CI4 방식: 카멜 케이스 (testCode, getUserData())

물론 이것은 프레임워크의 강제 사항은 아니다. 현대적인 PHP 코딩 표준(PSR)을 따르고, 다른 최신 프레임워크와의 일관성을 유지하기 위해 카멜 케이스를 표준으로 정했다. 사소해 보이지만, 코드의 가독성과 통일성을 높이는 중요한 첫걸음이다.


3. 라우터(Router) 사용의 의무화

솔직히 고백하자면, 이전 CI3 프로젝트에서는 편의를 위해 라우터를 거의 사용하지 않았다. URL이 도메인/컨트롤러/메서드 구조로 바로 매핑되는 방식이 직관적이라고 생각했기 때문이다.

하지만 CI4 프로젝트에서는 모든 요청을 라우터를 통해 처리하도록 규칙을 정했다.

// app/Config/Routes.php
$routes->get('/sample/sampleview', 'Sample::sampleView');
$routes->post('/sample/postsample', 'Sample::postSample');
$routes->post('/sample/insertsample', 'Sample::insertSample');
$routes->post('/sample/updatesample/(:num)', 'Sample::updateSample/$1');
$routes->get('/sample/sampleviewalias', 'Sample::sampleViewAlias', ['as' => 'sample.ali']);

이런식으로 앞에 get, post, patch, delete 등 원하는 form 전송 타입을 지정하여 받도록 만들었다.

 

매번 URL을 지정하는게 불편하긴해도 난 이게 좋은것같다.

협업하기도 편하고 한 눈에 보기 편하다.

추가로 컨트롤러나 메서드명을 변경하지 않고도 외부에 노출되는 URL을 자유롭게 변경할 수 있다.


4. MVC를 넘어 MVC+S (Service) 패턴으로

이번 전환에서 가장 만족스럽고, 가장 강력하게 추천하는 변화는 바로 서비스(Service) 레이어의 도입이다.

기존에 하던 ci3에서는 MVC패턴에서 그쳤지만나는 MVC패턴에 SERVICE 까지 추가하였다.

이점이 나는 제일 만족스러웠다.

물론 내가 주도자라서 내 개인의견으로 밀어붙이고 있는거지만 아주 잘했다고 생각한다.

기존 CI3의 문제점: 뚱뚱한 컨트롤러 (Fat Controller)

기존 CI3의 MVC 패턴에서는 컨트롤러가 너무 많은 역할을 담당했다.

  • 사용자 요청(Request) 처리
  • 데이터 유효성 검사
  • 모델을 호출하여 데이터 가져오기
  • 가져온 데이터 비즈니스 로직에 맞게 가공하기
  • 뷰(View) 호출 및 데이터 전달

이 모든 것을 컨트롤러가 처리하다 보니, 코드가 길어지고 복잡해져 유지보수가 어려워지는 '뚱뚱한 컨트롤러' 문제가 발생하기 쉬웠다.

새로운 해결책: 책임의 분리, 서비스 레이어

그래서 CI4 프로젝트에서는 MVC 패턴에 서비스(Service) 레이어를 추가하여 각 부분의 책임을 명확히 분리했다.

  • 컨트롤러 (Controller): 오직 HTTP 요청과 응답에만 집중한다. 필요한 데이터를 서비스에 요청하고, 서비스로부터 받은 결과를 뷰나 JSON 형태로 반환하는 역할만 수행한다.
  • 서비스 (Service): 모든 비즈니스 로직을 담당한다. 데이터 가공, 외부 API 통신, 트랜잭션 처리 등 핵심적인 로직은 모두 서비스에 존재한다. 모델을 호출하여 DB 데이터를 가져오는 역할도 서비스가 담당한다.
  • 모델 (Model): 데이터베이스 작업(CRUD)에만 집중한다.

아래는 팀원 교육을 위해 작성한 Sample 컨트롤러이다

PHP
<?php

namespace App\Controllers;

// namespace App\Controllers; 가 아닐시 아래 주석 필요 (controller를 폴더별로 나누는경우 ex) App\Controllers\Admin )
// use App\Controllers\BaseController;
class Sample extends BaseController
{
    public function index(): string
    {
        return view('welcome_message');
    }

    // 화면 출력 샘플
    public function sampleView(){
        // 1. 서비스 객체 생성
        $sampleService = service('sampleService');
        $testGet = $this->request->getGet('testGet');
        
        // 2. 서비스에 비즈니스 로직 처리 위임
        $returnData = $sampleService->testPrint($testGet);

        // 3. 컨트롤러는 데이터를 뷰에 전달하는 역할만 수행
        $data = [
            'title' => 'Sample View',
            'testStr' => $returnData,
        ];
        return view('sample/sample_view', $data);
    }

    // Get 샘플
    public function getSample(){

        $testGet = $this->request->getGet('testGet');
        /*
         * false 명시 하게되면 새로운 객체로됨
         * 예시로 getSample과 postSample 에서 각각 service('sampleService') 호출해도 sampleService 속 데이터는 유지되는 상태로 호출됨
         * */
        //$sampleService = service('sampleService', false);
        $sampleService = service('sampleService');
        $data = [
            'title'      => 'test',
            'testStr' => $sampleService->testPrint($testGet),
        ];
        return $data;
    }

    // POST, JSON 샘플
    public function postSample(){

        $testJson = $this->request->getJSON();
        $testData = $testJson->testData ?? null;

        $sampleService = service('sampleService');
        $returnData = $sampleService->postSampleDB();
        $data = [
            'title'      => 'test',
            'testStr' => $returnData,
        ];
        // 사용 가능 여부를 JSON으로 응답
        return $this->response->setJSON($data);

        // javascript에서 location.href 역할, with는 일회성 alert
        //return redirect()->to('/posts/' . $newPostId)->with('success', '게시글이 성공적으로 등록되었습니다.');
    }

    // INSERT 샘플
    public function insertSample(){
        $sampleService = service('sampleService');
        if($sampleService->insSampleDB()){
            $result = [
                'status'      => true,
                'msg' => 'insert성공'
            ];
        } else{
            $result = [
                'status'      => false,
                'msg' => 'insert실패'
            ];
        }
        return $this->response->setJSON($result);
    }

    // Update 샘플
    public function updateSample($f_id){
        $sampleService = service('sampleService');
        $jsonData = $this->request->getJSON(true);  //object->array 로 변환됨

        if($f_id == null){
            $result = [
                'status'      => false,
                'msg' => 'upd실패, id없음'
            ];
            return $this->response->setJSON($result);
        }

        if($sampleService->updSampleDB($f_id, $jsonData)){
            $result = [
                'status'      => true,
                'msg' => 'upd성공'
            ];
        } else{
            $result = [
                'status'      => false,
                'msg' => 'upd실패'
            ];
        }
        return $this->response->setJSON($result);
    }

    // delete 샘플
    public function deleteSample($f_id){
        $sampleService = service('sampleService');
        if($f_id == null){
            $result = [
                'status'      => false,
                'msg' => 'del실패, id없음'
            ];
            return $this->response->setJSON($result);
        }

        if($sampleService->deleteSampleData($f_id)){
            $result = [
                'status'      => true,
                'msg' => 'del성공'
            ];
        } else{
            $result = [
                'status'      => false,
                'msg' => 'del실패'
            ];
        }
        return $this->response->setJSON($result);
    }


}

 

보는 바와 같이, 컨트롤러 코드가 놀랍도록 간결하고 깔끔해졌다.

복잡한 비즈니스 로직은 sampleService 안에 숨어있기 때문에, 컨트롤러는 자신의 역할인 '연결'에만 충실할 수 있다. 이렇게 코드를 분리하니 흐름 파악이 쉽고 유지보수가 용이해지는 엄청난 장점을 얻었다.

CI4의 service() 헬퍼 함수는 기본적으로 싱글톤(Singleton) 패턴으로 동작한다. 즉, 한 요청 내에서 여러 번 호출해도 동일한 서비스 객체를 반환하여 상태를 유지하므로 매우 효율적이다.(당연히 새로운 객체로도 선언해줄수있다)

 

예시에서 컨트롤러는 분기처리와 VIEW에 리턴하는 부분만 남겨두었다.

코드 자체를 따로 떼어 분리만 해놔도 흐름 파악은 어렵지 않았다.(단, 파일을 2개 열어서 보는게 불편하다면 어쩔수없다)

 

이외에 서비스선언(싱글톤패턴), 쿼리 빌더, VIEW 출력 등의 부분이 더 있지만 이건 조금 더 진행하고 정리해야겠다.

반응형