Caution: You are browsing the legacy symfony 1.x part of this website.
Cover of the book Symfony 5: The Fast Track

Symfony 5: The Fast Track is the best book to learn modern Symfony development, from zero to production. +300 pages showcasing Symfony with Docker, APIs, queues & async tasks, Webpack, SPAs, etc.

symfony advent calendar day three: dive into the MVC architecture

1.0

지난 줄거리

이틀째 에는 관계 모델을 바탕으로 객체 모델을 생성하는 방법을 배워봤습니다. 지난 시간들에 만들었던 코드들은 SVN 저장소에서 사용가능합니다.

http://svn.askeet.com/

삼일째인 오늘에는 좀 더 괜찮은 레이아웃을 만들고, 사이트의 기본 페이지에 질문 목록들을 보여주고, 질문에 대한 사용자들의 관심도가 얼마나 되지를 표시하고, 마지막으로 텍스트파일에서 테스트 데이터를 데이터베이스에 채워넣도록 하겠습니다. 많지는 않아보이지만 읽고 이해해야 할 것들이 많습니다.

이번 튜토리얼을 일고 이해하기 위해서는 프로젝트, 어플리케이션, 모듈, 그리고 액션에 대한 개념이 있어야 합니다. 이들에 대한 개념은 온라인 문서중 컨트롤러 부분 에 자세히 있으니 참고하시기 바랍니다.

MVC 모델

이제 MVC 구조에 대해 본격적으로 알아보겠습니다. MVC 가 무슨 뜻일까요? 간단하게 말하자면, 하나의 페이지를 만들기 위한 소스 코드들이 각각의 성격에 맞게 여러군데로 분산되어 있다는 뜻입니다.

만약 소스코드가, 페이지를 표시하는것과는 별개로 자료를 조작하는 일을 한다면, 해당 소스코드는 모델 디렉토리에 (기본적으로는 askeet/lib/model/) 있어야 합니다. 만약 소스코드가 페이지를 꾸미는 역할을 한다면, 해당 소스코드는 디렉토리에 (심포니의 '뷰' 는 템플릿에 녹아있습니다. askeet/apps/frontend/modules/question/templates/ 같은 디렉토리에서 찾으실 수 있습니다.) 있어야 합니다. 마지막으로, 소스코드가 이들 전체를 통제하고 시스템 로직을 PHP 로 구현하는 역할을 한다면, 해당 소스코드는 컨트롤러 디렉토리에 (심포니에서는 각 페이지에 관한 컨트롤러들을 액션이라고 부릅니다. askeet/apps/frontend/modules/question/actins/ 디렉토리등에서 찾으실 수 있습니다.) 있어야 합니다. MVC 모델에 관한 좀 더 자세한 내용은 온라인 문서중 심포니의 MVC 구현 부분에서 찾아보시기 바랍니다.

오늘은 어플리케이션의 '뷰' 부분만 손을 대기보다는 많은 파일들에 손을 대보기로 하죠. 소스코드들이 여러군데 흩어져 있다는 것에 겁이 나실 수도 있지만, 곧 이런 형태가 분명하고 효과적이라고 생각하게 되실겁니다.

레이아웃 변경

심포니 템플릿은 데코레이터 디자인 패턴 을 이용하고 있기 때문에, 액션이 호출하는 템플릿들은 글로벌 템플릿, 또는 레이아웃이라고 불리는 템플릿과 합쳐져서 보입니다. 쉽게 얘기하자면, 화면의 변하지 않는 부분은 글로벌 템플릿 (또는 레이아웃) 이 가지고 있다가 각각의 페이지의 내용을 "꾸며" (decorate) 주는 역할을 하게 됩니다. 기본 템플릿을 열고 다음과 같이 바꿔보시기 바랍니다. 파일은 askeet/apps/frontend/templates/layout.php 에 있습니다.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
 
<?php echo include_http_metas() ?>
<?php echo include_metas() ?>
 
<?php echo include_title() ?>
 
<link rel="shortcut icon" href="/favicon.ico" />
 
</head>
<body>
 
  <div id="header">
    <ul>
      <li><?php echo link_to('about', '@homepage') ?></li>
    </ul>
    <h1><?php echo link_to(image_tag('askeet_logo.gif', 'alt=askeet'), '@homepage') ?></h1>
  </div>
 
  <div id="content">
    <div id="content_main">
      <?php echo $sf_data->getRaw('sf_content') ?>
      <div class="verticalalign"></div>
    </div>
 
    <div id="content_bar">
      <!-- Nothing for the moment -->
      <div class="verticalalign"></div>
    </div>
  </div>
 
</body>
</html>

참고: 마크업 (주 - HTML 이나 XML 등을 마크업 (markup) 언어라고 합니다.) 텍스트들이 최대한 의미를 가질 수 있도록 스타일들은 모두 CSS 스타일시트들에 저장해두었습니다. 이들 스타일시트나 CSS 문법들은 생략하기록 하겠습니다. 다만, SVN 저장소 에서 CSS 파일들을 다운로드 받으실 수 있으니 참고바랍니다.

우리는 main.csslayout.css, 두 개의 스타일시트를 만들어 두었습니다. askeet/web/css/ 디렉토리 아래에 이들 파일들 저장하고 frontend/config/view.yml 파일을 수정해서 이들 CSS 파일을 자동으로 불러들이도록 합시다.

stylesheets:    [main, layout]

이들 레이아웃은 아직 별 내용이 없습니다만 일주일정도 안에 다시 만들어질 계획입니다. 위 템플릿에서 중요한 것들은, 이미 많이 채워져있는 <head> 부분과, 각 액션의 결과를 담을 sf_content 부분입니다.

수정한 내용이 제대로 반영되었는지 홈페이지에 들어가서 확인해 봅시다. 이번에는 다음 주소로 개발환경을 이용해보십시오.

http://askeet/frontend_dev.php/

수정된 레이아웃

개발 환경에 대해

만약 http://askeet/frontend_dev.php/http://askeet/ 의 차이점이 궁금하시다면 온라인 문서중 설정 부분 을 살펴보시기 바랍니다. 지금은 그냥 두 파일이 같은 어플리케이션을 호출하지만, 다른 환경을 가지고 있다는 정도로만 이해하셔도 좋습니다. 여기서 말하는 '환경' 이란 필요에 의해 활성화되거나 비활성화된 프레임웍의 특정한 설정을 말합니다.

frontend_dev.php 의 경우, 해당 URL 은 개발 환경, 즉, 모든 설정을 매번 다시 불러오고, HTML 캐쉬를 하지 않으며, 오른쪽 윗부분에 있는 반투명 툴바를 포함한 모든 디버그 툴이 활성화된 상태를 호출하게 됩니다. /, 또는 /index.php/ 의 경우에는 서비스 환경 (production environment), 즉, 페이지를 빠르게 호출하기 위해 모든 "컴파일" 된 설정파일을 이용하고, 디버그 툴들은 비활성화한 상태를 호출하게 됩니다.

frontend_dev.phpindex.php, 두 PHP 스크립트는 대표 컨트롤러 (front controller) 라고 불리며, 어플리케이션에 관한 모든 요청은 이들에 의해 제어가 됩니다. 이들 파일은 askeet/web/ 디렉토리에서 찾으실 수 있습니다. 사실 index.php 파일은 frontend_prod.php 라고 이름지어져야 맞습니다만, frontend 는 가장 먼저 생성된 어플리케이션이기 때문에 심포니가 자동으로 축약해서 / 를 호출했을때 자동으로 frontend 어플리케이션이 실행되도록 한 것입니다. 대표 컨트롤러들이나 MVC 모델의 컨트롤러에 대해서 좀 더 알고 싶으시다면, 온라인 문서들 중 컨트롤러 부분 을 참고하시기 바랍니다.

개발에 있어서 가장 좋은 방법은, 여러분이 만들고 있는 기능이 맘에 들때까지 개발환경을 사용한 후, 서비스 환경으로 바꿔서 어플리케이션의 처리 속도와 "멋진" URL 을 확인하는 것입니다.

참고: 서비스 환경을 사용중에 클래스를 추가하거나 설정을 바꾸었다면, 캐시를 초기화하는 것을 잊지 마십시오.

기본 페이지를 다시 정하기

현재로써는 새로운 웹사이트의 메인 페이지를 호출해도 '축하합니다.' (Congratulation) 메시지 밖에는 보실 수 없을것입니다. 웹 사이트의 기본 페이지를 question/list 로 변경해서 질문 목록이 기본 페이지가 되도록 한다면 더 좋을 것입니다. (참고로, question/list 페이지는 question 모듈의 list 액션을 의미합니다.) 이를 위해서는 frontend 프로젝트의 라우팅 관련 설정 파일인 askeet/apps/frontend/config/routing.yml 파일을 수정해서 homepage: 섹션을 수정해야 합니다.

homepage:
  url:   /
  param: { module: question, action: list }

개발환경 페이지 (http://askeet/frontend_dev.php) 를 새로고침 해 보십시오. 이제 질문들의 목록을 보여줄 것입니다.

참고: 호기심많은 사람이라면 '축하합니다.' (Congratulation) 메세지가 어디서 불려지는지 찾아보셨을지도 모르겠습니다. 아마도 askeet 디렉토리 내에는 그런 내용의 페이지가 없다는 것을 알고 궁금해하고 계시겠지요? 사실 default/index 액션은 심포니 데이터 디렉토리에 정의되어 있고, 사용자가 생성한 프로젝트와는 독립적입니다. 만약 이를 재정의하고 싶으시다면, default 모듈을 만드시면 됩니다.

페이지 라우팅 시스템의 기능에 대해서는 앞으로 자세히 다뤄질 예정이지만, 더 알고 싶으시다면 온라인 문서중 라우팅 부분 을 읽어보시기 바랍니다.

테스트 데이터

아직 질문들을 채워보지 않으셨다면, 홈페이지의 질문 목록들이 텅 비어있을 것입니다. 어플리케이션을 개발할때 테스트 데이터가 있으면 편한 경우가 많습니다만, 손으로 직접 쳐넣는다는 것은 정말 괴로운 일이죠. 심포니는 텍스트 파일을 바탕으로 데이터베이스에 테스트 데이터를 채워넣을 수 있도록 하고 있습니다.

테스트 데이터 파일을 askeet/data/fixtures/ 디렉토리에 만들고 (이 디렉토리 역시 새로 만드셔야 합니다), test_data.yml 파일을 만들어 다음 내용을 넣습니다.

User:
  anonymous:
    nickname:   anonymous
    first_name: Anonymous
    last_name:  Coward

  fabien:
    nickname:   fabpot
    first_name: Fabien
    last_name:  Potencier

  francois:
    nickname:   francoisz
    first_name: François
    last_name:  Zaninotto

Question:
  q1:
    title: What shall I do tonight with my girlfriend?
    user_id: fabien
    body:  |
      We shall meet in front of the Dunkin'Donuts before dinner, 
      and I haven't the slightest idea of what I can do with her. 
      She's not interested in programming, space opera movies nor insects.
      She's kinda cute, so I really need to find something 
      that will keep her to my side for another evening.

  q2:
    title: What can I offer to my step mother?
    user_id: anonymous
    body:  |
      My stepmother has everything a stepmother is usually offered
      (watch, vacuum cleaner, earrings, del.icio.us account). 
      Her birthday comes next week, I am broke, and I know that 
      if I don't offer her something sweet, my girlfriend 
      won't look at me in the eyes for another month.

  q3:
    title: How can I generate traffic to my blog?
    user_id: francois
    body:  |
      I have a very swell blog that talks 
      about my class and mates and pets and favorite movies.

Interest:
  i1: { user_id: fabien, question_id: q1 }
  i2: { user_id: francois, question_id: q1 }
  i3: { user_id: francois, question_id: q2 }
  i4: { user_id: fabien, question_id: q2 }

먼저, YAML 이 쓰였습니다. YAML 포맷은 심포니 프레임웍의 설정 파일을 만드는데 많이 이용되었습니다. 하지만 여러분이 원하신다면 XML 이나 .ini 파일도 쉽게 사용하실 수 있습니다. 여유가 되신다면 YAML 과 심포니 설정파일에 대해서 온라인 문서 설정파일 부분 을 살펴보시기 바랍니다. 만약 YAML 문법에 익숙하시지 않다면, 지금 당장 배워보십시오. 이번 장에서는 YAML 설정 파일을 많이 다룰 것입니다.

자, 다시 테스트 데이터에 관해서입니다. 테스트 데이터는 내부 이름으로 이름지워진 객체 인스턴스들을 정의합니다. 이 내부 이름들은 관계 객체들을 id 로 정의하지 않고도 서로 연결하기에 용의합니다. 예를 들면, 제일 처음으로 생성된 객체는 User 클래스로, fabien 이란 이름을 가지고 있습니다. 제일 첫번째 Question 객체는 q1 이란 이름을 가지고 있습니다. 이를 가지고 우리는 QuestionUser 객체와 관계를 가지고 있는 Interest 클래스의 객체를 쉽게 정의할 수 있습니다.

Interest:
  i1:
    user_id: fabien
    question_id: q1

위의 데이터 파일은, YAML 과 테스트 데이터가 어떻게 동작하는지를 보여드리기 위해 비교적 간단한 문법만을 사용하였습니다. 테스트 데이터를 채우는 것에 관한 더 자세한 내용은 온라인 문서중 데이터 파일 부분 을 참고하십시오.

참고: created_atupdated_at 컬럼을 채우실 필요는 없습니다. 심포니가 해당 필드들은 자동으로 채울 것입니다.

테스트데이터 입력하기

다음 단계는 실제로 데이터베이스에 자료를 채워넣을 것입니다. 우리는 이일을 일괄처리 프로그램 (batch) 처럼, 커맨드 라인에서 호출되는 PHP 로 만들고 싶습니다.

일괄처리 프로그램 뼈대

askeet/batch 디렉토리 아래에 load_data.php 라는 파일을 만들고, 아래 내용을 저장합니다.

<?php
 
define('SF_ROOT_DIR',    realpath(dirname(__FILE__).'/..'));
define('SF_APP',         'frontend');
define('SF_ENVIRONMENT', 'dev');
define('SF_DEBUG',       true);
 
require_once(SF_ROOT_DIR.DIRECTORY_SEPARATOR.'apps'.DIRECTORY_SEPARATOR.SF_APP.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'config.php');
 
// initialize database manager
$databaseManager = new sfDatabaseManager();
$databaseManager->initialize();
 
?>

이 스크립트는 아무일도 하지 않습니다, 아니, 아무것도 아니라는편이 맞을 것 같습니다. 이 스크립트는 경로 및 설정을 가져올 어플리케이션과 환경을 저장하고, 설정파일을 불러들이고, 데이터베이스 관리자를 초기화할 뿐입니다. 하지만 벌써 많은 것들이 보이지 않는 곳에서 이루어졌습니다. 모든것이 자동으로 적재되었고, Propel 객체와 연결이 되었으며, 심포니 기본 클래스들도 사용이 가능하게 되었습니다.

참고: 심포니의 대표 컨트롤러 (askeet/web/index.php 같은) 를 살펴보셨다면, 위 코드가 그것과 상당히 비슷하다고 생각하실 것입니다. 이는 모든 웹 리퀘스트라는 것이 일반적인 일괄처리 프로세스가 사용하는 것과 같은 객체와 설정들을 사용하기 때문입니다.

데이터 불러오기

일괄처리 프로세스의 뼈대가 완성되었으니 스크립트가 자료를 입력할 수 있도록 하겠습니다. 일괄처리 프로세스는 다음과 같은 일을 합니다.

  1. YAML 파일을 읽는다.
  2. Propel 객체의 인스턴스를 생성한다.
  3. 연결된 데이터 베이스의 테이블에 자료를 생성한다.

복잡해 보이지만, 심포니에서는, sfPropelData 객체 덕분에, 단지 두 줄만 써주면 됩니다. 다음 라인들을 askeet/batch/load_data.php 스크립트의 마지막 ?> 앞에 삽입하십시오.

$data = new sfPropelData();
$data->loadData(sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'fixtures');

이게 다입니다. sfPropelData 객체가 생성되었고, 해당 디렉토리 (우리의 경우, fixtures 디렉토리) 에 있는 모든 데이터들을 데이터 베이스에 입력하라고 시켰습니다.

참고: DIRECTORY_SEPARATOR 는 *nix 와 Windows 운영체제 사이의 호환성을 위하여 사용되었습니다.

일괄처리 프로세스 실행하기

드디어, 이 몇줄 안 되는 코드들이 잘 동작하는지 확인할 시간입니다. 다음을 커맨드라인에서 입력하십시오.

$ cd /home/sfprojects/askeet/batch
$ php load_data.php

개발환경 홈페이지를 새로 고치셔서 데이터베이스가 잘 변경되었는지를 확인해 보십시오.

http://askeet/frontend_dev.php

변경된 데이터

만세! 데이터가 입력되었습니다.

참고: 기본적으로, sfPropelData 객체는 새로운 자료를 입력하기 전에 기존 데이터를 모두 지웁니다. 다음을 추가함으로써 기존 데이터에 신규 데이터를 추가로 입력할 수가 있습니다.

$data = new sfPropelData();
$data->setDeleteCurrentData(false);
$data->loadData(sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'fixtures');

데이터 모델 사용하기

우리가 question 모듈의 list 액션을 호출한다면, askeet/apps/frontend/modules/question/actions/action.class.phpexecuteList() 메쏘드가 호출되고, 템플릿으로는 askeet/apps/frontend/modules/question/templates/listSuccess.php 가 사용됩니다. 이는 심포니의 이름짓기 규약이 사용된 것이며, 좀 더 자세한 내용은 온라인 문서중 컨트롤러 부분 을 참조하시기 바랍니다. 그럼, 어떤 코드가 실행되는지 좀 더 자세히 살펴보도록 하겠습니다.

actions.class.php:

public function executeList ()
{
  $this->questions = QuestionPeer::doSelect(new Criteria());
}

listSuccess.php:

...
<?php foreach ($questions as $question): ?>
<tr>
    <td><?php echo link_to($question->getId(), 'question/show?id='.$question->getId()) ?></td>
    <td><?php echo $question->getTitle() ?></td>
    <td><?php echo $question->getBody() ?></td>
    <td><?php echo $question->getCreatedAt() ?></td>
    <td><?php echo $question->getUpdatedAt() ?></td>
  </tr>
<?php endforeach; ?>

각 단계별로, 위 코드는 아래와 같은 일들을 합니다.

  1. 액션은 Question 테이블에 제약조건이 없이 (empty criteria) 자료를 요청합니다. 즉, 모든 자료를 요청하는 것이지요.
  2. 레코드들은 배열에 담겨서 ($questions) 템플릿으로 전달됩니다.
  3. 템플릿은 액션에서 전달된 배열을 하나씩 처리해나갑니다.
  4. 템플릿은 레코드의 각 컬럼들을 표시합니다.

symfony propel-build-model 코맨드를 호출하였을때 (어제 였지요?) 만들어진 ->getId(), ->getTitle(), ->getBody() 와 같은 메쏘드들이 id, title, body 등의 자료를 읽어들입니다. 이들이 기본적인 게터 메소드 (getter method) 입니다. get 이라는 접두어로 시작하고 컬럼이름들은 각 단어의 첫글자가 대문자로 (CamelCase) 바뀌어 있습니다. 이 외에도 Propel 은, set 이라는 접두어로 시작하는, 기본적인 세터 (setter) 메소드들을 지원합니다. 각 클래스마다 생성되는 이러한 접근자들은 Propel 문서화 에 잘 설명되어 있습니다.

QuestionPeer::doSelect(new Criteria()) 메소드도 역시 Propel 과 관련되어 있습니다. 좀 더 자세한 내용은 Propel 문서를 참조해 주시기 바랍니다.

위 코드가 아직 이해가 안 된다고 걱정하시진 마십시오. 몇일 안에는 이해하실 수 있으실 것입니다.

question/list 템플릿 수정

데이터 베이스에는 각 질문에 대한 흥미도 자료 역시 있기때문에, 이를 함께 보여주도록 합시다. askeet/lib/model/om/ 디렉토리의 BaseQuestion.php 파일을 살펴보셨다면, ->getInterests() 메쏘드가 있다는 것을 알아차리셨을 겁니다. Propel 은 Interest 테이블 정의서 중 question_id 라는 외부 키 (foreign key) 를 발편하고 하나의 질문은 여러개의 흥미도 자료를 가지고 있다고 판단을 하고, 해당 메쏘드를 생성해 두었습니다. 따라서 askeet/apps/frontend/modules/question/templates/ 디렉토리의 listSuccess.php 템플릿 파일을 수정하는 것이 매우 쉽게 되었습니다. 템플릿에 흥미도를 추가함과 동시에, 보기싫은 테이블들을 모두 지우고, div 를 사용하도록 합시다.

<?php use_helper('Text') ?>
 
<h1>popular questions</h1> 
 
<?php foreach($questions as $question): ?>
  <div class="question">
    <div class="interested_block">
      <div class="interested_mark" id="interested_in_<?php echo $question->getId() ?>">
        <?php echo count($question->getInterests()) ?>
      </div>
    </div>
 
    <h2><?php echo link_to($question->getTitle(), 'question/show?id='.$question->getId()) ?></h2>
 
    <div class="question_body">
      <?php echo truncate_text($question->getBody(), 200) ?>
    </div>
  </div>
<?php endforeach; ?>

listSuccess.php 파일의 foreach 문은 바뀌지 않았습니다. link_to()truncate_text() 함수는 심포니가 제공하는 템플릿 헬퍼 들입니다. link_to() 는 같은 모듈의 다른 액션에 대한 하이퍼링크를 만듭니다. 그리고 truncate_text() 헬퍼는 본문을 200 자 길이로 만들어 줍니다. link_to() 헬퍼는 자동으로 적재되지만, truncate_text() 헬퍼를 사용하기 위해서는 Text 헬퍼 그룹을 사용하겠다는 것을 명시해주어야 합니다.

자, 새로운 홈페이지를 새로 고치셔서 바뀐 템플릿을 확인하십시오.

http://askeet/frontend_dev.php/

바뀐 질문 목록

각 질문에 대한 흥미도가 제대로 표시되고 있군요. 여러분의 사이트를 위의 화면과 같이 보이게 하고 싶으시다면, main.css 를 다운로드 받으셔서 askeet/web/css 디렉토리에 넣으시기 바랍니다.

정리

propel-generate-crud 명령은 필요하지 않은 액션들과 템플릿까지 생성하였습니다. 이를 지우도록 하겠습니다.

askeet/apps/frontend/modules/question/actions/actions.class.php 파일에서 다음 메쏘드들을 삭제합니다.

  • executeIndex
  • executeEdit
  • executeUpdate
  • executeCreate
  • executeDelete

askeet/apps/frontend/modules/question/templates/ 디렉토리에서 다음 템플릿을 삭제합니다.

  • editSuccess.php

내일 이시간에

오늘 우리는 모델-뷰-컨트롤러 패러다임 세계에 첫 발을 내딛었습니다. 레이아웃, 템플릿, 액션 그리고 Propel 객체 모델의 객체들을 다뤄보시면서 MVC 구조의 어플리케이션의 모든 항목들을 살펴보았습니다. 이들 항목들이 어떻게 상호작용하는지에 대해서는 아직 걱정하지 않으셔도 됩니다. 튜토리얼을 따라하시면서 조금씩 조금씩 명확하게 이해하게 되실 겁니다.

많은 파일들을 사용하였는데요, 만약 프로젝트에 관한 파일들이 어떻게 구성되어 있는지 좀 더 알고 싶으시다면, 온라인 문서중 파일구조 부분 을 참고하시기 바랍니다.

내일 역시도 대단할겁니다. 우리는 뷰와 모델을 수정하고, 좀 더 복잡한 URL 라우팅 정책을 설정할 것입니다. 또한 데이터 조작에 관해서 좀 더 자세히 볼 것이고, 테이블들간의 연결이 어떻게 이뤄지는지 살펴볼 것입니다.

그럼, 편안히 주무시고, 오늘 작성된 소스 코드 (release_day_3 태그) 도 한 번 둘러보시기 바랍니다.

http://svn.askeet.com/tags/release_day_3