PH pullh

LANGUAGE DOSSIER

PHP는 커머스와 콘텐츠 현장에서 “빨리 만들고 오래 굴리는” 감각을 잘 안다

이커머스, CMS, 운영 어드민, 사내 툴. 서비스가 매일 돈과 주문을 다루는 환경에서 PHP는 여전히 실용적인 선택지다.

커머스 개발팀콘텐츠 플랫폼 팀운영 어드민 팀
PHP 커버 이미지
학습 예제 200
카테고리 12
잘 맞는 팀 커머스 운영 팀

복잡한 이론보다 매출과 운영 속도가 먼저인 팀이라면 PHP의 실용성이 생각보다 오래 간다.

요청마다 새로 태어나는 언어

PHP를 다른 언어 다음에 배우는 사람이 처음 부딪히는 것은 문법이 아니라 실행 모델입니다. 일반적인 웹 요청 처리에서 PHP 스크립트는 요청이 들어올 때 시작해 응답을 내보내고 끝납니다. 그 사이에 만든 변수, 열어 둔 객체, 등록한 핸들러는 요청이 끝나면 전부 사라집니다. 다음 요청은 같은 프로세스 위에서 돌더라도 깨끗한 상태에서 다시 시작합니다. 오래 살아 있는 서버 프로세스 안에 상태를 쌓아 두는 Java나 Node의 습관은 여기서 그대로 옮겨지지 않습니다.

PHP가 여전히 유리한 자리, 그리고 아닌 자리

폼과 세션, 관리자 화면, 결제 연동, 콘텐츠 편집처럼 웹의 전형적인 형태를 가진 제품에서 PHP는 여전히 실용적입니다. 서버에서 HTML을 만들어 내려주는 방식이 기본값이고, 데이터베이스 접근과 파일 업로드와 메일 발송 같은 것들이 표준 라이브러리와 성숙한 확장에 이미 갖춰져 있습니다. 사람이 적고 기능은 자주 바뀌는 커머스나 사내 도구 팀이라면 만드는 속도와 운영 안정성의 균형이 좋습니다.

반대로 권하기 어려운 자리도 분명합니다. 수만 개의 연결을 오래 유지하는 실시간 서버, 요청 사이에 무거운 상태를 계속 들고 있어야 하는 시스템, CPU를 갈아 넣는 수치 연산, 그리고 브라우저와 코드를 공유해야 하는 구조에서는 다른 선택지가 더 자연스럽습니다. 데이터 분석과 머신러닝 생태계도 여기에 있지 않습니다. 이런 요구가 제품의 중심이라면 PHP로 밀어붙이는 것보다 역할을 나누는 편이 낫습니다.

요청 생명주기가 설계를 바꾼다

요청마다 아무것도 공유하지 않는 구조에는 대가와 보상이 함께 옵니다. 보상은 격리입니다. 한 요청에서 생긴 메모리 누수나 망가진 전역 상태가 다음 요청으로 넘어가지 않습니다. 배포도 단순해집니다. 파일을 바꾸면 그다음 요청부터 새 코드가 실행되므로, 애플리케이션 서버를 통째로 재시작하지 않아도 됩니다. 오래된 코드베이스가 그토록 오래 굴러가는 이유 중 하나입니다.

대가는 프로세스 안에 뭔가를 쌓아 둘 수 없다는 점입니다. 싱글턴 객체에 캐시를 담아 두는 습관은 여기서 의미가 없습니다. 요청마다 다시 채워지고, 워커 프로세스가 여러 개이므로 서로 내용도 다릅니다. 캐시가 필요하면 Redis 같은 외부 저장소로 나가야 하고, 요청이 끝난 뒤에도 계속 돌아야 하는 작업은 큐 워커나 스케줄러로 넘겨야 합니다. 요청 안에서 무거운 일을 붙잡고 있는 코드는 곧 응답 시간으로 돌아옵니다.

입력을 받는 통로도 다릅니다. PHP는 요청 정보를 슈퍼글로벌이라는 배열로 건네줍니다. 이름은 전역이지만 수명은 요청 하나입니다. 문제는 이것들이 신뢰 경계 바로 바깥에서 온 값이라는 점입니다. 검증하지 않은 값을 그대로 쿼리나 파일 경로에 넣는 코드가 여전히 인터넷에 널려 있으므로, 예제를 베낄 때는 출처를 봅니다. 세션도 기본 설정에서는 서버의 파일로 저장되기 때문에, 서버를 두 대로 늘리는 순간 공유 저장소로 옮겨야 로그인이 유지됩니다.

레거시 PHP와 지금의 PHP는 다른 언어에 가깝다

검색으로 나오는 PHP 코드는 시대가 뒤섞여 있습니다. 한쪽에는 파일 상단에 include를 스무 줄 늘어놓고 문자열을 이어 붙여 SQL을 만드는 코드가 있고, 다른 쪽에는 파일마다 엄격한 타입 선언을 켜고 인자와 반환값에 타입을 붙이며 예외로 오류를 다루는 코드가 있습니다. 둘은 문법을 공유할 뿐 작업 방식이 다릅니다.

지금 배운다면 후자에서 시작해야 합니다. 파일 첫 줄에 strict_types를 선언하면 느슨한 형 변환이 조용히 일어나지 않고 타입이 맞지 않을 때 바로 실패합니다. 클래스 프로퍼티와 함수 시그니처에 타입을 적으면 정적 분석 도구가 실행 전에 문제를 잡아 줍니다. 오류 코드를 반환값으로 돌려주는 대신 예외로 다루는 방식은 PHP 오류 처리 가이드에서, 클래스와 인터페이스를 나누는 기준은 PHP 객체지향 가이드에서 이어 볼 수 있습니다.

파일을 불러오는 방식도 바뀌었습니다. 요즘은 include 체인을 직접 관리하지 않습니다. Composer가 의존성을 내려받고, PSR-4 규칙에 따라 네임스페이스와 디렉터리를 대응시켜 클래스가 처음 쓰이는 순간 알아서 파일을 읽어 옵니다. 새 프로젝트를 만들 때 가장 먼저 하는 일이 composer.json을 여는 것이라면 방향은 맞게 잡은 것입니다.

프레임워크가 언어보다 큰 결정이다

실무 PHP에서 하루의 대부분은 언어 기능이 아니라 프레임워크 관례를 쓰는 시간입니다. Laravel이나 Symfony는 라우팅, 의존성 주입 컨테이너, ORM, 데이터베이스 마이그레이션, 큐, 스케줄러, 입력 검증, 템플릿 엔진을 한 묶음으로 제공합니다. 팀이 어느 쪽을 쓰느냐에 따라 디렉터리 구조도, 테스트 작성법도, 배포 절차도 달라집니다. 언어를 고르는 것보다 이 선택이 일상에 미치는 영향이 큽니다.

그래서 학습 순서가 중요합니다. 프레임워크부터 시작하면 튜토리얼 범위 안에서는 빠르게 움직이지만, 관례를 벗어난 문제가 오면 손을 대지 못합니다. 언어의 타입 체계와 예외 처리, 클래스 설계를 먼저 잡고 그 위에 프레임워크를 얹으면 생성된 코드가 무엇을 대신해 주고 있는지가 보입니다. 기본기부터 순서대로 밟고 싶다면 PHP 학습 라이브러리에서 예제 단위로 시작하는 편이 낫습니다.

Why It Works

이럴 때 특히 힘을 발합니다

웹 기능을 빠르게 붙인다

폼, 세션, 관리자 화면, 결제 연동처럼 전형적인 웹 기능을 짧은 시간 안에 움직이게 만들기 좋다.

비즈니스 규칙을 가깝게 담는다

프로모션, 쿠폰, 배송 정책처럼 자주 바뀌는 커머스 규칙을 현장 속도로 반영하기 편하다.

운영팀과의 거리가 짧다

관리자 도구와 프런트 오피스가 가까운 문화에서 수정-배포-검증 사이클이 빠르다.

Business Scenes

사업 현장에서 자주 보이는 장면

프로모션 운영

기간 한정 할인, 장바구니 조건, 쿠폰 중복 규칙이 잦게 바뀌는 쇼핑몰에서 민첩하게 대응하기 좋다.

콘텐츠 CMS

에디터, 편집 워크플로, 간단한 승인 기능이 함께 있는 미디어/브랜드 사이트에 잘 맞는다.

판매자 어드민

상품 등록, 주문 조회, CS 처리처럼 관리 화면 중심의 제품을 빠르게 구축하기 쉽다.

Workflow

팀이 움직이는 방식

  1. 프로모션 규칙은 쿼리보다 도메인 함수로 먼저 모은다.
  2. 운영팀이 자주 보는 관리자 화면의 문구와 동작을 함께 설계한다.
  3. 결제/주문 장애는 예외 로그보다 주문 상태 변경 이력을 먼저 본다.
  4. 매출 시즌 전에는 신규 기능보다 관리자 검수 흐름을 먼저 고정한다.

프로모션 규칙을 함수로 빼는 예

$cartTotal = array_reduce(
    $items,
    fn ($sum, $item) => $sum + ($item['qty'] * $item['price']),
    0
);

$discount = $cartTotal >= 70000 ? 5000 : 0;
echo $cartTotal - $discount;

현장에서는 로직의 화려함보다 매출 규칙을 빨리 읽고 바꿀 수 있는지가 중요하다.

위 예제가 저렇게 생긴 이유

앞의 장바구니 코드는 화려하지 않습니다. 그게 의도입니다. 합계를 구하는 부분과 할인을 정하는 부분이 분리되어 있어서, 기획이 할인 기준을 바꿔 달라고 할 때 고칠 자리가 한 줄로 특정됩니다. 계산 로직이 화면 템플릿이나 쿼리 안에 흩어져 있으면 같은 요청을 처리하는 데 며칠이 걸립니다. 커머스 코드에서 자주 바뀌는 것은 알고리즘이 아니라 숫자와 조건이라는 전제를 깔고 쓴 형태입니다.

다만 그 코드는 규칙을 함수로 모으는 감각까지만 보여줍니다. 실제로 데이터베이스를 만지는 자리에서는 형태가 하나 더 정해져 있습니다.

지금 방식으로 쓴 조회 함수

<?php
declare(strict_types=1);

function findPaidOrders(PDO $db, int $customerId, int $limit): array
{
    $sql = 'SELECT order_id, total_amount, created_at
            FROM orders
            WHERE customer_id = :customer_id
              AND status = :status
            ORDER BY created_at DESC
            LIMIT :limit';

    $stmt = $db->prepare($sql);
    $stmt->bindValue(':customer_id', $customerId, PDO::PARAM_INT);
    $stmt->bindValue(':status', 'paid');
    $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
    $stmt->execute();

    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

문자열을 이어 붙이지 않고, 값은 전부 바인딩으로 넘깁니다.

첫 줄의 선언 덕분에 숫자처럼 생긴 문자열이 조용히 정수로 바뀌는 일이 없습니다. 인자와 반환값에 타입을 적어 두면 호출하는 쪽이 무엇을 넘겨야 하는지 문서를 찾지 않아도 되고, 정적 분석 도구가 잘못된 호출을 실행 전에 잡아냅니다. 쿼리에 값을 직접 끼워 넣는 대신 이름 붙인 자리표시자에 바인딩하는 것이 SQL 주입을 막는 기본 형태이고, 데이터베이스 입장에서도 같은 쿼리를 반복해서 다시 해석하지 않아도 됩니다.

배포와 운영에서 만지는 것들

프로젝트를 시작하면 Composer로 의존성을 설치하고, 잠금 파일을 저장소에 함께 커밋합니다. 개발 기계와 서버가 같은 버전을 쓰게 만드는 최소 장치입니다. 테스트는 PHPUnit이나 Pest 중 팀이 쓰는 쪽을 따라가면 되고, 둘 다 Composer로 들어옵니다. 도메인 규칙을 함수로 모아 두는 습관은 여기서 값을 합니다. 테스트를 붙일 자리가 명확해지기 때문입니다. 실전 구성은 PHP 테스트 가이드에 정리해 두었습니다.

그다음이 정적 분석입니다. PHPStan이나 Psalm을 낮은 단계부터 켜 두면 타입이 맞지 않는 호출, 존재하지 않는 메서드, 처리되지 않은 null이 실행 전에 드러납니다. 오래된 코드베이스에 도입할 때는 기존 오류를 기준선으로 저장해 두고 새로 추가되는 코드부터 막는 방식이 현실적입니다. 처음부터 최고 단계로 켜면 대개 사흘 만에 꺼 버리게 됩니다.

실행 환경은 보통 nginx 앞단에 PHP-FPM을 붙인 구성입니다. FPM은 워커 프로세스를 미리 띄워 두고 요청을 나눠 줍니다. 동시 처리 한계는 이 워커 수와 각 워커가 물고 있는 데이터베이스 커넥션에서 정해지므로, 부하가 몰릴 때 볼 지표도 여기입니다. opcache는 컴파일된 스크립트를 메모리에 올려 두어 매 요청마다 다시 파싱하지 않게 해 줍니다. 켜져 있는지 확인하는 것만으로 응답 시간이 달라지는 항목이라 배포 점검표에 넣어 둘 만합니다. 성능을 더 파고들 거리는 PHP 성능 가이드에 모아 두었습니다.

그래서 배포는 여전히 단순한 편입니다. 파일을 서버에 동기화하고, opcache를 비우거나 새 디렉터리로 심볼릭 링크를 바꾸고, 마이그레이션을 돌리는 정도입니다. 문제가 생겼을 때 한 요청 안에서 무슨 일이 있었는지 따라가야 하면 Xdebug로 단계 실행을 하거나 프로파일을 떠 봅니다. 다만 Xdebug는 운영 환경에 켜 둘 물건이 아닙니다.

선택하지 않는 편이 나은 경우

  • 이미 PHP로 돌아가는 제품을 맡았다면 고민할 것이 없습니다. 언어를 바꾸는 비용보다 지금 코드베이스를 현대적인 방식으로 옮기는 편이 훨씬 빠르게 회수됩니다.
  • 서버에서 HTML을 만들어 내려주는 웹 제품을 작은 인원으로 만들고 운영해야 한다면 여전히 합리적인 선택입니다.
  • 커머스나 CMS 계열로 취업이나 이직을 준비 중이라면 채용 공고에 프레임워크 이름이 함께 붙어 있는지 먼저 확인하세요. 언어만 배우면 절반입니다.
  • 지금은 아니다: 실시간 연결을 오래 유지하거나 요청 사이에 상태를 들고 있어야 하는 서비스가 목표라면 실행 모델부터 어긋납니다.
  • 지금은 아니다: 첫 언어를 고르는 중이고 특별히 PHP 코드베이스에 들어갈 예정이 없다면, 데이터와 웹을 함께 다루기 좋은 언어에서 시작하는 편이 선택지를 넓게 둡니다.
  • 지금은 아니다: 브라우저와 서버에서 같은 코드를 공유하고 싶다면 이 언어로는 되지 않습니다. 그 요구가 크다면 JavaScript 쪽을 먼저 보세요.

플레이북

행사 주간을 버티는 PHP 커머스 플레이북

프로모션이 매시간 바뀌고 고객센터 문의가 쏟아지는 날. 커머스 팀은 무엇을 먼저 잠그는가.