Ver3.5 PHP로 구현하는 MVC 아키텍처

PHP로 구현하는 MVC 아키텍처
야, 솔직히 말해봐. 처음 PHP 코드 짤 때 어땠어? 😅
HTML 파일 하나에 DB 쿼리, 비즈니스 로직, 화면 출력까지 다 때려박고 "이거 돌아가잖아? 완성!" 했던 경험, 다들 있잖아.
근데 그 코드 한 달 뒤에 다시 열어봤을 때의 그 공포감... 🫠 "이게 내가 짠 코드라고?"
바로 그 순간을 위해 오늘 우리가 이야기할 게 MVC 아키텍처야.
MVC는 Model-View-Controller의 약자인데, 쉽게 말하면 코드를 역할별로 깔끔하게 나눠서 관리하는 설계 패턴이야.
PHP로 이걸 직접 구현해보면서, "아 이래서 다들 MVC 쓰는구나!" 하는 그 감각을 같이 느껴보자. 💡
🤔 MVC가 뭔데? 진짜로 설명해줄게
MVC는 1970년대 Trygve Reenskaug라는 노르웨이 컴퓨터 과학자가 Smalltalk 언어를 위해 처음 제안한 디자인 패턴이야.
그게 지금까지 살아남아서 PHP, Java, Python, Ruby 등 거의 모든 웹 프레임워크의 기반이 됐어. 진짜 레전드 패턴이지. 🏆
"이 사용자가 성인인지 확인해줘", "이 상품의 재고가 있는지 체크해줘" 같은 로직이 여기 들어가.
데이터를 어떻게 예쁘게 보여줄지만 생각하면 돼. 비즈니스 로직은 절대 여기 없어야 해!
교통 정리하는 경찰관 같은 존재! 🚦
📁 프로젝트 구조 잡기 — 집부터 제대로 짓자
MVC를 PHP로 구현할 때 가장 먼저 해야 할 일은 디렉토리 구조를 잡는 것이야.
구조가 엉망이면 나중에 파일 찾다가 시간 다 날아가거든. 😤
my-mvc-app/
├── app/
│ ├── controllers/
│ │ ├── HomeController.php
│ │ └── UserController.php
│ ├── models/
│ │ ├── User.php
│ │ └── Post.php
│ └── views/
│ ├── home/
│ │ └── index.php
│ ├── user/
│ │ ├── index.php
│ │ └── show.php
│ └── layouts/
│ └── main.php
├── core/
│ ├── App.php
│ ├── Controller.php
│ ├── Model.php
│ └── Router.php
├── config/
│ └── database.php
├── public/
│ ├── index.php ← 진입점(Entry Point)
│ ├── css/
│ └── js/
└── .htaccess
이 구조에서 핵심은 public/index.php가 모든 요청의 진입점이라는 거야.
외부에서는 public 폴더만 접근 가능하고, 나머지 로직은 다 숨겨져 있어. 보안상으로도 훨씬 좋지! 🔒
.htaccess — URL 라우팅의 마법
Apache 서버를 쓴다면 .htaccess 파일로 모든 요청을 index.php로 보내야 해.
Options -MultiViews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)$ public/index.php [QSA,L]
이렇게 하면 /user/profile이든 /post/123이든 전부 public/index.php로 들어와서 우리가 만든 라우터가 처리하게 돼. 🎯
⚙️ 코어 시스템 구현 — 엔진 만들기
1단계: 진입점 (public/index.php)
모든 요청이 여기서 시작돼. 오토로더 설정하고 앱을 실행하는 게 전부야.
<?php
// 오토로더 설정 - 클래스 파일 자동으로 불러오기
spl_autoload_register(function($className) {
$paths = [
'../app/controllers/',
'../app/models/',
'../core/'
];
foreach ($paths as $path) {
$file = $path . $className . '.php';
if (file_exists($file)) {
require_once $file;
return;
}
}
});
// 앱 시작!
require_once '../core/App.php';
$app = new App();
$app->run();
2단계: 라우터 (core/Router.php)
URL을 분석해서 어떤 컨트롤러의 어떤 메서드를 실행할지 결정하는 핵심 컴포넌트야.
<?php
class Router {
private array $routes = [];
// 라우트 등록
public function get(string $path, string $controller, string $method): void {
$this->routes['GET'][$path] = [
'controller' => $controller,
'method' => $method
];
}
public function post(string $path, string $controller, string $method): void {
$this->routes['POST'][$path] = [
'controller' => $controller,
'method' => $method
];
}
// URL 파싱 및 라우팅
public function dispatch(string $url, string $httpMethod): void {
$url = trim($url, '/');
$httpMethod = strtoupper($httpMethod);
// 정확한 매칭 먼저 시도
if (isset($this->routes[$httpMethod][$url])) {
$route = $this->routes[$httpMethod][$url];
$this->callAction($route['controller'], $route['method'], []);
return;
}
// 동적 파라미터 매칭 (예: /user/{id})
foreach ($this->routes[$httpMethod] ?? [] as $pattern => $route) {
$regex = preg_replace('/\{[^}]+\}/', '([^/]+)', $pattern);
$regex = '#^' . $regex . '$#';
if (preg_match($regex, $url, $matches)) {
array_shift($matches); // 전체 매칭 제거
$this->callAction($route['controller'], $route['method'], $matches);
return;
}
}
// 404 처리
http_response_code(404);
echo "404 - 페이지를 찾을 수 없어요 😢";
}
private function callAction(string $controller, string $method, array $params): void {
if (!class_exists($controller)) {
throw new \Exception("컨트롤러 '{$controller}'를 찾을 수 없어요!");
}
$controllerInstance = new $controller();
if (!method_exists($controllerInstance, $method)) {
throw new \Exception("메서드 '{$method}'를 찾을 수 없어요!");
}
call_user_func_array([$controllerInstance, $method], $params);
}
}
3단계: 앱 클래스 (core/App.php)
<?php
class App {
private Router $router;
public function __construct() {
$this->router = new Router();
$this->registerRoutes();
}
private function registerRoutes(): void {
// 라우트 등록
$this->router->get('', 'HomeController', 'index');
$this->router->get('home', 'HomeController', 'index');
$this->router->get('users', 'UserController', 'index');
$this->router->get('users/{id}', 'UserController', 'show');
$this->router->post('users', 'UserController', 'store');
$this->router->get('users/{id}/edit', 'UserController', 'edit');
$this->router->post('users/{id}/update', 'UserController', 'update');
$this->router->post('users/{id}/delete', 'UserController', 'destroy');
}
public function run(): void {
// 현재 URL 가져오기
$url = $_SERVER['REQUEST_URI'] ?? '/';
// 쿼리스트링 제거
$url = strtok($url, '?');
// 베이스 경로 제거 (서브디렉토리에 설치된 경우)
$basePath = dirname($_SERVER['SCRIPT_NAME']);
if ($basePath !== '/') {
$url = str_replace($basePath, '', $url);
}
$httpMethod = $_SERVER['REQUEST_METHOD'];
// POST 메서드 오버라이드 (_method 파라미터로 PUT, DELETE 지원)
if ($httpMethod === 'POST' && isset($_POST['_method'])) {
$httpMethod = strtoupper($_POST['_method']);
}
$this->router->dispatch($url, $httpMethod);
}
}
🏛️ 베이스 클래스 구현 — 공통 기능 묶기
각 컴포넌트의 공통 기능을 베이스 클래스로 만들어두면, 실제 컨트롤러나 모델 작성이 훨씬 편해져. 코드 중복도 확 줄고! ✂️
베이스 컨트롤러 (core/Controller.php)
<?php
class Controller {
// View 렌더링 메서드
protected function view(string $viewPath, array $data = []): void {
// 데이터를 변수로 추출 (extract 사용)
extract($data);
// 뷰 파일 경로
$viewFile = '../app/views/' . str_replace('.', '/', $viewPath) . '.php';
if (!file_exists($viewFile)) {
throw new \Exception("뷰 파일을 찾을 수 없어요: {$viewFile}");
}
// 레이아웃이 있으면 레이아웃 안에 뷰 삽입
$layoutFile = '../app/views/layouts/main.php';
if (file_exists($layoutFile)) {
// 뷰 내용을 버퍼에 담기
ob_start();
require $viewFile;
$content = ob_get_clean();
// 레이아웃에 content 변수로 전달
require $layoutFile;
} else {
require $viewFile;
}
}
// JSON 응답
protected function json(mixed $data, int $statusCode = 200): void {
http_response_code($statusCode);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
}
// 리다이렉트
protected function redirect(string $url): void {
header("Location: {$url}");
exit;
}
// POST 데이터 가져오기 (XSS 방지)
protected function input(string $key, mixed $default = null): mixed {
$value = $_POST[$key] ?? $_GET[$key] ?? $default;
if (is_string($value)) {
return htmlspecialchars(trim($value), ENT_QUOTES, 'UTF-8');
}
return $value;
}
// 간단한 유효성 검사
protected function validate(array $data, array $rules): array {
$errors = [];
foreach ($rules as $field => $rule) {
$ruleList = explode('|', $rule);
foreach ($ruleList as $r) {
if ($r === 'required' && empty($data[$field])) {
$errors[$field][] = "{$field} 필드는 필수입니다.";
}
if (str_starts_with($r, 'min:')) {
$min = (int) substr($r, 4);
if (strlen($data[$field] ?? '') < $min) {
$errors[$field][] = "{$field}은 최소 {$min}자 이상이어야 합니다.";
}
}
if (str_starts_with($r, 'max:')) {
$max = (int) substr($r, 4);
if (strlen($data[$field] ?? '') > $max) {
$errors[$field][] = "{$field}은 최대 {$max}자 이하여야 합니다.";
}
}
if ($r === 'email' && !filter_var($data[$field] ?? '', FILTER_VALIDATE_EMAIL)) {
$errors[$field][] = "{$field}은 올바른 이메일 형식이 아닙니다.";
}
}
}
return $errors;
}
}
베이스 모델 (core/Model.php)
PDO를 활용한 베이스 모델이야. DB 연결과 기본 CRUD 쿼리를 여기서 처리해.
<?php
class Model {
protected static ?PDO $db = null;
protected string $table = '';
protected string $primaryKey = 'id';
// DB 연결 (싱글톤 패턴)
protected static function getDB(): PDO {
if (self::$db === null) {
$config = require '../config/database.php';
$dsn = "mysql:host={$config['host']};dbname={$config['dbname']};charset=utf8mb4";
self::$db = new PDO($dsn, $config['username'], $config['password'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
return self::$db;
}
// 전체 조회
public function all(): array {
$stmt = self::getDB()->query("SELECT * FROM {$this->table}");
return $stmt->fetchAll();
}
// ID로 단건 조회
public function find(int $id): array|false {
$stmt = self::getDB()->prepare(
"SELECT * FROM {$this->table} WHERE {$this->primaryKey} = ?"
);
$stmt->execute([$id]);
return $stmt->fetch();
}
// 조건 조회
public function where(string $column, mixed $value): array {
$stmt = self::getDB()->prepare(
"SELECT * FROM {$this->table} WHERE {$column} = ?"
);
$stmt->execute([$value]);
return $stmt->fetchAll();
}
// 생성
public function create(array $data): int {
$columns = implode(', ', array_keys($data));
$placeholders = implode(', ', array_fill(0, count($data), '?'));
$stmt = self::getDB()->prepare(
"INSERT INTO {$this->table} ({$columns}) VALUES ({$placeholders})"
);
$stmt->execute(array_values($data));
return (int) self::getDB()->lastInsertId();
}
// 수정
public function update(int $id, array $data): bool {
$setClause = implode(', ', array_map(fn($col) => "{$col} = ?", array_keys($data)));
$stmt = self::getDB()->prepare(
"UPDATE {$this->table} SET {$setClause} WHERE {$this->primaryKey} = ?"
);
return $stmt->execute([...array_values($data), $id]);
}
// 삭제
public function delete(int $id): bool {
$stmt = self::getDB()->prepare(
"DELETE FROM {$this->table} WHERE {$this->primaryKey} = ?"
);
return $stmt->execute([$id]);
}
// 커스텀 쿼리
public function query(string $sql, array $params = []): array {
$stmt = self::getDB()->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
}
🛠️ 실전! User CRUD 구현해보기
이제 진짜 재미있는 부분이야. 실제로 사용자 관리 기능을 MVC로 구현해볼게.
이걸 이해하면 어떤 기능이든 MVC로 만들 수 있어! 💪
User 모델 (app/models/User.php)
<?php
class User extends Model {
protected string $table = 'users';
// 이메일로 사용자 찾기
public function findByEmail(string $email): array|false {
$result = $this->where('email', $email);
return $result[0] ?? false;
}
// 비밀번호 해싱해서 생성
public function createWithPassword(array $data): int {
$data['password'] = password_hash($data['password'], PASSWORD_BCRYPT);
$data['created_at'] = date('Y-m-d H:i:s');
return $this->create($data);
}
// 비밀번호 검증
public function verifyPassword(string $plainPassword, string $hashedPassword): bool {
return password_verify($plainPassword, $hashedPassword);
}
// 최근 가입자 조회
public function getRecentUsers(int $limit = 10): array {
return $this->query(
"SELECT id, name, email, created_at FROM users ORDER BY created_at DESC LIMIT ?",
[$limit]
);
}
// 검색
public function search(string $keyword): array {
return $this->query(
"SELECT * FROM users WHERE name LIKE ? OR email LIKE ?",
["%{$keyword}%", "%{$keyword}%"]
);
}
}
User 컨트롤러 (app/controllers/UserController.php)
<?php
class UserController extends Controller {
private User $userModel;
public function __construct() {
$this->userModel = new User();
}
// GET /users - 사용자 목록
public function index(): void {
$keyword = $_GET['search'] ?? '';
if ($keyword) {
$users = $this->userModel->search($keyword);
} else {
$users = $this->userModel->all();
}
$this->view('user.index', [
'users' => $users,
'keyword' => $keyword,
'title' => '사용자 목록'
]);
}
// GET /users/{id} - 사용자 상세
public function show(string $id): void {
$user = $this->userModel->find((int) $id);
if (!$user) {
http_response_code(404);
$this->view('errors.404', ['title' => '사용자를 찾을 수 없어요']);
return;
}
$this->view('user.show', [
'user' => $user,
'title' => $user['name'] . '님의 프로필'
]);
}
// POST /users - 사용자 생성
public function store(): void {
$data = [
'name' => $this->input('name'),
'email' => $this->input('email'),
'password' => $_POST['password'] ?? '', // 해싱 전이라 raw로
];
// 유효성 검사
$errors = $this->validate($data, [
'name' => 'required|min:2|max:50',
'email' => 'required|email',
'password' => 'required|min:8',
]);
if (!empty($errors)) {
$this->json(['success' => false, 'errors' => $errors], 422);
return;
}
// 이메일 중복 체크
if ($this->userModel->findByEmail($data['email'])) {
$this->json(['success' => false, 'message' => '이미 사용 중인 이메일이에요!'], 409);
return;
}
$userId = $this->userModel->createWithPassword($data);
$this->json([
'success' => true,
'message' => '사용자가 생성됐어요! 🎉',
'userId' => $userId
], 201);
}
// POST /users/{id}/update - 사용자 수정
public function update(string $id): void {
$user = $this->userModel->find((int) $id);
if (!$user) {
$this->json(['success' => false, 'message' => '사용자를 찾을 수 없어요'], 404);
return;
}
$data = [
'name' => $this->input('name'),
'email' => $this->input('email'),
'updated_at' => date('Y-m-d H:i:s'),
];
$errors = $this->validate($data, [
'name' => 'required|min:2|max:50',
'email' => 'required|email',
]);
if (!empty($errors)) {
$this->json(['success' => false, 'errors' => $errors], 422);
return;
}
$this->userModel->update((int) $id, $data);
$this->redirect('/users/' . $id);
}
// POST /users/{id}/delete - 사용자 삭제
public function destroy(string $id): void {
$user = $this->userModel->find((int) $id);
if (!$user) {
$this->json(['success' => false, 'message' => '사용자를 찾을 수 없어요'], 404);
return;
}
$this->userModel->delete((int) $id);
$this->json(['success' => true, 'message' => '삭제 완료! 👋']);
}
}
View 파일들
뷰는 최대한 단순하게! 로직은 절대 여기 넣으면 안 돼. 데이터 출력만!
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($title ?? 'My MVC App') ?></title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav>
<a href="/">홈</a>
<a href="/users">사용자 목록</a>
</nav>
<main>
<?= $content ?>
</main>
<footer>
<p>My MVC App © 2024</p>
</footer>
</body>
</html>
<div class="container">
<h1>사용자 목록 👥</h1>
<!-- 검색 폼 -->
<form method="GET" action="/users">
<input type="text" name="search"
value="<?= htmlspecialchars($keyword) ?>"
placeholder="이름 또는 이메일 검색...">
<button type="submit">검색</button>
</form>
<!-- 사용자 목록 -->
<?php if (empty($users)): ?>
<p>사용자가 없어요 😢</p>
<?php else: ?>
<table>
<thead>
<tr>
<th>ID</th>
<th>이름</th>
<th>이메일</th>
<th>가입일</th>
<th>액션</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td><?= $user['id'] ?></td>
<td><?= htmlspecialchars($user['name']) ?></td>
<td><?= htmlspecialchars($user['email']) ?></td>
<td><?= $user['created_at'] ?></td>
<td>
<a href="/users/<?= $user['id'] ?>">보기</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
🗄️ 데이터베이스 설정
<?php
return [
'host' => 'localhost',
'dbname' => 'my_mvc_app',
'username' => 'root',
'password' => 'your_password',
'charset' => 'utf8mb4',
];
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
🔐 심화: 미들웨어 패턴 추가하기
실제 프로젝트에서는 인증 체크, 로깅, CORS 처리 같은 공통 작업이 필요해.
이걸 미들웨어(Middleware)로 처리하면 컨트롤러가 훨씬 깔끔해져! 🧹
인증 확인, 로그 기록, 입력 필터링 등을 여기서 처리해.
<?php
interface MiddlewareInterface {
public function handle(callable $next): void;
}
// 인증 미들웨어
class AuthMiddleware implements MiddlewareInterface {
public function handle(callable $next): void {
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: /login');
exit;
}
// 다음 미들웨어 또는 컨트롤러 실행
$next();
}
}
// 로깅 미들웨어
class LogMiddleware implements MiddlewareInterface {
public function handle(callable $next): void {
$start = microtime(true);
$method = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];
// 요청 로그
error_log("[" . date('Y-m-d H:i:s') . "] {$method} {$uri}");
$next();
// 응답 시간 로그
$elapsed = round((microtime(true) - $start) * 1000, 2);
error_log(" → 처리 시간: {$elapsed}ms");
}
}
// 미들웨어 파이프라인
class Pipeline {
private array $middlewares = [];
public function pipe(MiddlewareInterface $middleware): static {
$this->middlewares[] = $middleware;
return $this;
}
public function run(callable $destination): void {
$pipeline = array_reduce(
array_reverse($this->middlewares),
fn($carry, $middleware) => fn() => $middleware->handle($carry),
$destination
);
$pipeline();
}
}
라우터에서 미들웨어를 적용하는 방법:
<?php
// App.php에서 미들웨어 파이프라인 사용
$pipeline = new Pipeline();
$pipeline
->pipe(new LogMiddleware())
->pipe(new AuthMiddleware()) // 인증이 필요한 라우트에만
->run(function() use ($router, $url, $httpMethod) {
$router->dispatch($url, $httpMethod);
});
🎯 서비스 레이어 — MVC를 더 강하게
순수 MVC에서 한 단계 더 나아가면 서비스 레이어(Service Layer)가 등장해.
복잡한 비즈니스 로직이 컨트롤러에 쌓이기 시작하면, 서비스 클래스로 분리하는 게 정석이야. 🏆
Controller → Service → Model → Database
컨트롤러는 요청/응답만, 서비스는 비즈니스 로직만, 모델은 DB 접근만!
<?php
class UserService {
private User $userModel;
public function __construct() {
$this->userModel = new User();
}
// 회원가입 처리 (복잡한 비즈니스 로직)
public function register(array $data): array {
// 1. 이메일 중복 체크
if ($this->userModel->findByEmail($data['email'])) {
throw new \RuntimeException('이미 사용 중인 이메일이에요!');
}
// 2. 비밀번호 강도 체크
if (!$this->isStrongPassword($data['password'])) {
throw new \RuntimeException('비밀번호는 대소문자, 숫자, 특수문자를 포함해야 해요!');
}
// 3. 사용자 생성
$userId = $this->userModel->createWithPassword([
'name' => $data['name'],
'email' => $data['email'],
'password' => $data['password'],
]);
// 4. 환영 이메일 발송 (실제로는 메일 서비스 호출)
$this->sendWelcomeEmail($data['email'], $data['name']);
// 5. 생성된 사용자 반환
return $this->userModel->find($userId);
}
private function isStrongPassword(string $password): bool {
return strlen($password) >= 8
&& preg_match('/[A-Z]/', $password)
&& preg_match('/[a-z]/', $password)
&& preg_match('/[0-9]/', $password)
&& preg_match('/[^A-Za-z0-9]/', $password);
}
private function sendWelcomeEmail(string $email, string $name): void {
// 실제 메일 발송 로직
// mail($email, "환영합니다!", "안녕하세요, {$name}님!");
error_log("환영 이메일 발송: {$email}");
}
}
이렇게 서비스 레이어를 쓰면 컨트롤러가 엄청 깔끔해져:
<?php
class UserController extends Controller {
private UserService $userService;
public function __construct() {
$this->userService = new UserService();
}
public function store(): void {
try {
$user = $this->userService->register([
'name' => $this->input('name'),
'email' => $this->input('email'),
'password' => $_POST['password'] ?? '',
]);
$this->json(['success' => true, 'user' => $user], 201);
} catch (\RuntimeException $e) {
$this->json(['success' => false, 'message' => $e->getMessage()], 400);
}
}
}
💡 실전에서 꼭 알아야 할 팁들
<?php if ($user->isAdmin() && $user->hasPermission('edit')) ?> 이런 복잡한 조건은 컨트롤러에서 처리하고, 뷰에는 단순한 boolean 변수만 전달해. 뷰는 멍청하게 유지해야 해(Dumb View 원칙).
모델이 HTML을 리턴하는 순간 MVC가 무너지기 시작해. 모델은 순수한 데이터(배열, 객체)만 반환해야 해.
컨트롤러 메서드가 30줄을 넘어가기 시작하면 서비스 레이어로 분리할 타이밍이야. 컨트롤러는 교통 정리만 해야 해.
뷰에서 데이터를 출력할 때 항상
htmlspecialchars()를 써. 아니면 헬퍼 함수 e()를 만들어서 써.
실제 프로젝트에서는
spl_autoload_register 대신 Composer의 PSR-4 오토로더를 써. 훨씬 표준적이고 안정적이야.
{
"autoload": {
"psr-4": {
"App\\": "app/",
"Core\\": "core/"
}
}
}
그 다음 composer dump-autoload 실행하면 끝!
🆚 직접 구현 vs 프레임워크 — 뭘 써야 해?
이렇게 MVC를 직접 구현해봤는데, 실제 프로젝트에서는 어떻게 해야 할까? 🤔
솔직하게 비교해줄게.
재능넷 같은 플랫폼에서 PHP 개발 재능을 거래할 때도, 클라이언트들이 가장 많이 요청하는 건 Laravel 기반 프로젝트야.
하지만 MVC 원리를 직접 구현해본 개발자는 프레임워크를 써도 훨씬 깊이 이해하고 활용할 수 있어. 기초가 탄탄해야 응용도 잘 되는 법! 💪
실제로 Laravel도 내부적으로 우리가 만든 것과 거의 동일한 구조야:
routes/web.php → 라우터
app/Http/Controllers/ → 컨트롤러
app/Models/ → 모델 (Eloquent ORM)
resources/views/ → 뷰 (Blade 템플릿)
우리가 만든 구조랑 완전히 같지? 😄
🧪 MVC의 숨겨진 장점 — 테스트하기 쉬워!
MVC의 진짜 강점 중 하나는 단위 테스트(Unit Test)가 쉽다는 거야.
역할이 분리돼 있으니까 각 컴포넌트를 독립적으로 테스트할 수 있어! 🎯
<?php
use PHPUnit\Framework\TestCase;
class UserModelTest extends TestCase {
private User $userModel;
protected function setUp(): void {
$this->userModel = new User();
}
public function testPasswordHashing(): void {
// 비밀번호 해싱 테스트
$userId = $this->userModel->createWithPassword([
'name' => '테스트 유저',
'email' => 'test@example.com',
'password' => 'TestPass123!',
]);
$user = $this->userModel->find($userId);
// 원본 비밀번호와 다른지 확인
$this->assertNotEquals('TestPass123!', $user['password']);
// 비밀번호 검증이 되는지 확인
$this->assertTrue(
$this->userModel->verifyPassword('TestPass123!', $user['password'])
);
// 정리
$this->userModel->delete($userId);
}
public function testFindByEmail(): void {
$result = $this->userModel->findByEmail('nonexistent@example.com');
$this->assertFalse($result);
}
}
🎓 마무리 — 이제 너도 MVC 마스터!
오늘 우리가 함께 만든 것들을 정리해볼게:
MVC는 처음엔 "이렇게까지 해야 해?" 싶을 수 있어.
근데 프로젝트가 커지고, 팀원이 늘어나고, 6개월 뒤에 코드를 다시 봤을 때 — 그때 MVC의 진가를 느끼게 될 거야. 😌
재능넷에서 PHP 개발 재능을 공유하거나 의뢰할 때도, MVC 아키텍처를 이해하고 적용할 수 있는 개발자는 확실히 더 높은 가치를 인정받아. 코드의 품질이 곧 실력의 증거니까! 🏆
자, 이제 직접 만들어봐. 코드는 읽는 게 아니라 짜는 것이니까! 💻🔥
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.