로컬 실행과 운영 환경은 다르다
지금까지 만든 PHP 애플리케이션은 내 컴퓨터에서 잘 동작했다. 하지만 실제 사용자가 접속하는 서비스는 조건이 다르다. 여러 사용자가 동시에 접속하고, 서버가 꺼지면 안 되며, 오류 메시지가 외부에 노출되어서도 안 된다. 데이터를 잃지 않도록 백업도 필요하다.
배포는 로컬에서 만든 코드를 실제 서버에 올려 사용자가 접근할 수 있게 만드는 과정이다. 운영은 배포한 뒤 서비스가 안정적으로 계속 동작하도록 관리하는 일이다. 배포가 "서비스를 세상에 내보내는 일"이라면, 운영은 "그 서비스가 계속 안전하게 돌아가도록 돌보는 일"에 가깝다.
개발자는 보통 다음 질문에 답할 수 있어야 한다.
웹 요청은 어떤 서버를 거쳐 PHP 코드까지 도착하는가?
개발용 설정과 운영용 설정은 어떻게 나누는가?
데이터베이스 비밀번호와 비밀 키는 어디에 보관하는가?
오류는 사용자 화면이 아니라 어디에 기록되는가?
장애가 났을 때 무엇을 확인하고 어떻게 복구할 것인가?
이 장에서는 프레임워크나 클라우드 서비스에 깊게 들어가기보다, PHP 애플리케이션을 운영할 때 반드시 알아야 하는 기본 구조를 정리한다.
AI 생성 이미지
운영 서버의 기본 구조
로컬에서는 php -S localhost:8000으로 간단히 실행했다. 이 내장 웹 서버는 학습과 개발용이다. 실제 운영 환경에서는 보통 다음 구성 요소가 함께 동작한다.
브라우저
↓ HTTP 요청
웹 서버(Nginx 또는 Apache)
↓ PHP 실행 요청
PHP-FPM
↓ SQL 실행
데이터베이스(MySQL)웹 서버는 사용자의 HTTP 요청을 처음 받는다. CSS, JavaScript, 이미지 같은 정적 파일은 웹 서버가 직접 응답할 수 있다. PHP 실행이 필요한 요청은 PHP-FPM으로 넘긴다.
PHP-FPM은 PHP FastCGI Process Manager의 줄임말이다. 쉽게 말해 PHP 코드를 실행할 준비가 된 작업자 프로세스들을 관리하는 프로그램이다. 웹 서버가 PHP 실행을 요청하면 PHP-FPM이 PHP 코드를 실행하고, 그 결과인 HTML이나 JSON을 웹 서버로 돌려준다. 웹 서버는 다시 그 응답을 브라우저에 전달한다.
서버 구성은 환경에 따라 달라질 수 있다. 예를 들어 Apache에서는 PHP를 다른 방식으로 실행하는 구성도 가능하다. 다만 최근의 PHP 운영 환경에서는 Nginx 또는 Apache가 앞에서 요청을 받고, PHP-FPM이 PHP 실행을 담당하는 구성이 흔하다.
데이터베이스는 별도 서버에서 동작할 수도 있고 같은 서버에서 동작할 수도 있다. 작은 서비스는 한 서버에 웹 서버, PHP-FPM, MySQL을 함께 둘 수 있다. 사용자가 늘어나면 데이터베이스를 별도 서버로 분리하거나 캐시, 로드 밸런서 같은 구성을 추가한다. 처음부터 복잡하게 시작할 필요는 없지만, 구성 요소의 역할은 분명히 이해해야 한다.
public 디렉터리만 공개하기
MVC 구조로 애플리케이션을 만들 때 public/index.php를 진입점으로 두었다. 운영 서버에서도 이 원칙을 지켜야 한다. 웹 서버의 문서 루트는 프로젝트 전체가 아니라 public/ 디렉터리를 가리켜야 한다.
좋은 구조는 다음과 같다.
myapp/
├── config/
├── logs/
├── public/
│ ├── index.php
│ ├── css/
│ └── uploads/
├── src/
├── views/
├── vendor/
├── .env
└── composer.json웹에서 직접 접근 가능한 위치는 public/뿐이어야 한다. config/, src/, views/, .env, composer.json 같은 파일이 브라우저에서 열리면 안 된다. 특히 .env나 설정 파일에는 데이터베이스 비밀번호가 들어갈 수 있으므로 절대 공개 디렉터리에 두지 않는다.
예를 들어 문서 루트가 프로젝트 루트인 myapp/로 잡혀 있으면 브라우저에서 /config/app.php나 /.env 같은 경로를 직접 시도할 수 있다. 설정에 따라 내용이 그대로 노출되지 않더라도, 애초에 접근 가능한 위치에 두는 것 자체가 위험하다. 문서 루트를 myapp/public/으로 제한하면 이런 위험을 크게 줄일 수 있다.
public/index.php는 보통 다음처럼 Composer 오토로더와 설정 파일을 불러온다.
<?php
declare(strict_types=1);
require_once dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/config/app.php';
// 라우터 실행dirname(__DIR__)는 public/의 상위 디렉터리, 즉 프로젝트 루트를 가리킨다. 상대 경로를 대충 맞추는 것보다 기준 경로를 명확히 잡는 편이 안전하다.
개발 설정과 운영 설정 분리하기
개발 환경과 운영 환경은 설정이 달라야 한다. 개발 환경에서는 오류를 화면에 보여 주는 편이 편하다. 어디에서 문제가 났는지 바로 확인할 수 있기 때문이다. 반대로 운영 환경에서는 내부 경로, SQL, 스택 트레이스가 사용자에게 보이면 보안 문제가 된다.
설정은 보통 환경 이름에 따라 나눈다.
<?php
$appEnv = getenv('APP_ENV') ?: 'production';
if ($appEnv === 'local') {
error_reporting(E_ALL);
ini_set('display_errors', '1');
} else {
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', dirname(__DIR__) . '/logs/php-error.log');
}핵심은 코드 자체를 수정해서 환경을 바꾸지 않는 것이다. 운영 서버에 올릴 때마다 display_errors 값을 손으로 고치는 방식은 실수하기 쉽다. 대신 APP_ENV=local, APP_ENV=production 같은 환경 값을 서버에서 주입하고, PHP 코드는 그 값을 읽어 동작을 바꾼다.
운영 환경에서 기본적으로 지켜야 할 설정은 다음과 같다.
display_errors=0: 오류를 사용자 화면에 출력하지 않는다.log_errors=1: 오류를 로그 파일에 기록한다.error_reporting=E_ALL: 가능한 오류를 최대한 기록한다.업로드 크기 제한, 실행 시간 제한, 메모리 제한을 서비스에 맞게 조정한다.
date.timezone을 명확히 설정한다.
오류를 숨긴다는 말은 문제를 무시한다는 뜻이 아니다. 사용자는 일반적인 안내 메시지를 보고, 개발자는 로그에서 자세한 원인을 확인해야 한다.
환경 변수로 비밀 정보 관리하기
데이터베이스 비밀번호, 세션 비밀 키, 외부 API 키는 코드에 직접 적지 않는다. Git 저장소에 올라간 코드는 여러 사람이 볼 수 있고, 실수로 공개될 수도 있다.
나쁜 예는 다음과 같다.
$pdo = new PDO(
'mysql:host=localhost;dbname=myapp;charset=utf8mb4',
'myapp_user',
'real_password_here'
);비밀번호가 코드에 박혀 있다. 이 파일이 저장소에 올라가면 비밀번호도 함께 공유된다. 나중에 저장소에서 코드를 삭제하더라도 이미 기록에 남아 있을 수 있으므로, 한 번 노출된 비밀번호는 바꾸는 것이 안전하다.
더 나은 방식은 환경 변수에서 읽는 것이다.
<?php
function env(string $key, ?string $default = null): ?string
{
$value = getenv($key);
if ($value === false) {
return $default;
}
return $value;
}
$dbHost = env('DB_HOST', '127.0.0.1');
$dbName = env('DB_NAME', 'myapp');
$dbUser = env('DB_USER');
$dbPass = env('DB_PASS');
$dsn = "mysql:host={$dbHost};dbname={$dbName};charset=utf8mb4";
$pdo = new PDO($dsn, $dbUser, $dbPass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);개발 환경에서는 .env 파일을 사용해 값을 관리할 수 있다.
APP_ENV=local
DB_HOST=127.0.0.1
DB_NAME=myapp
DB_USER=root
DB_PASS=secret
APP_KEY=change_me단, .env는 Git에 올리지 않는다. 대신 필요한 키 이름만 적은 .env.example을 저장소에 둔다.
APP_ENV=local
DB_HOST=127.0.0.1
DB_NAME=
DB_USER=
DB_PASS=
APP_KEY=운영 서버에서는 .env 파일을 사용할 수도 있지만, 더 안전하게는 서버의 환경 변수, 배포 도구, 호스팅 서비스의 secret 관리 기능을 사용한다. 어떤 방식을 쓰든 중요한 원칙은 같다. 비밀 정보는 코드와 분리한다.
배포 전 Composer와 의존성 정리
Composer를 사용하는 프로젝트는 운영 서버에서도 의존성을 설치해야 한다. 다만 테스트 도구나 코드 스타일 도구처럼 개발할 때만 필요한 패키지는 운영 서버에 필요하지 않다.
일반적인 운영 설치 명령은 다음과 같다.
composer install --no-dev --optimize-autoloader--no-dev:require-dev에 있는 개발용 패키지를 설치하지 않는다.--optimize-autoloader: 클래스 자동 로딩 정보를 운영에 적합하게 최적화한다.
운영 서버에서 composer update를 실행하는 것은 보통 피하는 편이 좋다. composer update는 의존성 버전을 새로 계산하기 때문에, 로컬이나 CI에서 검증한 버전과 다른 코드가 운영에 설치될 수 있다. 배포할 때는 저장소에 포함된 composer.lock을 기준으로 composer install을 실행하는 흐름이 더 안전하다.
배포 전에 로컬 또는 CI에서 다음 검사를 통과시키는 습관을 들이면 좋다.
composer test
composer analyse
composer format명령 이름은 프로젝트마다 다를 수 있다. 중요한 것은 운영 서버에 올리기 전에 테스트, 정적 분석, 코드 스타일 검사를 자동으로 실행하는 흐름이다. 테스트와 코드 품질 관리에서 배운 내용이 배포 단계와 연결되는 지점이다.
파일 권한과 쓰기 가능한 경로
운영 서버에서는 모든 파일을 PHP가 마음대로 수정할 수 있게 두면 위험하다. 원칙은 간단하다.
소스 코드 파일은 웹 서버/PHP가 쓰지 못하게 한다.
로그, 캐시, 업로드 디렉터리만 쓰기 가능하게 한다.
비밀 설정 파일은 필요한 사용자만 읽을 수 있게 한다.
예를 들어 다음 디렉터리만 쓰기 가능하게 둘 수 있다.
logs/
public/uploads/
storage/cache/업로드 파일을 저장할 때도 주의가 필요하다. 사용자가 올린 파일이 PHP로 실행되면 안 된다. 가능하면 업로드 파일은 공개 디렉터리 밖에 저장하고, 다운로드가 필요할 때 PHP가 검증 후 읽어 주는 구조가 안전하다. 공개 디렉터리 안에 저장해야 한다면 웹 서버 설정으로 업로드 디렉터리에서 PHP 실행을 막아야 한다.
권한 문제는 배포 후 자주 발생한다. 로그가 기록되지 않거나 이미지 업로드가 실패한다면 먼저 해당 디렉터리를 PHP-FPM 프로세스 사용자가 쓸 수 있는지 확인한다. 반대로 권한을 급하게 해결하려고 전체 프로젝트에 쓰기 권한을 넓게 주는 것은 피해야 한다. 당장 오류는 사라질 수 있지만, 운영 서버의 위험이 커진다.
배포 체크리스트
운영 반영 전에는 매번 같은 항목을 점검해야 한다. 기억에 의존하면 빠뜨리기 쉽다.
public/만 웹에 공개되어 있는가?.env,config/,src/,views/가 브라우저에서 접근되지 않는가?APP_ENV가production으로 설정되어 있는가?display_errors가 꺼져 있는가?오류 로그 경로가 존재하고 쓰기 가능한가?
데이터베이스 계정 권한이 필요한 범위로 제한되어 있는가?
운영 데이터베이스 비밀번호가 코드에 하드코딩되어 있지 않은가?
composer install --no-dev --optimize-autoloader를 실행했는가?업로드 디렉터리에서 PHP 실행이 차단되어 있는가?
HTTPS가 적용되어 있는가?
세션 쿠키에
Secure,HttpOnly,SameSite옵션이 적용되어 있는가?배포 전 테스트가 통과했는가?
데이터베이스 백업이 있는가?
문제가 생겼을 때 되돌릴 이전 버전이 있는가?
체크리스트는 단순하지만 효과가 크다. 운영 장애의 많은 부분은 어려운 기술보다 기본 점검 누락에서 시작된다.
백업, 모니터링, 장애 대응
운영은 배포가 끝난 뒤 시작된다. 서비스는 언제든 문제가 생길 수 있다. 디스크가 가득 찰 수 있고, 데이터베이스 연결이 끊길 수 있으며, 코드 변경 후 특정 기능만 실패할 수도 있다.
최소한의 운영 습관은 세 가지다.
첫째, 백업이다. 데이터베이스는 정기적으로 백업해야 한다. 백업 파일을 만드는 것만으로는 부족하다. 실제로 복원할 수 있는지도 확인해야 한다. 복원해 보지 않은 백업은 믿을 수 없다. 특히 배포 중 데이터베이스 구조를 바꾸는 작업이 있다면, 변경 전 백업과 되돌릴 방법을 함께 준비하는 것이 좋다.
둘째, 모니터링이다. 서버가 살아 있는지, 응답 시간이 지나치게 느려지지 않았는지, 디스크 용량이 부족하지 않은지 확인해야 한다. 작은 서비스라도 오류 로그와 웹 서버 로그를 정기적으로 보는 습관이 필요하다. 사용자가 알려 주기 전에 먼저 이상 징후를 발견하는 것이 운영의 중요한 목표다.
셋째, 장애 대응 절차다. 문제가 생겼을 때 무엇부터 볼지 정해 두어야 한다.
1. 최근 배포가 있었는지 확인한다.
2. 웹 서버 로그와 PHP 오류 로그를 확인한다.
3. 데이터베이스 연결 상태를 확인한다.
4. 디스크 용량과 파일 권한을 확인한다.
5. 특정 기능만 문제인지 전체 서비스 문제인지 나눈다.
6. 필요하면 이전 버전으로 되돌린다.장애 대응에서 중요한 태도는 당황해서 서버를 무작정 고치는 것이 아니다. 증거를 남기고, 원인을 좁히고, 사용자 영향을 줄이는 순서로 움직여야 한다.
간단한 배포 흐름 예시
작은 프로젝트라면 다음 흐름으로도 충분히 시작할 수 있다.
1. 로컬에서 기능 개발
2. 테스트와 정적 분석 실행
3. Git에 커밋
4. 서버에서 최신 코드 가져오기
5. composer install --no-dev --optimize-autoloader 실행
6. 환경 변수 확인
7. 데이터베이스 마이그레이션 또는 SQL 변경 적용
8. PHP-FPM 또는 웹 서버 리로드가 필요한지 확인
9. 주요 페이지 접속 확인
10. 로그 확인항상 PHP-FPM이나 웹 서버를 재시작해야 하는 것은 아니다. 코드 변경만으로 반영되는 경우도 있고, OPcache 설정이나 서버 구성 변경 때문에 리로드가 필요한 경우도 있다. 중요한 것은 "무조건 재시작"이 아니라, 현재 서버 구성이 어떤 방식으로 변경 사항을 반영하는지 알고 있는 것이다.
규모가 커지면 이 과정은 CI/CD 도구로 자동화한다. CI/CD는 코드를 저장소에 올렸을 때 테스트, 빌드, 배포를 자동으로 실행하는 흐름이다. 자동화의 목적은 사람을 배제하는 것이 아니라 반복 작업에서 실수를 줄이는 것이다.
처음에는 수동 배포로 시작해도 괜찮다. 대신 배포 명령과 순서를 문서로 남겨야 한다. 같은 작업을 두 번 이상 반복한다면 스크립트로 만들 수 있다.
실습 과제
기존 MVC 프로젝트에
APP_ENV,DB_HOST,DB_NAME,DB_USER,DB_PASS환경 변수를 읽는 설정 파일을 추가해 보자.운영 환경일 때
display_errors를 끄고logs/php-error.log에 오류를 기록하도록 설정해 보자.프로젝트에서 웹에 공개되어야 하는 파일과 공개되면 안 되는 파일을 구분해 목록으로 작성해 보자.
composer install --no-dev --optimize-autoloader를 실행해 보고,vendor/디렉터리가 어떻게 구성되는지 확인해 보자.데이터베이스 백업 명령을 조사하고, 작은 테스트 데이터베이스를 백업한 뒤 복원해 보자.
자주 발생하는 오류 점검
운영 서버에서 흰 화면만 보인다.display_errors가 꺼져 있으면 화면에는 아무것도 안 보일 수 있다. PHP 오류 로그와 웹 서버 로그를 먼저 확인한다.
로컬에서는 되는데 서버에서는 데이터베이스 연결이 실패한다.
환경 변수 값, 데이터베이스 호스트, 계정 권한, 방화벽, 문자셋 설정을 확인한다. 운영 서버의 DB_HOST가 로컬과 같다고 가정하면 안 된다.
이미지 업로드가 실패한다.
업로드 디렉터리 권한, PHP의 upload_max_filesize, post_max_size, 웹 서버 요청 크기 제한을 함께 확인한다.
로그 파일이 생성되지 않는다.
로그 디렉터리가 존재하는지, PHP-FPM 실행 사용자가 해당 디렉터리에 쓸 수 있는지 확인한다.
배포 후 예전 코드가 계속 실행된다.
OPcache, PHP-FPM 리로드 여부, 실제 배포 경로가 맞는지 확인한다. 서버에 여러 프로젝트 복사본이 있으면 엉뚱한 경로를 수정하는 실수가 생긴다.
배포와 운영은 한 번에 완벽히 익히기 어렵다. 그러나 기본 원칙은 분명하다. 공개할 것과 숨길 것을 나누고, 설정을 환경별로 분리하며, 오류는 화면이 아니라 로그에 남기고, 데이터는 반드시 백업한다. 이 원칙 위에 자동화와 모니터링을 조금씩 더하면 PHP 애플리케이션을 실제 서비스로 운영할 준비가 된다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.