PHpullh

PHP · 심층 가이드

PHP 파일/IO 완전 정리

스트림 래퍼로 통일된 PHP의 입출력 위에서 CSV와 JSON, 디렉터리 순회, 파일 잠금과 원자적 교체까지 데이터를 잃지 않는 파일 처리 방법을 정리합니다.

주제 20개 · 예제 코드 포함 · 최종 수정 2026-09-03 · 작성 pullh 편집팀

PHP 입출력의 중심에는 스트림 래퍼라는 추상화가 있습니다. fopen에 넘기는 경로가 로컬 파일일 수도, php://memory 같은 가상 스트림일 수도, https:// 로 시작하는 원격 자원일 수도 있고, 그 위에서 동작하는 함수 집합은 동일합니다. 덕분에 파일을 다루던 코드를 거의 그대로 네트워크나 메모리 버퍼로 돌릴 수 있습니다. 반대로 이 편의성 때문에 사용자 입력이 경로로 흘러들면 그대로 원격 접근이 되어 버리는 위험도 함께 따라옵니다.

파일 읽기와 파일 쓰기처럼 한 번에 처리하는 방식으로 시작하되, 파일이 커지는 순간 fopen 과 fgets 로 줄 단위 읽기로 갈아타는 감각을 먼저 만드십시오. 구조화된 데이터는 CSV 읽기와 JSON 파일 다루기가 담당하고, 대상 파일을 찾는 일은 단순한 경우 glob 패턴 검색, 하위 디렉터리까지 훑어야 하면 RecursiveDirectoryIterator 순회가 맞습니다. 동시에 여러 프로세스가 같은 파일을 건드리는 상황이라면 flock 파일 잠금과 원자적 파일 교체를 한 묶음으로 읽는 편이 좋습니다.

설정 파일이나 캐시를 갱신할 때 file_put_contents를 그대로 쓰면 위험합니다. 이 함수는 원자적이지 않아서 쓰는 도중 프로세스가 죽으면 잘린 파일이 남고, 그 사이에 읽은 다른 프로세스는 깨진 내용을 받습니다. 안전한 방법은 같은 파일시스템에 임시 파일을 쓴 뒤 rename()으로 덮어쓰는 것입니다. rename은 같은 파일시스템 안에서 원자적이라 독자는 항상 이전 버전이나 새 버전 중 하나만 봅니다. 업로드 파일 검사도 비슷한데, 확장자와 $_FILES의 type 값은 클라이언트가 보낸 문자열이라 신뢰할 수 없습니다.

01파일 읽기

file_get_contents()에 __FILE__을 넘겨 스크립트 자신을 읽고, 반환값이 false인지로 성공 여부만 출력합니다. 설정 파일처럼 크기가 뻔한 파일을 통째로 다룰 때 쓰는 방식입니다. 다만 내용 전체가 메모리에 올라가므로 로그처럼 큰 파일에는 fopen 계열이 맞고, 실패해도 예외가 아니라 false와 경고만 나옵니다.

PHP code

<?php
$path = __FILE__;
echo file_get_contents($path) !== false ? 'loaded' : 'failed';
알아두면 좋은 점

네 번째와 다섯 번째 인자로 offset과 maxlen을 줄 수 있어 파일 앞부분만 읽을 수도 있습니다. 스트림 래퍼 덕분에 http로 시작하는 경로도 같은 함수로 읽힙니다.

자주 하는 실수

반환값을 확인하지 않고 바로 문자열 함수에 넘기면 실패했을 때 false가 흘러가 빈 결과가 정상처럼 처리됩니다. 큰 파일이면 memory_limit에 걸려 프로세스가 죽습니다.

02파일 쓰기

임시 디렉터리 경로를 만들어 file_put_contents()로 한 줄을 저장한 뒤 그 경로를 화면에 찍습니다. 캐시나 렌더링 결과를 파일 하나로 떨어뜨릴 때 이 한 줄이면 fopen·fwrite·fclose를 대신합니다. 기본 동작이 덮어쓰기라 기존 내용은 사라지고, 반환값은 성공 여부가 아니라 기록한 바이트 수입니다.

PHP code

<?php
$path = sys_get_temp_dir() . '/php-write.txt';
file_put_contents($path, "hello
");
echo $path;
알아두면 좋은 점

세 번째 인자로 LOCK_EX를 주면 쓰는 동안 배타 잠금을 잡아 줍니다. 상위 디렉터리가 없으면 실패하므로 is_dir() 확인이나 재귀 mkdir()이 앞에 필요합니다.

자주 하는 실수

반환된 바이트 수를 성공 여부로 쓰면 빈 문자열을 저장했을 때 0이 나와 실패로 오인합니다. 판정은 !== false로 해야 합니다.

03로그 이어쓰기

세 번째 인자 FILE_APPEND 하나로 같은 함수가 덮어쓰기 대신 이어쓰기로 바뀝니다. 요청마다 한 줄씩 쌓는 애플리케이션 로그가 전형적인 사용처입니다. 여러 프로세스가 동시에 같은 파일에 붙이면 줄이 섞일 수 있으니 FILE_APPEND | LOCK_EX처럼 잠금 플래그를 함께 주는 편이 안전합니다.

PHP code

<?php
$path = sys_get_temp_dir() . '/app.log';
file_put_contents($path, "[info] boot
", FILE_APPEND);
echo $path;
알아두면 좋은 점

로그가 무한히 커지지 않게 하려면 크기를 확인해 회전시키거나 시스템의 로그 회전에 맡깁니다. 한 줄 단위 기록이라면 PSR-3 로거를 쓰는 편이 나중에 갈아 끼우기 쉽습니다.

자주 하는 실수

FILE_APPEND를 빼면 매 요청마다 파일이 통째로 덮어써져 이전 로그가 사라집니다. 문제가 생긴 뒤에야 로그가 한 줄뿐이라는 것을 알게 됩니다.

04CSV 읽기

메모리 스트림 php://temp를 열어 fputcsv()로 헤더와 데이터 행을 쓰고, rewind() 후 fgetcsv()로 되읽습니다. 실제 파일을 만들지 않고 CSV 생성·파싱 로직을 시험하거나 다운로드 응답을 조립할 때 유용합니다. php://temp는 일정 크기를 넘으면 조용히 디스크로 넘어가고, fgetcsv()가 돌려주는 값은 숫자여도 전부 문자열입니다.

PHP code

<?php
$stream = fopen('php://temp', 'r+');
fputcsv($stream, ['name', 'score']);
fputcsv($stream, ['php', 100]);
rewind($stream);

while (($row = fgetcsv($stream)) !== false) {
    print_r($row);
}
알아두면 좋은 점

구분자와 인클로저를 인자로 바꿀 수 있어 탭 구분 파일도 같은 코드로 읽습니다. 첫 행을 헤더로 쓰려면 한 번 읽어 두고 array_combine()으로 각 행과 짝지으면 됩니다.

자주 하는 실수

빈 줄을 만나면 null 한 칸짜리 배열이 돌아와 false와 구분되므로, 그대로 인덱스 접근을 하면 값이 아닌 null을 다루게 됩니다. 엑셀용 한글 CSV는 인코딩 변환도 따로 필요합니다.

05JSON 파일 다루기

배열을 json_encode()로 직렬화해 파일에 저장하고, 다시 읽어 json_decode()의 두 번째 인자에 true를 줘 연관 배열로 되돌립니다. 소규모 설정이나 상태 스냅숏을 DB 없이 보관할 때 쓰는 짝입니다. true를 빼면 stdClass가 되고, 깨진 JSON은 예외 없이 null만 반환하므로 json_last_error() 확인이 따라붙어야 합니다.

PHP code

<?php
$path = sys_get_temp_dir() . '/config.json';
file_put_contents($path, json_encode(['cache' => true]));
$config = json_decode(file_get_contents($path), true);

var_dump($config);
알아두면 좋은 점

두 함수 모두 JSON_THROW_ON_ERROR를 주면 실패가 JsonException으로 올라와 검사 코드를 줄일 수 있습니다. 큰 정수는 문자열로 받는 옵션도 함께 고려합니다.

자주 하는 실수

정상적인 값 null과 파싱 실패의 null이 똑같이 보이기 때문에 검사 없이 배열로 취급하면 다음 줄에서 타입 오류가 납니다. 인코딩 실패도 확인해야 합니다.

06디렉터리 목록 읽기

scandir()로 현재 디렉터리 항목을 배열로 받은 뒤 앞의 다섯 개만 잘라 출력합니다. 항목 수가 적은 폴더의 내용을 한 번에 손에 쥐고 싶을 때 가장 짧은 방법입니다. 결과에는 .과 ..이 포함되고 하위 디렉터리까지 들어가지는 않으므로, 파일만 필요하면 걸러내는 단계가 하나 더 필요합니다.

PHP code

<?php
$files = scandir(__DIR__);
print_r(array_slice($files, 0, 5));
알아두면 좋은 점

두 번째 인자로 SCANDIR_SORT_DESCENDING을 주면 역순으로 받고, 정렬이 필요 없으면 SCANDIR_SORT_NONE이 빠릅니다. 항목이 많으면 FilesystemIterator가 메모리에 유리합니다.

자주 하는 실수

결과에 든 것은 경로가 아니라 파일명뿐이라 그대로 is_file()에 넘기면 현재 작업 디렉터리를 기준으로 찾습니다. 디렉터리 경로를 앞에 붙여야 합니다.

07RecursiveDirectoryIterator 순회

RecursiveDirectoryIterator를 RecursiveIteratorIterator로 감싸 하위 디렉터리까지 평평하게 순회하다가 세 번째 항목에서 break합니다. 깊이를 알 수 없는 트리에서 조건에 맞는 파일을 찾아 나가는 작업에 맞습니다. 순회가 지연 방식이라 중간에 멈추면 나머지는 아예 읽지 않고, 루프 변수는 문자열이 아니라 SplFileInfo 객체입니다.

PHP code

<?php
$iterator = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator(__DIR__, FilesystemIterator::SKIP_DOTS)
);

$count = 0;
foreach ($iterator as $file) {
    $count++;
    if ($count === 3) {
        break;
    }
}

echo $count;
알아두면 좋은 점

RecursiveIteratorIterator에 SELF_FIRST를 주면 디렉터리 자체도 순회 대상에 들어옵니다. 깊이를 제한하려면 setMaxDepth()로 몇 단계까지 내려갈지 정할 수 있습니다.

자주 하는 실수

루프 변수를 문자열처럼 쓰면 SplFileInfo 객체라 비교가 어긋납니다. 경로가 필요하면 getPathname()을, 파일인지 확인하려면 isFile()을 불러야 합니다.

08업로드 파일 검사

업로드 배열에서 error가 UPLOAD_ERR_OK인지와 size가 2백만 바이트 미만인지를 보고 참/거짓을 돌려주는 검사 함수입니다. 컨트롤러가 파일을 옮기기 전에 통과시킬 최소 관문으로 쓰입니다. 여기서 걸러지는 건 크기와 오류 코드뿐이며, 클라이언트가 보낸 확장자나 MIME은 위조가 가능해 move_uploaded_file()과 서버 측 타입 확인이 별도로 필요합니다.

PHP code

<?php
function validateUpload(array $file): bool {
    return ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK
        && ($file['size'] ?? 0) < 2_000_000;
}

var_dump(validateUpload(['error' => 0, 'size' => 1200]));
알아두면 좋은 점

error 값에는 UPLOAD_ERR_INI_SIZE처럼 원인별 코드가 담겨 사용자에게 정확한 안내를 줄 수 있습니다. 크기 한도는 upload_max_filesize와 post_max_size도 함께 맞춰야 합니다.

자주 하는 실수

업로드된 name을 그대로 저장 경로에 쓰면 경로 조작 문자가 섞여 들어올 수 있습니다. 이름은 서버에서 새로 만들고 저장은 move_uploaded_file()로 해야 합니다.

09fopen 과 fgets 로 줄 단위 읽기

스트림에 두 줄을 써넣고 되감은 다음 fgets()로 한 줄씩 꺼내 trim()해 출력합니다. 수 기가바이트짜리 로그를 훑거나 줄 단위로 조기 종료해야 할 때 이 형태가 기본입니다. fgets()는 줄 끝 개행 문자까지 붙여 반환하므로 비교 전에 다듬어야 하고, 파일 전체가 아니라 한 줄 분량만 메모리를 차지합니다.

PHP code

<?php
$stream = fopen('php://temp', 'r+');
fwrite($stream, "a
b
");
rewind($stream);

while (($line = fgets($stream)) !== false) {
    echo trim($line) . PHP_EOL;
}
알아두면 좋은 점

두 번째 인자로 최대 길이를 주면 개행이 없는 거대한 한 줄에서도 메모리를 지킬 수 있습니다. 줄 단위 순회는 SplFileObject를 foreach로 도는 방법도 있습니다.

자주 하는 실수

while (!feof($fp)) 형태로 바꾸면 파일 끝에서 빈 값을 한 번 더 처리하게 됩니다. 반환값을 !== false로 검사하는 이 형태를 유지하는 편이 안전합니다.

10flock 파일 잠금

c+ 모드로 파일을 열어 LOCK_EX를 잡은 뒤 값을 쓰고 fflush() 후 잠금을 풉니다. 카운터나 크론 중복 실행 방지처럼 한 번에 한 프로세스만 손대야 하는 파일에 씁니다. flock()은 권고적 잠금이라 이 규약을 지키지 않는 프로세스는 그대로 쓸 수 있고, 일부 네트워크 파일시스템에서는 동작을 신뢰하기 어렵습니다.

PHP code

<?php
$path = sys_get_temp_dir() . '/counter.txt';
$fp = fopen($path, 'c+');

if (flock($fp, LOCK_EX)) {
    fwrite($fp, "1
");
    fflush($fp);
    flock($fp, LOCK_UN);
}

fclose($fp);
알아두면 좋은 점

c+ 모드는 파일이 없으면 만들고 있으면 자르지 않은 채 열기 때문에 잠금을 먼저 잡고 내용을 다룰 수 있습니다. w 모드는 여는 순간 내용을 지워 잠금이 무의미해집니다.

자주 하는 실수

잠금을 잡은 뒤 기존 내용을 지우지 않고 더 짧은 값을 쓰면 앞부분만 덮여 옛 내용이 뒤에 남습니다. 카운터를 갱신할 때는 ftruncate()를 함께 불러야 합니다.

11임시 파일 만들기

tempnam()에 디렉터리와 접두사를 주면 충돌하지 않는 이름을 골라 빈 파일까지 실제로 만들어 주고, 거기에 내용을 채운 뒤 경로를 출력합니다. 외부 명령에 넘길 중간 산출물이나 다운로드 조립용 버퍼에 적합합니다. 이 파일은 스크립트가 끝나도 남으므로 직접 unlink()해야 하며, 자동 정리를 원하면 tmpfile() 쪽입니다.

PHP code

<?php
$path = tempnam(sys_get_temp_dir(), 'php-');
file_put_contents($path, 'temp');
echo $path;
알아두면 좋은 점

tempnam()은 파일을 소유자만 읽고 쓸 수 있는 권한으로 만들며, 지정한 디렉터리를 쓸 수 없으면 다른 위치를 고르므로 반환된 경로를 그대로 써야 합니다.

자주 하는 실수

예외가 나는 경로에서 unlink()를 건너뛰면 임시 파일이 계속 쌓여 디스크를 채웁니다. 정리는 finally 블록에 두는 편이 확실합니다.

12INI 설정 읽기

key=value 두 줄짜리 INI를 만들어 parse_ini_file()로 배열로 되돌립니다. .env 비슷한 단순 설정이나 레거시 php.ini 형식을 읽어야 할 때 쓰입니다. 기본 모드에서는 true·on·yes가 문자열 "1"로 바뀌어 불린이 아니고, 두 번째 인자를 켜야 [section]이 중첩 배열로 분리됩니다.

PHP code

<?php
$path = sys_get_temp_dir() . '/app.ini';
file_put_contents($path, "env=local
cache=true
");

print_r(parse_ini_file($path));
알아두면 좋은 점

INI_SCANNER_TYPED 모드를 주면 true와 숫자가 실제 타입으로 변환돼 문자열 비교를 하지 않아도 됩니다. 값에 특수문자가 있으면 따옴표로 감싸야 안전합니다.

자주 하는 실수

이 파일을 웹 루트 아래 두면 브라우저로 그대로 열려 설정값이 노출됩니다. 파싱 실패 시 false만 돌아오므로 검사를 빼면 빈 배열처럼 다루게 됩니다.

13glob 패턴 검색

glob()에 *.js 패턴을 주고 매칭된 개수를 세는데, is_array()로 감싼 이유는 이 함수가 실패 시 false를 돌려주기 때문입니다. 빌드 산출물이나 마이그레이션 파일을 이름 규칙으로 골라낼 때 씁니다. 패턴은 한 단계 디렉터리만 훑고 하위로 재귀하지 않으며, 점으로 시작하는 숨김 파일은 기본적으로 잡히지 않습니다.

PHP code

<?php
$matches = glob(__DIR__ . '/*.js');
echo is_array($matches) ? count($matches) : 0;
알아두면 좋은 점

GLOB_BRACE를 주면 여러 확장자를 한 번에 잡을 수 있고, 정렬이 필요 없으면 GLOB_NOSORT가 조금 빠릅니다. 결과에는 파일명이 아니라 경로 전체가 담깁니다.

자주 하는 실수

패턴에 사용자 입력을 그대로 넣으면 의도하지 않은 디렉터리까지 훑게 됩니다. 매칭이 없을 때의 빈 배열과 실패의 false를 구분하지 않으면 개수가 늘 0으로 보입니다.

14ZipArchive 압축

ZipArchive를 CREATE | OVERWRITE로 열고 addFromString()으로 디스크에 없는 내용을 바로 항목으로 넣은 뒤 닫습니다. 리포트 여러 개를 묶어 한 번에 내려보내는 기능에 그대로 쓰입니다. 실제 압축과 기록은 close()가 호출되는 시점에 일어나므로 그 전에 파일 크기를 재면 어긋나고, 이 클래스는 ext-zip 확장이 있어야 존재합니다.

PHP code

<?php
$zipPath = sys_get_temp_dir() . '/sample.zip';
$zip = new ZipArchive();
$zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE);
$zip->addFromString('readme.txt', 'hello zip');
$zip->close();

echo $zipPath;
알아두면 좋은 점

open()의 반환값은 true 또는 오류 코드 정수이므로 === true로 확인해야 합니다. 항목이 많으면 addFile()로 디스크의 파일을 직접 참조해 메모리 사용을 줄일 수 있습니다.

자주 하는 실수

close() 전에 파일 크기를 재거나 다운로드 응답을 시작하면 아직 기록되지 않은 파일을 읽어 깨진 zip이 나갑니다. close()의 실패 여부도 확인해야 합니다.

15SimpleXML 파싱

simplexml_load_string()으로 짧은 XML을 객체 트리로 바꾼 뒤 item 노드를 문자열로 캐스팅해 찍습니다. 구조가 단순한 RSS나 외부 API의 XML 응답을 훑을 때 코드가 가장 짧아집니다. $xml->item 자체는 문자열이 아니라 SimpleXMLElement라 (string) 없이 배열에 담거나 비교하면 어긋나고, 파싱 실패는 경고와 false로 알려집니다.

PHP code

<?php
$xml = simplexml_load_string('<root><item>php</item></root>');
echo (string) $xml->item;
알아두면 좋은 점

속성은 $xml->item['id']처럼 배열 첨자로 읽고, 네임스페이스가 있는 문서는 children()에 URI를 넘겨야 노드가 보입니다. 반복 노드는 foreach로 그대로 돌 수 있습니다.

자주 하는 실수

신뢰할 수 없는 XML을 그대로 파싱하면 외부 엔터티를 통한 정보 유출 위험이 있습니다. 파싱 실패도 예외가 아니라 false와 경고로만 알려 주므로 반환값 검사가 필요합니다.

16DOMDocument 수정

빈 <root/> 문서를 올린 뒤 createElement()로 노드를 만들어 루트에 붙이고 saveXML()로 문서 전체를 문자열화합니다. 기존 XML의 특정 위치에 값을 끼워 넣거나 고쳐야 하는 작업이라면 SimpleXML보다 이쪽이 다룰 수 있는 범위가 넓습니다. createElement()의 두 번째 인자는 텍스트를 그대로 넣기 때문에 &가 든 값은 createTextNode()로 넣어야 안전합니다.

PHP code

<?php
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->loadXML('<root/>');
$node = $dom->createElement('lang', 'php');
$dom->documentElement->appendChild($node);

echo $dom->saveXML();
알아두면 좋은 점

formatOutput을 켜면 보기 좋게 들여쓴 XML이 나오고, 특정 노드를 찾으려면 DOMXPath를 함께 씁니다. 노드를 다른 문서로 옮길 때는 importNode()가 필요합니다.

자주 하는 실수

두 번째 인자에 &나 꺾쇠가 든 문자열을 넣으면 경고와 함께 노드가 만들어지지 않습니다. 값은 createTextNode()로 넣어야 안전합니다.

17이미지 메타 정보 읽기

is_file()로 존재를 확인한 뒤에만 getimagesize()를 호출해 가로·세로·타입 정보를 출력합니다. 썸네일 크기를 계산하거나 업로드된 이미지의 실제 규격을 서버에서 확인할 때 부릅니다. 이 함수는 파일 앞부분의 헤더만 읽어 판단하므로 픽셀 전체를 디코딩하지 않지만, 반대로 헤더만 위장한 파일도 통과할 수 있어 보안 검사로는 부족합니다.

PHP code

<?php
$path = __DIR__ . '/og-image.png';
if (is_file($path)) {
    print_r(getimagesize($path));
}
알아두면 좋은 점

반환 배열에는 img 태그에 바로 넣을 수 있는 속성 문자열과 mime 키가 함께 들어 있습니다. 파일이 이미지가 아니면 false가 돌아오므로 결과 검사가 필요합니다.

자주 하는 실수

이 검사를 통과했다고 업로드 파일을 웹 루트에 저장하면, 헤더만 위조한 스크립트가 실행될 수 있습니다. 저장 위치와 실행 권한을 함께 막아야 합니다.

18stream context 로 HTTP 요청

stream_context_create()로 메서드·타임아웃·헤더를 담은 컨텍스트를 만들고 이를 file_get_contents()의 세 번째 인자로 넘겨 HTTP GET을 보냅니다. cURL을 끌어오기 아까운 단순 호출에 적당합니다. allow_url_fopen이 꺼져 있으면 아예 동작하지 않고, 404 같은 오류 응답도 본문이 있으면 false가 아니어서 상태 코드는 $http_response_header로 따로 봐야 합니다.

PHP code

<?php
$context = stream_context_create([
    'http' => [
        'method' => 'GET',
        'timeout' => 2,
        'header' => "Accept: application/json
",
    ],
]);

$body = @file_get_contents('https://example.com', false, $context);
echo $body === false ? 'failed' : 'ok';
알아두면 좋은 점

응답 헤더는 호출 뒤 $http_response_header 배열에 담기고, ignore_errors 옵션을 켜면 4xx 응답의 본문도 받을 수 있습니다. 리다이렉트 횟수도 컨텍스트로 제한할 수 있습니다.

자주 하는 실수

@로 경고를 눌러 두면 DNS 실패와 타임아웃을 구분할 단서까지 사라집니다. 상태 코드를 확인하지 않으면 404 본문을 정상 응답으로 저장하게 됩니다.

19경로 조합 헬퍼

가변 인자로 받은 조각들을 /로 이어 붙인 뒤 preg_replace()가 연속된 슬래시를 하나로 줄입니다. 설정값과 하위 경로를 합칠 때 슬래시가 겹치거나 빠지는 실수를 없애 줍니다. 정규식이 슬래시를 무조건 축약하므로 https:// 같은 URL을 넘기면 스킴이 망가지고, 윈도우의 역슬래시 구분자도 처리하지 못합니다.

PHP code

<?php
function joinPath(string ...$parts): string {
    return preg_replace('#/+#', '/', join('/', $parts));
}

echo joinPath(__DIR__, 'storage', 'logs', 'app.log');
알아두면 좋은 점

구분자를 하드코딩하는 대신 DIRECTORY_SEPARATOR를 쓰면 윈도우에서도 통합니다. 실제 존재하는 경로라면 realpath()가 심볼릭 링크와 상위 참조까지 풀어 줍니다.

자주 하는 실수

사용자 입력을 조각으로 받으면 상위 디렉터리 참조가 섞여 허용 범위 밖 파일을 가리킬 수 있습니다. 합친 결과가 기준 디렉터리 아래인지 확인해야 합니다.

20원자적 파일 교체

임시 파일에 새 JSON을 먼저 다 쓴 다음 rename()으로 대상 경로에 덮어씌웁니다. 다른 프로세스가 계속 읽고 있는 설정 파일을 교체할 때, 반쯤 쓰인 내용을 남에게 보여 주지 않으려는 패턴입니다. 원자성은 같은 파일시스템 안에서만 보장되므로 임시 디렉터리와 대상이 다른 마운트면 복사로 떨어지고, tempnam()이 만든 파일 권한도 확인해야 합니다.

PHP code

<?php
$target = sys_get_temp_dir() . '/config-live.json';
$temp = tempnam(sys_get_temp_dir(), 'cfg-');
file_put_contents($temp, json_encode(['version' => 2]));
rename($temp, $target);

echo $target;
알아두면 좋은 점

rename()은 대상이 이미 있어도 덮어쓰며 읽는 쪽은 옛 파일이나 새 파일 중 하나를 온전히 보게 됩니다. 교체 전에는 쓰기가 끝났는지 반드시 확인해야 합니다.

자주 하는 실수

tempnam()이 만든 파일은 소유자 전용 권한이라 그대로 옮기면 웹서버가 읽지 못할 수 있습니다. 교체 전에 chmod()로 권한을 맞추지 않으면 배포 직후 오류가 납니다.

정리하며

  • 설정과 캐시 갱신은 임시 파일에 쓴 뒤 rename으로 교체해 잘린 파일을 막습니다.
  • 큰 파일은 통째로 읽지 말고 fgets나 fgetcsv로 한 줄씩 스트리밍합니다.
  • 업로드 검증은 확장자와 전송된 MIME 대신 finfo로 실제 내용을 판별합니다.
  • 여러 프로세스가 쓰는 로그는 flock으로 구간을 감싸 줄 섞임을 방지합니다.

더 깊이 들어가고 싶다면 PHP 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.