서론
레거시 프로젝트는 종종 오래된 코드베이스와 기술 부채를 포함하고 있어 유지보수와 확장에 어려움을 겪는 경우가 많습니다. 특히, 데이터베이스와의 상호작용을 위해 PDO를 이용한 SQL 쿼리문 작성 방식은 다양한 단점을 가지고 있습니다. 이러한 문제점을 해결하고, 개발 생산성과 코드 품질을 향상시키기 위해 ORM(Object-Relational Mapping) 도입이 필수적입니다. 본 보고서에서는 PDO 기반 SQL 쿼리문 작성의 단점을 분석하고, ORM 도입의 필요성과 그 이점을 체계적으로 설명하겠습니다.
PDO 기반 SQL 쿼리문 작성의 단점
1. 코드의 가독성 및 유지보수성 저하
PDO를 이용한 SQL 쿼리문 작성은 SQL 문법이 직접 코드에 포함되기 때문에, 코드의 가독성이 떨어집니다. 복잡한 쿼리일수록 코드가 길어지고, 이를 이해하고 수정하는 데 많은 시간이 소요됩니다. 또한, SQL 쿼리가 분산되어 있으면, 데이터베이스 구조 변경 시 이를 일일이 찾아 수정해야 하는 비효율적인 상황이 발생합니다.
2. 보안 취약성
직접 SQL 쿼리를 작성하는 방식은 SQL 인젝션 공격에 취약할 수 있습니다. 물론, PDO의 준비된 문(Prepared Statements)을 통해 이를 어느 정도 방지할 수 있지만, 개발자가 항상 올바르게 사용하지 않을 위험이 존재합니다. 실수로 인해 발생할 수 있는 보안 취약점을 완전히 방지하기는 어렵습니다.
3. 재사용성 부족
SQL 쿼리는 특정한 데이터베이스와 밀접하게 결합되어 있어, 재사용성이 떨어집니다. 동일한 데이터 접근 로직을 다른 부분에서 다시 사용하려면, 동일한 쿼리를 중복 작성해야 하는 경우가 많습니다. 이는 코드 중복을 초래하고, 유지보수 비용을 증가시킵니다.
4. 테스트 어려움
직접 SQL 쿼리를 작성하는 방식은 테스트 코드 작성에 어려움을 줍니다. 데이터베이스 상태에 의존하는 쿼리는 단위 테스트를 작성하기 어렵고, 통합 테스트를 작성하더라도 테스트 환경 설정에 많은 노력이 필요합니다.
5. 데이터베이스 독립성 부족
PDO는 여러 데이터베이스 드라이버를 지원하지만, 직접 작성된 SQL 쿼리는 특정 데이터베이스에 종속적인 경우가 많습니다. 예를 들어, MySQL에서 사용된 특정 SQL 구문이나 함수는 PostgreSQL, SQLite 등 다른 데이터베이스에서는 호환되지 않을 수 있습니다. 이러한 종속성은 데이터베이스를 변경하거나 다중 데이터베이스 지원을 추가할 때 큰 장애물이 됩니다.
6. 코드 중복 증가
직접 SQL 쿼리를 작성하는 방식은 동일한 로직을 여러 곳에서 반복하게 만들 수 있습니다. 예를 들어, 특정 테이블에서 데이터를 가져오는 쿼리를 여러 파일에서 중복 작성하게 되면, 유지보수 시 이 모든 곳을 수정해야 합니다. 이는 코드 중복을 초래하고, 유지보수성을 떨어뜨립니다.
7. 에러 처리 복잡성
PDO는 에러 처리를 위한 메커니즘을 제공하지만, 각 쿼리에 대한 에러 처리가 반복적으로 작성되어야 합니다. 이는 코드의 복잡성을 증가시키고, 일관된 에러 처리 방식을 유지하기 어렵게 만듭니다. ORM을 사용하면 공통된 에러 처리 로직을 중앙에서 관리할 수 있어 코드가 간결해지고, 유지보수가 용이해집니다.
8. SQL 인젝션 방지의 번거로움
PDO는 준비된 문장을 통해 SQL 인젝션을 방지할 수 있지만, 이를 모든 쿼리에 적용하는 것은 개발자의 책임입니다. 모든 SQL 쿼리에 대해 준비된 문을 올바르게 사용하는지 확인하고 관리하는 것은 번거롭고, 실수로 인해 보안 취약점이 발생할 수 있습니다. ORM은 이러한 보안 취약점을 체계적으로 방지해 줍니다.
9. 성능 최적화의 어려움
PDO 기반의 SQL 쿼리는 성능 최적화를 위해 쿼리를 수동으로 튜닝해야 하는 경우가 많습니다. 쿼리 최적화, 인덱스 사용, 조인 최적화 등을 개발자가 직접 관리해야 합니다. ORM은 성능 최적화 기능을 제공하여, 개발자가 쿼리 최적화에 소요되는 시간을 줄이고, 더 중요한 비즈니스 로직에 집중할 수 있게 합니다.
10. 관계형 데이터 조작의 번거로움
관계형 데이터베이스에서는 여러 테이블 간의 관계를 다루는 일이 빈번합니다. 직접 SQL을 작성하는 경우, 이러한 관계를 관리하는 쿼리는 복잡해질 수 있으며, 관계를 명확하게 정의하고 유지하는 것이 어렵습니다. ORM은 객체 간의 관계를 명확하게 정의하고 관리할 수 있는 기능을 제공하여, 복잡한 쿼리 작성과 관계 관리의 번거로움을 줄여줍니다.
ORM 도입의 필요성
1. 코드의 가독성 및 유지보수성 향상
ORM을 도입하면 데이터베이스와의 상호작용을 객체 지향적으로 처리할 수 있습니다. 이는 코드의 가독성을 크게 향상시키고, 데이터베이스 구조 변경 시 ORM 설정만 변경하면 되어 유지보수성이 높아집니다. 복잡한 SQL 쿼리를 객체 지향적으로 표현할 수 있어, 코드를 이해하고 수정하는 데 드는 시간이 줄어듭니다.
2. 보안 강화
ORM은 내부적으로 준비된 문을 사용하여 SQL 인젝션 공격을 방지합니다. 개발자가 직접 SQL 쿼리를 작성하지 않으므로, 보안 취약점이 발생할 확률이 줄어듭니다. 또한, ORM은 데이터 검증과 같은 추가 보안 기능도 제공하여, 보다 안전한 애플리케이션을 개발할 수 있습니다.
3. 재사용성 증대
ORM을 사용하면 데이터 접근 로직을 재사용 가능한 객체로 캡슐화할 수 있습니다. 이는 코드 중복을 줄이고, 동일한 데이터 접근 로직을 여러 곳에서 쉽게 재사용할 수 있도록 합니다. 또한, ORM은 쿼리 빌더 기능을 제공하여 동적으로 쿼리를 생성할 수 있어, 다양한 요구사항에 유연하게 대응할 수 있습니다.
4. 테스트 용이성
ORM은 데이터베이스 상호작용을 객체 단위로 처리하므로, 단위 테스트를 작성하기가 용이합니다. 가짜 객체(Mock Objects)를 사용하여 데이터베이스와의 상호작용을 테스트할 수 있으며, 통합 테스트 작성 시에도 ORM이 제공하는 추상화를 통해 테스트 환경 설정이 간편해집니다.
5. 신규 개발자의 이탈 방지
ORM 도입은 신규 개발자의 이탈을 방지하는 데에도 중요한 역할을 합니다. 레거시 프로젝트에서 PDO를 이용한 직접 SQL 쿼리 작성 방식은 신규 개발자가 프로젝트에 적응하는 데 큰 장벽이 될 수 있습니다.
학습 곡선 완화
PDO 기반 SQL 쿼리 작성 방식은 데이터베이스와 SQL에 대한 깊은 이해가 필요하며, 신규 개발자가 이러한 방식을 익히고 효율적으로 활용하기까지 상당한 시간이 걸립니다. 반면, ORM은 객체 지향 프로그래밍 패러다임에 익숙한 개발자라면 비교적 쉽게 적응할 수 있습니다. 이를 통해 신규 개발자가 프로젝트에 빠르게 적응하고 생산성을 발휘할 수 있게 됩니다.
코드의 일관성과 가독성 향상
ORM을 사용하면 데이터베이스와의 상호작용을 객체 지향적으로 처리할 수 있어 코드의 일관성과 가독성이 크게 향상됩니다. 신규 개발자는 명확하고 일관된 코드를 통해 프로젝트 구조를 쉽게 이해하고, 수정 및 확장이 용이한 코드를 작성할 수 있습니다. 이는 개발 과정에서의 혼란을 줄이고, 작업 효율성을 높여줍니다.
개발 경험의 현대화
ORM을 도입하면 최신 개발 패턴과 도구를 사용할 수 있어, 프로젝트가 더 현대적이고 매력적으로 보일 수 있습니다. 신규 개발자는 최신 기술과 도구를 사용하고 싶어하며, 이를 통해 자신의 기술 스택을 확장하고자 합니다. ORM은 이러한 요구를 충족시켜 주며, 프로젝트에 대한 만족도를 높여줍니다.
협업과 코드 리뷰의 용이성
ORM은 표준화된 방법으로 데이터베이스와 상호작용하기 때문에 협업과 코드 리뷰가 용이해집니다. 신규 개발자는 팀 내에서 효율적으로 협업할 수 있고, 코드 리뷰 과정에서도 일관된 코딩 스타일과 명확한 쿼리 구조를 유지할 수 있습니다. 이는 팀 전체의 생산성을 높이고, 신규 개발자가 팀에 원활하게 통합될 수 있도록 돕습니다.
PHP 레거시 프로젝트에 Eloquent ORM 도입 방안
PHP 레거시 프로젝트에 Eloquent ORM을 도입하는 것은 프로젝트의 유지보수성과 생산성을 향상시키는 중요한 단계입니다. 다음은 Eloquent ORM을 도입하는 구체적인 단계입니다.
1. Composer 설치 및 설정
Composer는 PHP의 의존성 관리 도구로, Eloquent ORM을 설치하는 데 사용됩니다. 프로젝트 루트 디렉토리에 Composer를 설치하고 설정합니다.
Composer 설치
먼저, Composer를 설치합니다. 설치 방법은 Composer 공식 사이트에서 확인할 수 있습니다.
2. Eloquent ORM 및 관련 패키지 설치
composer.json 파일에 Eloquent ORM을 포함한 필요한 패키지를 추가합니다.
{
"require": {
"illuminate/database": "^8.0",
"illuminate/events": "^8.0"
}
}
이제 다음 명령어를 실행하여 패키지를 설치합니다.
composer install
3. Eloquent ORM 초기화 파일 생성
프로젝트의 루트 디렉토리에 bootstrap.php 파일을 생성하여 Eloquent ORM을 초기화합니다. 이 파일은 데이터베이스 설정과 Eloquent 초기화를 담당합니다. 레거시 프로젝트에서 기존 사용하였던 dbinfo.php 정보가 있다는 가정하에 그 정보를 가지고 자동으로 load하도록 만들어 두었습니다.
dbinfo.php
<?
$dbinfo = array();
$dbinfo["default"]["host"] = "defaultHost";
$dbinfo["default"]["db"] = "db";
$dbinfo["default"]["user"] = "mysql";
$dbinfo["default"]["pwd"] = "111111";
$dbinfo["default"]["port"] = "3306";
$dbinfo["log"]["host"] = "logHost";
$dbinfo["log"]["db"] = "logdb";
$dbinfo["log"]["user"] = "mysql";
$dbinfo["log"]["pwd"] = "2222";
$dbinfo["log"]["port"] = "3306";
bootstrap.php
<?php
require 'vendor/autoload.php';
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Events\Dispatcher;
use Illuminate\Container\Container;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\Facades\DB;
// 기존 dbinfo.php 정보 활용
require_once 'class/config/dbinfo.php';
$ormDbInfo = [];
foreach ($dbinfo as $key => $value) {
$ormDbInfo[$key] = [
'driver' => 'mysql',
'host' => $value['host'] ?? 'localhost',
'port' => $value['port'] ?? '3306',
'database' => $value['db'] ?? 'mydb',
'username' => $value['user'] ?? 'user',
'password' => $value['pwd'] ?? '',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
];
}
//var_dump($ormDbInfo);exit();
// 애플리케이션 컨테이너 생성 및 설정
$app = new Container();
$app->instance('app', $app);
$app->singleton('config', function () use ($ormDbInfo) {
return [
'database.default' => 'default',
'database.connections' => $ormDbInfo,
];
});
// 이벤트 디스패처 설정
$events = new Dispatcher($app);
$app->instance('events', $events);
// Facade 초기화
Facade::setFacadeApplication($app);
// Eloquent ORM 설정
$capsule = new Capsule;
foreach ($ormDbInfo as $key => $value) {
$capsule->addConnection($app['config']['database.connections'][$key], $key);
}
$capsule->setEventDispatcher($events);
$capsule->setAsGlobal();
$capsule->bootEloquent();
// Facades 설정
$app->instance('db', $capsule->getDatabaseManager());
DB::setFacadeApplication($app);
4. 모델 클래스 생성
Eloquent ORM을 사용하여 데이터베이스와 상호작용할 모델 클래스를 생성합니다. 모델 클래스는 데이터베이스 테이블을 나타내며, 각 클래스는 한 테이블과 매핑됩니다.
본 문서는 ORM 도입을 위한 제안 및 초기 사용을 목적으로 제작되었습니다. 때문에 예시에서 사용할 내용은 실제로 Legacy Project에서 사용하는 기능을 기반으로 작성하였음을 안내드립니다.
모델 클래스 :
legacy-project/app/Models/Member.php
<?php
namespace app\Models;
use Illuminate\Database\Eloquent\Model;
class Member extends Model
{
protected $table = 'member';
protected $primaryKey = 'index';
public $timestamps = false;
const CREATED_AT = 'create_date';
const UPDATED_AT = 'update_date';
protected $fillable = []; // 대량 할당시 필요한 컬럼.
protected $guarded = []; // 대량 할당시 제외할 컬럼.
public function lectureUser()
{
return $this->hasMany(LectureUser::class,'member_index','index');
}
}
legacy-project/app/Models/Lecture.php
<?php
namespace app\Models;
use Illuminate\Database\Eloquent\Model;
class Lecture extends Model
{
protected $table = 'lecture';
protected $primaryKey = 'index';
public $timestamps = false;
const CREATED_AT = 'create_date';
const UPDATED_AT = 'update_date';
protected $fillable = []; // 대량 할당시 필요한 컬럼.
protected $guarded = []; // 대량 할당시 제외할 컬럼.
public function lectureUser()
{
return $this->hasMany(LectureUser::class,'lecture_index','index');
}
}
legacy-project/app/Models/LectureUser.php
<?php
namespace ctm\rest\Models;
use Illuminate\Database\Eloquent\Model;
class LectureUser extends Model
{
protected $table = 'lecture_user';
protected $primaryKey = 'index';
public $timestamps = false;
const CREATED_AT = 'create_date';
const UPDATED_AT = 'update_date';
protected $guarded = [];
public function member()
{
return $this->belongsTo(Member::class,'member_index','index');
}
public function lecture()
{
return $this->belongsTo(Lecture::class,'lecture_index','index');
}
}
5. 프로젝트의 엔트리 포인트 수정
프로젝트의 엔트리 포인트 파일 (기존 PDO Query문을 사용하였던 파일) (예:lecture.php) 에서 bootstrap.php 파일을 불러와 Eloquent ORM을 초기화합니다.
<?php
require 'bootstrap.php';
use app\Repositories\LectureUserRepository;
class Lecture {
protected $lectureUserRepository;
function __construct(?LectureUserRepository $lectureUserRepository) {
$this->lectureUserRepository = $lectureUserRepository ?? new LectureUserRepository();
// ... 이하 생략
}
}
Repository 패턴은 데이터 접근 로직과 비즈니스 로직을 분리하는 디자인 패턴입니다. 이 패턴은 데이터베이스 쿼리나 데이터 액세스 관련 로직을 하나의 클래스에 모아서 처리함으로써 코드의 재사용성과 유지보수성을 높이는 데 목적이 있습니다.
단순히 모델만을 사용하여 비즈니스 로직을 구현하던 시대는 지났습니다. Repository 패턴을 통해 코드의 재사용성을 높이고, 데이터 접근 로직을 깔끔하게 관리할 수 있습니다. 또한, 이 패턴은 비즈니스 로직을 독립적으로 테스트할 수 있게 해주며, 데이터 저장소의 변경에 따른 코드 변경을 최소화할 수 있습니다.
Repository 패턴의 장점
- 코드 재사용성: 데이터 접근 로직을 한 곳에 모아 재사용할 수 있으므로, 중복 코드를 줄일 수 있습니다.
- 유지보수성 향상: 데이터 접근 로직이 비즈니스 로직과 분리되어 있어 유지보수가 용이합니다.
- 테스트 용이성: Repository 패턴을 사용하면 데이터 접근 로직을 목(Mock)으로 대체하여 비즈니스 로직을 쉽게 테스트할 수 있습니다.
- 유연성: 데이터 저장소가 변경되더라도 Repository 클래스만 수정하면 되므로, 비즈니스 로직에 대한 영향을 최소화할 수 있습니다.
legacy-project/app/Repositories/LectureUserRepository.php
<?php
namespace app\Repositories;
use app\Models\LectureUser;
use Illuminate\Support\Facades\DB;
class LectureUserRepository extends BaseRepository
{
protected $cacheTtl = 60;
protected $cacheChangeTtl = 10;
public function __construct(LectureUser $model = null)
{
if ($model == null) {
$model = new LectureUser();
}
parent::__construct($model);
}
}
lagacy-project/app/Repositories/BaseRepository.php
<?php
namespace app\Repositories;
use app\Repositories\RepositoryInterface;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class BaseRepository implements RepositoryInterface
{
protected $model;
protected $connection = null;
protected $cache_prefix = 'model';
protected $cacheTtl = 60;
protected $cacheChangeTtl = 10;
public $pageSize = 50;
public function __construct(Model $model)
{
$this->model = $model;
}
public function setConnection($connection)
{
$this->connection = $connection;
return $this;
}
protected function getModel()
{
if (empty($this->connection)) {
return $this->model;
}
return $this->model->setConnection($this->connection);
}
protected function query()
{
$this->newQuery();
}
protected function getQuery()
{
if (empty($this->connection)) {
return $this->model->query();
}
return $this->model->on($this->connection)->getQuery();
}
protected function newQuery()
{
if (empty($this->connection)) {
return $this->model->newQuery();
}
return $this->model->setConnection($this->connection)->newQuery();
}
public function findById($id, array $columns = ['*'], array $relations = [])
{
return $this->getModel()->with($relations)->find($id, $columns);
}
public function all(array $columns = ['*'], array $relations = [])
{
return $this->getModel()->with($relations)->get($columns);
}
public function paginate($perPages, array $columns = ['*'], array $relations = [])
{
return $this->getModel()->select($columns)->with($relations)->orderByDesc($this->model->getKeyName())->paginate($perPages);
}
public function create(array $attributes)
{
return $this->newQuery()->create($attributes);
}
public function firstOrNew(array $attributes = [], array $values = [])
{
return $this->newQuery()->firstOrNew($attributes, $values);
}
public function firstOrCreate(array $attributes = [], array $values = [])
{
return $this->newQuery()->firstOrCreate($attributes, $values);
}
public function updateOrCreate(array $attributes, array $values = [])
{
return $this->newQuery()->updateOrCreate($attributes, $values);
}
public function insert($arrItems)
{
return $this->newQuery()->insert($arrItems);
}
public function insertGetId($arrItems)
{
return $this->newQuery()->insertGetId($arrItems);
}
public function update($id, array $attributes)
{
// id는 수정할 수 없음 - $fillable에 id가 들어갈경우 처리 해줘야됨
if (isset($attributes[$this->model->getKeyName()])) {
unset($attributes[$this->model->getKeyName()]);
}
return $this->newQuery()->where($this->model->getKeyName(), $id)->update($attributes);
}
public function log($id, $logType, array $attributes)
{
// id는 수정할 수 없음 - $fillable에 id가 들어갈경우 처리 해줘야됨
if (isset($attributes[$this->model->getKeyName()])) {
unset($attributes[$this->model->getKeyName()]);
}
return true;
}
public function delete($ids)
{
if (is_array($ids)) {
return $this->newQuery()->whereIn($this->model->getKeyName(), $ids)->delete();
}
return $this->newQuery()->where($this->model->getKeyName(), $ids)->delete();
}
}
lagacy-project/app/Repositories/RepositoryInterface.php
<?php
namespace app\Repositories;
interface RepositoryInterface
{
public function findById($id , array $columns = ['*'], array $relations = []);
public function all(array $columns = ['*'], array $relations = []);
public function paginate($perPages, array $columns = ['*'], array $relations = []);
public function create(array $attributes);
public function firstOrNew(array $attributes = [], array $values = []);
public function firstOrCreate(array $attributes = [], array $values = []);
public function updateOrCreate(array $attributes, array $values = []);
public function insert($arrItems);
public function update($id, array $attributes);
public function delete($id);
}
6. 기존 코드의 Eloquent ORM으로의 변환
위에서 구현한 Repository와 ORM Relationship을 활용하여 PDO 기반 SQL Legacy Query를 걷어내며 Eloquent ORM의 도입을 진행할 수 있습니다.
결론
PDO를 이용한 SQL 쿼리문 작성 방식은 레거시 프로젝트에서 여전히 많이 사용되고 있지만, 여러 단점으로 인해 유지보수와 확장에 어려움을 겪고 있습니다. ORM을 도입함으로써 코드의 가독성과 유지보수성을 향상시키고, 보안을 강화하며, 재사용성을 증대시킬 수 있습니다.
또한, 테스트 용이성을 높여 보다 안정적인 애플리케이션을 개발할 수 있습니다. 따라서, 레거시 프로젝트의 기술 부채를 해소하고, 효율적인 개발 환경을 조성하기 위해 ORM 도입은 필수적입니다.
이를 통해 프로젝트의 성공 가능성을 높이고, 향후 저희 개발연구소 팀원들의 생산성을 극대화할 수 있습니다. ORM 도입은 초기 학습 곡선이 존재할 수 있지만, 장기적으로는 큰 이점을 제공할 것입니다.
'Server Language > PHP' 카테고리의 다른 글
[Legacy to Modernization] 3. Language Context Switching에 따른 작업 효율성 고려하기 (8) | 2024.09.25 |
---|---|
[Legacy to Modernization] 2. 형식없는 Appliction API를 표준화된 RestAPI로 교체하기. (0) | 2024.07.21 |
활용하면 좋은 PHP 매직 메소드 (0) | 2024.01.02 |
내가 만든 패키지를 packagist.org에서부터 설치해보자 (0) | 2023.11.08 |
내가 만든 패키지를 packagist.org에 올려보자 (0) | 2023.11.08 |
댓글