PHP 예제로 알아보는 MVC 패턴

jijipapa
14 min readJul 8, 2016

--

웹 애플리케이션을 작성할 때 관심사 분리를 위해 가장 흔히 사용되는 것이 MVC 패턴입니다. 현재 많은 PHP 프레임워크들이 MVC 패턴을 사용하고 있고, 다른 언어에서도 각각 가장 인기있는 프레임워크라고 할 수 있는 레일즈나 스프링, 장고 등도 모두 MVC 패턴을 사용하고 있습니다.

혹시 처음 들어보신 분들은 용어나 개념이 낯설 수 있습니다만 겁먹을 필요는 없습니다. 원리나 적용 방법 자체는 굉장히 간단하므로 실무에 손쉽게 적용할 수 있을 것입니다.

MVC(Model-View-Controller) 패턴이란

MVC 패턴은 프로그램의 관심사를 모델, 뷰, 컨트롤러의 세 가지 구성요소로 나누는 설계 패턴입니다. 각 구성요소는 다음과 같은 역할을 합니다.

  • 모델은 컨트롤러에 의해 호출되어 데이터소스에 데이터를 저장하거나, 데이터소스에서 데이터를 가져와서 뷰가 사용할 수 있는 형태로 컨트롤러에 반환합니다.
  • 는 컨트롤러에 의해 호출되어 클라이언트에게 응답으로 제공할 템플릿(HTML, XML, JSON 등)을 생성해서 컨트롤러에 반환합니다.
  • 컨트롤러는 사용자의 요청을 처리하고 응답을 되돌려주는 전체 과정을 관장합니다.

MVC 패턴이 적용된 웹 애플리케이션은 일반적으로 [그림 1]과 같은 순서로 요청을 처리하게 됩니다.
1. 사용자가 브라우저를 통해 요청을 전송하면 컨트롤러가 받습니다.
2. 컨트롤러가 모델에게 데이터 처리를 요청하고
3. 모델은 처리된 데이터를 컨트롤러에 반환합니다.
4. 컨트롤러는 모델로부터 되돌려받은 데이터를 뷰에 전달합니다.
5. 뷰는 응답을 생성하여 컨트롤러에 반환합니다.
6. 컨트롤러가 브라우저에 뷰로 부터 되돌려받은 응답을 전송합니다.

[그림 1] MVC 패턴이 적용된 웹 애플리케이션의 요청 처리 흐름

MVC 패턴을 사용하면 좋은 점

MVC 패턴을 잘 적용하면 관심사 분리에 따른 장점을 얻게 됩니다. 즉 개별 부문을 이해하기 쉬워지고, 각 부문을 재사용할 수 있게 되며, 다른 부문에 대해 신경 쓸 필요가 없어져 유지보수와 협업이 쉬워집니다. 스파게티 예제를 통해 MVC 패턴을 적용함에 따라 얻게 되는 이점을 알아보도록 하겠습니다.

스파게티 예제

다음은 할 일 목록을 출력하는 예제 입니다.

task.php

<?php
$conn = new mysqli(HOST, ID, PW, DB);
if ($conn->connect_error) {
die($conn->connect_error);
}
echo “<!DOCTYPE html>
<html>
<head>
<meta charset=’utf-8'>
</head>
<body>“;
$result = $conn->query(“SELECT * FROM tasks”);
if($result) {
echo “<ul>”;
while($row = $result->fetch(MYSQLI_ASSOC)) {
echo “<li>” . $row[‘name’] . “</li>”;
}
echo “</ul>”;
} else {
echo “<p>할 일이 없습니다.</p>”;
}
echo “</body>
</html>”;

뷰가 분리되지 않은 상태에서 요구사항 추가하기

웹 브라우저가 아닌 모바일 앱에서 할 일 목록을 요청하는 경우에는 HTML 이 아닌 JSON 형식으로 응답을 해줘야 하는 요구사항이 추가되었다고 생각해봅시다. 다음과 같이 $mode에 따라 $mode 가 ‘app’ 인 경우 JSON 형식으로 응답하고, 그렇지 않은 경우는 원래대로 HTML로 응답하도록 할 수 있을 것입니다.

<?php
$mode = filter_var($_GET[‘mode’], FILTER_SANITIZE_STRING);
if($mode === “app”) {
//JSON 출력
} else {
//HTML 출력
}

출력의 형식이 JSON 이냐 HTML 이냐의 차이일 뿐 데이터 자체는 차이가 없으므로 데이터베이스에서 할 일 목록을 조회하는 로직은 한 번만 있으면 됩니다. 그런데 스파게티 예제에서는 다음과 같이 MySQL에서 가져온 데이터를 fetch 하는 것과 HTML을 생성하는 로직이 엮여 있는 상태입니다.

while($task = $result->fetch(MYSQLI_ASSOC)) {
echo “<li>” . $task[‘name’] . “</li>”;
}

이렇게 엮여있는 것을 풀지 않으면 아마도 다음과 같은 정도의 코드가 최선일 것입니다.

<?php
$conn = new mysqli(HOST, ID, PW, DB);
if ($conn->connect_error) {
die($conn->connect_error);
}
$result = $conn->query(“SELECT * FROM tasks”);$mode = filter_var($_GET[‘mode’], FILTER_SANITIZE_STRING);if($mode === “app”) {
$tasks = [];
while($task = $result->fetch(MYSQLI_ASSOC)) {
$tasks[] = $task;
}
echo json_encode($tasks);
} else {
echo “<!DOCTYPE html>
<html>
<head>
<meta charset=’utf-8'>
</head>
<body>“;
if($result) {
echo “<ul>”;
while($task = $result->fetch(MYSQLI_ASSOC)) {
echo “<li>” . $task[‘name’] . “</li>”;
}
echo “</ul>”;
} else {
echo “<p>할 일이 없습니다.</p>”;
}
echo “</body>
</html>”;
}

코드의 중복을 없애는 것은 깔끔한 코드를 유지하는 가장 기본적인 원리 중 하나입니다. 위의 코드의 경우 데이터베이스의 결괏값을 배열로 생성하는 것과 HTML을 생성하는 로직이 엮여있음으로 인해 불필요하게 데이터베이스 조회 결과를 배열로 생성하는 코드가 중복되었습니다.

if($mode === “app”) {
while($task = $result->fetch(MYSQLI_ASSOC)) {
}
} else {
while($task = $result->fetch(MYSQLI_ASSOC)) {
}
}

스파게티 예제에서 뷰를 분리해 봅시다

만약에 데이터를 fetch 하는 것과 HTML을 생성하는 로직이 엮여 있지 않고 분리되어 있었다면 어땠을까요? 먼저 데이터를 fetch 하는 것과 HTML을 생성하는 로직을 분리해봅시다. 방법은 간단합니다. 코드 상단에서 데이터베이스에서 조회한 값들을 응답 생성 시 사용할 수 있는 형태(여기에서는 배열)로 미리 만들어두면 됩니다.

<?php
$conn = new mysqli(HOST, ID, PW, DB);
if ($conn->connect_error) {
die($conn->connect_error);
}
$tasks = [];
$result = $conn->query(“SELECT * FROM tasks”);
if($result) {
while($task = $result->fetch(MYSQLI_ASSOC)) {
$tasks[] = $task;
}
}
echo “<!DOCTYPE html>
<html>
<head>
<meta charset=’utf-8'>
</head>
<body>
“;
if($tasks) {
echo “<ul”>;
foreach($tasks as $task) {
echo “<li>” . $task[‘name’] . “</li>”;
}
echo “</ul>”;
} else {
echo “<p>할 일이 없습니다.</p>”;
}
echo “</body>
</html>”;

PHP로 HTML를 생성하니 영 보기가 불편하네요. 응답을 생성하는 부분은 PHP의 장점을 살려 PHP로 HTML 코드를 생성하는 게 아니고 HTML 코드 사이에 PHP를 끼워 넣는 방식으로 변경해봅시다.

<?php
$conn = new mysqli(HOST, ID, PW, DB);
if ($conn->connect_error) {
die($conn->connect_error);
}
$tasks = [];
$result = $conn->query(“SELECT * FROM tasks”);
if($result) {
while($task = $result->fetch(MYSQLI_ASSOC)) {
$tasks[] = $task;
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset=’utf-8'>
</head>
<body>
<?php if(($tasks)): ?>
<ul>
<?php foreach($tasks as $task): ?>
<li><?=$task[‘name’]?></li>
<?php endforeach;?>
</ul>
<?php else: ?>
<p>할 일이 없습니다.</p>
<?php endif;?>
</body>
</html>

한결 읽기가 쉬워졌지요? 특히 화면이 어떻게 구성되어 있고, 어떤 로직에 의해 생성되는지 한 눈에 파악되는 것을 알 수 있을 것입니다. 굳이 MVC가 아니더라도 위와 같이 비즈니스 로직(데이터베이스에서 데이터를 조회하는 등)과 표현 로직만 분리해도 가독성이 크게 향상됩니다.

한발 더 나아가 HTML 응답을 생성하는 코드를 task-list-html.php 라는 파일로 분리해봅시다.

task-list-html.php

<!DOCTYPE html>
<html>
<head>
<meta charset=’utf-8'>
</head>
<body>
<?php if(count($tasks) > 0): ?>
<ul>
<?php foreach($tasks as $task): ?>
<li><?=$task[‘name’]?></li>
<?php endforeach;?>
</ul>
<?php else: ?>
<p>할 일이 없습니다.</p>
<?php endif;?>
</body>
</html>

task.php

<?php
$conn = new mysqli(HOST, ID, PW, DB);
if ($conn->connect_error) {
die($conn->connect_error);
}
$tasks = [];
$result = $conn->query(“SELECT * FROM tasks”);
if($result) {
while($task = $result->fetch(MYSQLI_ASSOC)) {
$tasks[] = $task;
}
}
require “./task_list_html.php”;

직접 파일을 require 하는 대신 View 클래스를 만들어 객체지향 방식으로 처리할 수도 있습니다만, 본 예제에서는 논의의 편의를 위해 View 클래스를 만드는 단계까지는 가지 않도록 하겠습니다.

관심사 분리를 통해 얻게 되는 이점 중 하나로 한 부문을 개선하거나 수정할 때 다른 부문에 대해 자세히 알 필요가 없어지고, 다른 부문이 변하는 것에도 신경 쓸 필요가 없어진다는 점을 들었었습니다. 위의 코드를 보면 뷰가 별개의 파일로 분리됨으로써, 만약 프론트엔드 개발자가 화면 작업을 하는 경우 데이터가 처리되는 task.php에 대해 자세히 알 필요도, 신경 쓸 필요도 없음을 알 수 있습니다. 반대로 백엔드 개발자 역시 화면에 대해 신경 쓰지 않고 데이터 처리에만 신경 쓰면 되겠죠.

이제 뷰가 분리된 상태의 예제에서 앱에서 요청 시 JSON 형식으로 응답해야 한다는 추가 요청사항을 처리해봅시다.

다음과 같이 JSON 형식으로 응답을 생성하는 task-list-json.php 를 생성한 후, $mode 값에 따라 app 이면 task-list-json.php 를 그렇지 않으면 task-list-html.php 를 로드하여 응답을 전송하는 방식으로 간단하게 처리할 수 있습니다.

task-list-json.php

<?php
echo json_encode($tasks);

task.php

<?php
$conn = new mysqli(HOST, ID, PW, DB);
if ($conn->connect_error) {
die($conn->connect_error);
}
$result = $conn->query(“SELECT * FROM tasks”);
if($result) {
while($task = $result->fetch(MYSQLI_ASSOC)) {
$tasks[] = $task;
}
}
# 이 라인의 위쪽은 기존과 동일.$mode = filter_var($_GET[‘mode’], FILTER_SANITIZE_STRING);if($mode === “app”) {
include “./task-list-json.php”;
} else {
include “./task-list-html.php”;
}

요구사항이 추가되었지만, 여전히 소스코드가 깔끔하게 유지됩니다.

모델을 분리해 봅시다

앞서 뷰를 분리해서 task.php 에서 ‘할 일 목록을 어떻게 표현하지?’ 라는 관심사를 제거했습니다. 눈치가 빠르신 분들은 벌써 눈치를 채셨겠지만, 이 예제에서는 task.php 가 컨트롤러입니다.

이번에는 남아있는 관심사 중 모델에 해당하는 데이터베이스와 관련된 관심사를 모두 제거해보겠습니다. tasks 테이블과 상호작용하는 역할을 담당하는 TaskModel 클래스를 TaskModel.php 라는 파일을 만들고 스파게티 예제에서 할 일 목록을 조회하는 코드를 이 파일로 옮겨보겠습니다. 사실 DB에 연결하는 것은 TaskModel의 관심사가 아니므로 이 역시도 따로 분리하면 좋으나 논의의 편의를 위해 따로 분리하진 않겠습니다.

TaskModel.php

class TaskModel
{
public function all()
{
$tasks = [];
$conn = new mysqli(HOST, ID, PW, DB);
if ($conn->connect_error) {
die($conn->connect_error);
}
$result = $conn->query(“SELECT * FROM tasks”); if($result) {
$tasks = $result->fetch_all();
}
return $tasks;
}
}

task.php

<?php
require_once __DIR__ . “./vendor/autoload.php”;
$taskModel = new TaskModel();
$tasks = $taskModel->all();
$mode = filter_var($_GET[‘mode’], FILTER_SANITIZE_STRING);if($mode === “app”) {
include “./task-list-json.php”;
} else {
include “./task-list-html.php”;
}

만약 task.php 이외의 파일에서 또 전체 할 일 목록을 조회할 일이 필요했다면, 파일마다 데이터베이스에 접속해서 할 일 목록을 조회하는 코드가 중복되었을 것입니다. 하지만 이제는 전체 할 일 목록이 필요하면 어디서든 TaskModel 클래스의 all() 함수를 호출하여 재사용하면 됩니다.

이로써 스파게티 예제에서 task.php 에는 컨트롤러 로직만 남기고 모델과 뷰를 별도의 클래스(와 파일)로 분리하여 MVC 패턴을 적용해 보면서 MVC를 사용하면 좋은 점을 알아봤습니다. MVC 패턴을 사용하면 좋은 점은 결국 관심사를 분리하여 얻게 되는 장점과 같습니다. 가독성과 코드 재사용성이 향상되고, 애플리케이션이 유연해지며, 협업이 쉬워집니다. MVC 패턴의 의의는 애플리케이션의 관심사를 어떻게 나눌 것이냐에 대한 가이드를 제시해준다는 점입니다. 예제를 통해 알 수 있었듯 응답을 생성하는 뷰와 데이터를 가공하는 모델을 분리한다는 간단한 아이디어만으로도 훨씬 읽기 쉽고 유지보수 하기 쉬도록 웹 애플리케이션의 구조를 향상할 수 있습니다.

이 글이 포함된, 바쁜 팀장님 대신 알려주는 신입 PHP 개발자 안내서가 출간되었습니다. 어떤 내용이 담겨있는지 목차를 확인하러 가보세요. 흥미로울 것입니다.

--

--

jijipapa

Seeking Alpha $OHM, $KLIMA, $ABI, $BTRFLY, $LUNA, $MINE