목차

1. 목표 - MVC에서 모델 단순화하기

  • MVC(Model, View, Controller)란 소프트웨어 공학에서 사용되는 아키텍처 패턴.
  • 기존의 프로그램은 아래와 같은 패턴을 가지고 있음.

프로그램()
{
입력을 받는다.
정보를 조금 처리한다.
조금 출력한다.

입력을 또 받는다.
정보를 또 처리한다.
더 출력한다.
}  

  • 이렇게 프로그램을 구성할 경우, 조금만 구성을 변경하려 해도 많은 비용을 초래하기 때문에, 이를 각 영역별로 나누어 처리하는 것이 좋겠다는 생각을 가짐.
  • 이처럼 나누어 처리하기 위해 구성한 것이 입력 처리(Controller), 정보 처리(Model), 출력 처리(View) 이며, 이를 MVC라 함.
  • MVC의 역할은 아래와 같음
Controller사용자의 입력을 받아 처리하고, Model과 상호작용하며, 적절한 View를 선택해 주는 역할
Model프로그램이 사용될 영역에 따라서 처리해야 하는 논리들을 처리하는 부분입니다. 대개의 경우 Database에 내용을 넣고, 빼고, 바꾸고 하는 것이 Model에서 처리
View프로그램의 외관을 책임지는 부분입니다. Model의 정보들을 받아서 화면에 그리는 역할

2. 안티패턴 - 액티브 레코드인 모델

  • 간단한 애플리케이션에서는, 모델에 맞춤 로직이 많이 필요하지 않기 때문에, CRUD 동작만 알면 됨.
  • Martin Fowler는 이런 매핑을 지원하는 디자인 패턴을 액티브 레코드(Active Record)라 지칭했는데, 이는 데이터 접근 패턴임.
  • 데이터베이스의 테이블이나 뷰에 대응되는 클래스를 정의하고, 클래스 메소드인 find() 또는 save()를 호출하여 DML을 처리함.

<?php
$bugsTable = Doctrine_Core::getTable('Bugs');
$bugsTable->find(1234);

$bug = new Bugs();
$bug->summary = "Crashes when I save";
$bug->save();


  • Joel Spolsky는 2002년 누설되기 쉬운 추상화(leaky abstraction)란 용어를 만듬. 추상화는 어떤 기술의 내부 동작을 단순화해 사용하기 쉽게 해주지만, 생산적이 되기 위해 내부 구조를 알아야만 한다면, 누설되기 쉬운 추상화라 할 수 있음.
  • MVC에서 액티브 레코드를 모델로 사용하는 것은 누설되기 쉬운 추상화의 좋은 예인데, 아주 단순한 경우 액티브 레코드는 마치 마법처럼 동작하지만, GROUP BY 같은 구문을 사용할 경우 액티브 레코드에서는 다루기 힘들어짐.
  • 어떤 프레임워크에서는 액티브 레코드를 개선해 다양한 SQL 절을 지원하려 하지만, 이는 SQL을 직접 사용하는 것이 더 낫겠다는 느낌만 들게 함.
  • 2004년, Ruby on rails는 액티브 레코드를 웹 개발 프레임워크에 보급했고, 이제 이 패턴은 대부분의 웹 개발 프레임워크에서 데이터 접근 객체(DAO, Data Access Object)에 대한 사실상의 표준이 됨.
  • 액티브 레코드를 사용하는 것에는 아무런 잘못이 없지만, 안티패턴은 MVC 애플리케이션에서 모든 모델 클래스가 액티브 레코드 클래스를 상속하기 때문에 문제가 됨.

1) 액티브 레코드는 모델을 스키마와 결합시킨다

  • 액티브 레코드는 단순한 패턴인데, 액티브 레코드 클래스는 데이터베이스 테이블 하나 또는 뷰 하나를 나타내며, 액티브 레코드 객체의 각 필드는 대응되는 테이블의 각 컬럼과 매칭됨. 테이블이 16개라면, 모델 서브클래스도 16개를 정의함.
  • 이는 새로운 구조의 데이터를 표현하기 위해 데이터베이스를 리팰토링해야 할 때 모델 클래스도 수정해야 하고, 이들 모델 클래스를 사용하는 애플리케이션 코드도 수정해야 함을 뜻함.

2) 액티브 레코드는 CRUD 함수를 노출시킨다

  • 또 다른 문제는, 당신의 모델 클래스를 사용하는 다른 프로그래머가 당신이 의도한 사용법을 우회해, CRUD 함수로 데이터를 직접 업데이트 할 수 있다는 것.
  • 예를 들어, 버그 모델에 assignUser() 메소드를 추가해 버그가 업데이트되면 담당 엔지니어에게 메일을 보내도록 했다고 하자.

<?php
class CustomBugs extends BaseBugs
{
  public function assignUser(Accounts $a)
  {
    $this->assigned_to = $a->account_id;
    $this->save();
    mail($a->email, "Assigned bug",
      "You are now responsible for bug #{$this->bug_id}.");
  }
}

  • 그러나 다른 프로그래머가 이 메소드를 우회해 메일을 보내지 않고 버그를 직접 할당할 수 있다.

<?php
$bugsTable = Doctrine_Core::getTable('Bugs');
$bugsTable->find(1234);

$bug = new Bugs();
$bug->summary = "Crashes when I save";
$bug->save();

  • 요구사항은 할당이 바뀔 때마다 이메일을 보내 알려주는 것인데, 이메일을 보내는 단계를 우회할 수 있는 것이다.
  • 이처럼, 상속한 모델 클래스에 액티브 레코드 클래스의 CRUD 메소드를 노출시켜, 다른 프로그래머가 이를 부적적하게 사용할 경우 어떻게 막을 수 있는가?

3) 액티브 레코드는 빈약한 도메인 모델을 조장한다

  • 액티브 레코드는 모델이 기본적인 CRUD 메소드 외에는 아무런 동작도 가지지 않는 경우가 많으며, 많은 개발자들이 액티브 레코드 클래스를 상속한 다음, 모델이 해야 하는 동작과 관련한 메소드를 추가하지 않고 사용함.
  • 모델을 단순한 데이터 접근 객체로 취급하면, 비지니스 로직은 모델 외부에 여러 개의 컨트롤러 클래스에 걸쳐 존재하게 되고, 이로 인해 모델 동작의 응집도(cohesion)가 낮아짐.
  • Martin Fowler는 자신의 블로그에서 이런 안티패턴을 빈약한 도메인 모델(Anemic Domain Model)이라 부름.
  • 만약 Bugs, Accounts, Products 테이블에 대응되는 액티브 레코드 클래스를 각각 가질 수 있는데, 많은 애플리케이션에서 이 3개 테이블의 데이터다 모두 필요할 경우 불리하게 작용함.
  • 버그 추적 애플리케이션에서 버그 할당, 데이터 입력, 버그 표시, 버그 검색 작업을 구현하는 간단한 코드 예제를 살펴보자.

<?php
class AdminController extends Zend_Controller_Action
{
  public function assignAction()
  {
    $bugsTable = Doctrine_Core::getTable("Bugs");
    $bug = $bugsTable->find($_POST["bug_id"]);
    $bug->Products[] = $_POST["product_id"];
    $bug->assigned_to = $_POST["user_assigned_to"];
    $bug->save();
  }
}

class BugController extends Zend_Controller_Action
{
  public function enterAction()
  {
    $bug = new Bugs();
    $bug->summary = $_POST["summary"];
    $bug->description = $_POST["summary"];
    $bug->status = "NEW";

    $accountsTable = Doctrine_Core::getTable("Accounts");
    $auth = Zend_Auth::getInstance();
    if ($auth && $auth->hasIdentity()) {
      $bug->reported_by = $auth->getIdentity();
    }
    $bug->save();
  }

  public function displayAction()
  {
    $bugsTable = Doctrine_Core::getTable("Bugs");
    $this->view->bug = $bugsTable->find($_GET["bug_id"]);

    $accountsTable = Doctrine_Core::getTable("Accounts");
    $this->view->reportedBy = $accountsTable->find($bug->reported_by);
    $this->view->assignedTo = $accountsTable->find($bug->assigned_to);
    $this->view->verifiedBy = $accountsTable->find($bug->verified_by);

    $productsTable = Doctrine_Core::getTable("Products");
    $this->view->products = $bug->Products;
  }
}

class SearchController extends Zend_Controller_Action
{
  public function bugsAction()
  {
    $q = Doctrine_Query::create()
      ->from("Bugs b")
      ->join("b.Products p")
      ->where("b.status = ?", $_GET["status"])
      ->andWhere("MATCH(b.summary, b.description) AGAINST (?)", $_GET["search"]);
    $this->view->searchResults = $q->fetchArray();
  }
}

  • 컨트롤러 클래스에서 액티브 레코드를 사용하는 코드는 애플리케이션 로직을 정리하기 위해 절차적 방법을 사용했음.
  • 데이터베이스 스키마나 애플리케이션 동작이 변하기라도 한다면, 코드의 여러 부분을 수정해야 하고, 마찬가지로 컨트롤러를 추가할 경우 모델에 대한 쿼리가 다른 컨트롤러에 있는 것과 비슷하다 하더라도 새로운 코드를 작성해야 함.
  • 또한 클래스 상호작용 다이어그램이 너저분하고 읽기도 어려우며, 컨트롤러나 DAO 클래스를 추가하면 더 나빠짐.
  • 이는 다른 모델을 함께 사용하는 코드가 컨트롤러 여기저기에 중복되어 있음을 나타내는 강력한 단서가 될 수 있으며, 애플리케이션을 단순화하고 캡슐화기 위한 다른 접근방법이 필요하다는 것을 알 수 있음.

4) 마법의 콩은 단위 테스트가 어렵다

  • 마법의 콩 안티패턴을 사용하면, MVC의 각 계층을 테스트하기 어려워짐
모델 테스트모델을 액티브 레코드와 같은 클래스로 만들었기 때문에, 데이터 접근과 분리해 모델의 동작을 테스트할 수 없음. 모델을 테스트하려면, 실제 데이터베이스에 쿼리를 실행해야 하며, 이로 인해 모델테스트를 위하 준비작업과 정리 작업을 느리게 하고 에러 발생 가능성을 높임.
뷰 테스트뷰를 테스트한다는 말응 뷰를 HTML로 렌더링하고 결과를 파싱해 모델로부터 제공받은 동적 HTML 요소가 출력에 나타나는지 확인한다는 의미.
컨트롤러 테스트데이터 접근 객체인 모델로 인해 여러 컨트롤러에 동일한 코드가 반복해서 나타나게 되는데, 이 모든 것이 테스트되어야 하며, 이로 인해 비지니스 로직을 테스트하는 데 많은 설정 코드가 필요해지고, 테스트 실행도 느려짐

3. 안티패턴 인식 방법

  • "모델에 맞춤 SQL 쿼리를 어떻게 넘길 수 있을까?"
    • 질문을 보면 모델 클래스를 데이터베이스 접근 클래스로 사용하고 있는 것을 알 수 있음.
    • 모델에 SQL 쿼리를 넘길 필요가 없어야 하고, 모델 클래스는 필요한 쿼리가 어떤 것이든 캡슐화해야 함.
  • "복잡한 모델 쿼리를 모든 컨트롤러에 복사해야 할까, 아니면 코드를 추상 컨트롤러에 한 번만 작성해야 할까?"
    • 어떤 방법도 안정성이나 단순성 같은 원하는 것을 얻을 수 없음.
    • 모델 안에 복잡한 쿼리를 코딩해야 하고, 모델의 인터페이스로 노출해서, DRY(Dont Repeat Yourself) 원칙을 따르고, 모델의 사용을 쉽게 해야 함.
  • "내 모들을 단위 테스트하기 위해 데이터베이스 픽스처를 더 작성해야 해."
    • 데이터베이스 픽스처(템프테이블)를 사용하고 있다면, 데이터베이스 접근을 테스트하는 것이지 비지니스 로직을 테스트 하는 것이 아님.
    • 데이터베이스와 격리된 상태에서 모델을 단위 테스트 할 수 있어야 함.

4. 안티패턴 사용이 합당한 경우

  • 액티브 레코드 디자인 패턴은 CRUD 동작에서는 편리한 패턴이므로, 한정지어서 사용할 경우 유용함.
  • 추가로, 프로토타입 처럼 코드를 빨리 작성해서 테스트 및 유지보수하기 쉬운 코드 작성 시 좋음.

5. 해법 - 액티브 레코드를 가지는 모델

  • 액티브 레코드를 가지는 모델에서는 입력/출력을 담당하는 컨트톨러와 뷰 보다, 어떻게 모델을 객체지향적으로 설계해야 하는지에 대한 고민이 더 필요함.

1) 모델 이해하기

  • 모델을 이해하기 위해 각 파트별로 역할을 살펴본다.
1-1) 정보 전문가
  • 어떤 동작에 책임이 있는 객체는 동작을 수행하는 데 필요한 모든 데이터를 가지고 있어야 함.
  • 애플리케이션의 어떤 동작은 여러 테이블과 관련이 있을 수 있는데, 액티브 레코드는 한 번에 한 테이블하고만 잘 동작하므로, 여러 개의 데이터 접근 객체를 모아 합성 동작에 사용할 다른 클래스가 필요함.
  • 모델과 액티브 레코드와 같은 DAO 사이의 관계는 IS-A(상속)가 아닌 HASH(집합연관)여야 함.
1-2) 창조자
  • 모델이 데이터를 데이터베이스 저장하는 방법은 내부적 구현 상세여야 하며, DAO를 모은 도메인 모델이 이런 DAO 객체 생성을 책임져야 함.
  • 컨트롤러와 뷰는 모델이 데이터를 어떻게 구성하는지 알 필요 없이 인터페이스만을 고민해야 하며, 이렇게 해야만 모델에서 쿼리를 변경하기도 쉬워짐
1-3) 낮은 결합도
  • 논리적으로 독립적인 코드 블록을 분리하는 것이 중요하며, 이렇게 할 경우 다른 곳에 영향을 주지 않으면서 클래스의 구현을 변경할 수 있는 유연성을 얻을 수 있음.
1-4) 높은 응집도
  • 도메인 모델 클래스의 인터페이스는 의도된 사용법을 반영해야 하며, 데이터베이스의 물리적 구조나 CRUD 동작을 반영하면 안됨.
  • 또한 메소드 이름은 find(), insert() 같이 불명확하게 할 경우 컨트롤러에서 처리하기 힘드므로, assignUser() 같은 이름으로 해야 함.

2) 도메인 모델 동작하게 하기

  • 모델을 데이터베이스 Layout이 아닌 애플리케이션 개념에 따라 설계하면, 데이터베이스 동작을 모델 클래스 속에 완전히 숨길 수 있는데, 이를 도메인 모델이라 함.
  • 아래는 앞에서 살펴본 예제를 도메인 모델로 리팩토링 한 결과.

<?php

class BugReport
{
  protected $bugsTable;
  protected $accountsTable;
  protected $productsTable;

  public function __construct()
  {
    $this->bugsTable = Doctrine_Core::getTable("Bugs");
    $this->accountsTable = Doctrine_Core::getTable("Accounts");
    $this->productsTable = Doctrine_Core::getTable("Products");
  }

  public function create($summary, $description, $reportedBy)
  {
    $bug = new Bugs();
    $bug->summary = $summary
    $bug->description = $description
    $bug->status = "NEW";
    $bug->reported_by = $reportedBy;
    $bug->save();
  }

  public function assignUser($bugId, $assignedTo)
  {
    $bug = $bugsTable->find($bugId);
    $bug->assigned_to = $assignedTo"];
    $bug->save();
  }

  public function get($bugId)
  {
    return $bugsTable->find($bugId);
  }

  public function search($status, $searchString)
  {
    $q = Doctrine_Query::create()
      ->from("Bugs b")
      ->join("b.Products p")
      ->where("b.status = ?", $status)
      ->andWhere("MATCH(b.summary, b.description) AGAINST (?)", $searchString]);
    return $q->fetchArray();
  }
}


class AdminController extends Zend_Controller_Action
{
  public function assignAction()
  {
    $this->bugReport->assignUser(
      $this->_getParam("bug"),
      $this->_getParam("user"));
  }
}


class BugController extends Zend_Controller_Action
{
  public function enterAction()
  {
    $auth = Zend_Auth::getInstance();
    if ($auth && $auth->hasIdentity()) {
      $identity = $auth->getIdentity();
    }
    $this->bugReport->create(
      $this->_getParam("summary"),
      $this->_getParam("description"),
      $identity);
  }


  public function displayAction()
  {
    $this->view->bug = $this->bugReport->get(
      $this->_getParam("bug"));
  }
}


class SearchController extends Zend_Controller_Action
{
  public function bugsAction()
  {
    $this->view->searchResults = $this->bugReport->search(
      $this->_getParam("status", "OPEN"),
      $this->_getParam("search"));
  }
}

3) 간단한 객체 테스트하기

  • 이상적으로, 실제 데이터베이스에 접속하지 않고도 모델을 테스트할 수 있어야 함.
  • 이와 마찬가지로, 도메인 모델의 인터페이스도 다른 객체지향 테스트와 비슷하게 테스트할 수 있음.
  • 객체의 메소드를 호출하고 리턴 값을 검증하는 방식으로 할 수 있으며, 이렇게 하는 것이 가짜 HTTP 요청을 만들어 컨트롤러에 넘기고 결과로 리턴된 HTTP 응답을 파싱하는 것보다 쉽고 빠름.

4) 땅으로 내려오기

  • 어떤 프레임워크이든 데이터 접근 객체를 생산적으로 사용할 수 있지만, 객체지향 설계 원칙을 어떻게 활용하는지 배우지 못한 개발자는 스파게티처럼 복잡한 코드를 작성할 수 밖에 없음.
  • 그러므로, 위에서 설명한 방법론을 숙지할 경우 최적의 설계를 선정하는 데 도움이 될 것이다.

테이블에서 모델을 분리하라.

문서에 대하여

  • 최초작성자 : ~xsoft
  • 최초작성일 : 2012년 01월 14일
  • 이 문서는 오라클클럽 코어 오라클 데이터베이스 스터디 모임에서 작성하였습니다.
  • {*}이 문서의 내용은 인사이트(insight) 에서 출간한 'SQL AntiPatterns : 개발자가 알아야 할 25가지 SQL 함정과 해법'를 참고하였습니다.*