PHP는 비교적 빠르게 기능을 만들 수 있는 언어지만, 프로젝트가 커질수록 구조가 무너지기 쉽다.
초기에 패턴과 책임 분리를 정해 두지 않으면, 기능 추가와 수정이 어려운 형태로 굳어지기 때문이다.
실무에서는 “패턴을 얼마나 많이 아는가”보다 “일관된 구조로 유지하는가”가 더 중요하다.
아래는 PHP에서 자주 쓰는 설계 패턴과, 개발 시 흔히 터지는 주의점을 정리한 내용이다.
1. PHP에서 자주 쓰는 개발 설계 패턴
1) MVC 패턴
MVC는 Controller가 요청을 받고, Model이 데이터를 처리하고, View가 화면을 렌더링하는 구조다.
PHP 웹 프로젝트에서 가장 기본이 되는 분리 방식이다.
- Controller — 라우팅, 입력 검증, 응답 생성
- Model — 도메인/데이터 처리
- View — 템플릿 렌더링
Controller가 비대해지는 순간 유지보수가 급격히 어려워진다.
Controller는 얇게 유지하고, 핵심 로직은 Service로 내려야 한다.
2) Service Layer 패턴
Service Layer는 비즈니스 로직을 한 곳에 모아 관리하는 구조다.
컨트롤러에서 조건 분기와 처리 로직이 늘어나는 것을 막을 수 있다.
final class OrderService
{
public function __construct(
private OrderRepository $orders,
private PaymentGateway $pg
) {}
public function placeOrder(int $memberId, array $items): int
{
$orderId = $this->orders->create($memberId, $items);
$this->pg->reserve($orderId);
return $orderId;
}
}
핵심은 “흐름은 Service에서 통제”하고, 저장소/외부 연동은 하위 객체로 위임하는 것이다.
3) Repository 패턴
Repository는 DB 접근 코드를 한 곳으로 모으는 패턴이다.
쿼리가 컨트롤러나 서비스에 흩어지지 않도록 막아준다.
final class MemberRepository
{
public function __construct(private PDO $pdo) {}
public function findById(int $id): ?array
{
$stmt = $this->pdo->prepare('SELECT id, email FROM member WHERE id = :id');
$stmt->execute([':id' => $id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
}
DB 스키마 변경이 발생해도 수정 범위를 Repository로 제한할 수 있어 유지보수에 유리하다.
4) DI(Dependency Injection)와 IoC
DI는 의존 객체를 내부에서 생성하지 않고 외부에서 주입받는 방식이다.
테스트가 쉬워지고, 구현 교체가 쉬워진다.
Laravel, Symfony 같은 프레임워크는 컨테이너 기반 DI가 기본이다.
프레임워크 없이도 생성 책임을 한 곳으로 모으면 구조가 안정된다.
5) DTO / Value Object
배열로만 데이터를 주고받으면 키 오타와 구조 불일치가 누적된다.
DTO나 값 객체를 도입하면 경계가 명확해지고 검증 포인트가 생긴다.
final class CreatePostDto
{
public function __construct(
public readonly int $memberId,
public readonly string $title,
public readonly string $content
) {}
}
입력값을 DTO로 묶으면 컨트롤러에서 검증 후 서비스로 넘기기가 깔끔해진다.
2. PHP로 개발할 때 주의할 점
1) 전역 상태 사용 최소화
전역 변수, 싱글톤, static 남발은 의존성이 숨는다.
기능이 커질수록 사이드 이펙트가 발생하고, 원인 추적이 어려워진다.
요청 단위 상태는 Request 객체로, 공용 의존성은 DI로 관리하는 편이 안전하다.
2) SQL 문자열 결합 금지
SQL Injection은 여전히 가장 흔한 보안 사고다.
문자열 결합 방식은 제거하고 Prepared Statement로 통일해야 한다.
$stmt = $pdo->prepare('SELECT id, email FROM member WHERE email = :email');
$stmt->execute([':email' => $email]);
ORDER BY 컬럼명 같은 “구조 요소”는 바인딩이 불가능하므로 화이트리스트로 제한해야 한다.
3) 문자셋과 타임존 고정
문자셋이 흔들리면 이모지 저장 오류나 정렬/비교 오류가 발생한다.
DB와 커넥션은 utf8mb4로 통일하는 것이 안전하다.
ALTER DATABASE app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
타임존도 애플리케이션/DB/서버가 다르면 운영 중 시간 데이터가 틀어진다.
기준 타임존을 정하고 저장 규칙을 통일해야 한다.
4) 오류를 숨기지 말고 로깅
운영에서 에러를 화면에 출력하면 정보 노출이 된다.
반대로 에러를 완전히 숨기면 장애 원인을 찾을 수 없다.
운영에서는 에러 출력은 끄고, 파일/중앙 로그로 남기는 방식으로 구성해야 한다.
# PHP-FPM 로그, Nginx 에러 로그 위치는 환경마다 다르므로 운영 서버에서 경로를 고정 관리한다.
tail -f /var/log/nginx/error.log
5) 긴 요청과 배치 작업 분리
웹 요청에서 무거운 작업을 처리하면 타임아웃이 발생한다.
이미지 변환, 대량 정산, 대용량 파일 처리 같은 작업은 큐/배치로 분리하는 편이 안전하다.
분리하지 않으면 요청 지연이 누적되고, PHP-FPM 워커가 묶여 전체 서비스가 느려질 수 있다.
6) 캐시와 세션 스토리지 전략
세션을 파일로 두면 서버가 여러 대일 때 문제가 생긴다.
스케일아웃을 고려한다면 Redis 같은 공용 스토리지로 이동하는 전략이 필요하다.
캐시는 DB 부하를 줄이는 가장 빠른 방법이지만, 무효화 정책이 없으면 장애 원인이 된다.
캐시 키 규칙과 TTL 기준을 먼저 정해 두는 것이 운영에 유리하다.
한 줄 요약
PHP는 MVC 기반으로 Controller를 얇게 유지하고 Service·Repository로 책임을 분리하는 것이 기본. 전역 상태와 SQL 문자열 결합을 피하고, 문자셋·로깅·배치 분리·세션/캐시 전략을 초기에 정하면 운영 단계에서 구조가 무너지지 않는다.
댓글 0