웹 애플리케이션을 개발하다가 한 사용자가 여러 개의 AJAX 요청을 동시에 보내거나 장시간 실행되는 비동기 작업(대용량 파일 다운로드, 외부 API 연동 등)을 수행할 때, 다른 요청들이 무한정 대기 상태에 빠지는 경험을 해봤을 것이다.
다만 대부분의 개발자들은 이를 단순한 네트워크 지연이나 서버 성능 문제로 오인하고, 세션 메커니즘에서 발생하는 블로킹(Session Blocking) 현상 때문이라는 정확한 원인을 모르는 경우가 많다.
이번에는 PHP 세션 블로킹이 정확히 무엇인지, 왜 발생하는지, 그리고 session_write_close() 함수를 활용해 동시 요청 병목을 완벽하게 해결하는 방법을 정리해서 소개하겠다.
PHP의 기본 세션 핸들러는 파일 시스템(File System)을 사용한다. 클라이언트가 session_start()를 호출하면 PHP는 해당 사용자의 세션 ID에 해당하는 세션 파일(예: sess_xyz...)을 열고 배타적 잠금(Exclusive Lock, flock)을 걸어버린다.
이 잠금은 해당 스크립트의 실행이 완료되어 완전히 종료되거나, 명시적으로 세션이 닫힐 때까지 유지된다.
문제는 동일한 세션 Cookie를 가진 브라우저가 첫 번째 요청이 끝나기 전에 두 번째, 세 번째 요청을 보낼 때 발생한다. 두 번째 요청 역시 session_start()를 만나 세션 파일에 접근하려 하지만, 첫 번째 요청이 파일 잠금을 쥐고 있으므로 첫 번째 요청이 끝날 때까지 스크립트 실행이 중단(Block)된 채 대기하게 된다.
예를 들어 10초가 걸리는 대용량 보고서 생성 API 요청을 보낸 직후, 사용자가 다른 페이지로 이동하거나 프로필 정보를 조회하는 요청을 보내면, 프로필 조회 요청 역시 10초 동안 멈춰 서게 된다. 이는 서버의 CPU나 메모리 자원이 부족해서가 아니라 순전히 세션 파일 잠금에 의한 인위적인 병목 현상이다.
| 구분 | 기본 파일 세션 (Default) | session_write_close() 적용 후 |
|---|---|---|
| 동작 방식 | 스크립트 종료 시까지 세션 파일 잠금 유지 | 세션 읽기/쓰기 직후 즉시 잠금 해제 |
| 동시 요청 처리 | 동일 세션 요청이 순차적으로 대기 (블로킹) | 동일 세션 요청이 병렬로 즉시 처리됨 |
| 응답 대기 시간 | 앞선 작업 완료 시간만큼 누적 대기 | 대기 없이 즉시 응답 시작 |
| 주요 활용처 | 단순 페이지 이동 및 단발성 요청 | AJAX 비동기 통신, 대용량 파일 다운로드, 외부 API 연동 |
이 문제를 해결하는 열쇠가 바로 session_write_close() 함수다(동일한 기능을 하는 별칭으로 session_commit()도 존재한다).
이 함수는 현재 메모리에 로드된 세션 데이터를 세션 저장소에 즉시 기록하고, 세션 파일 잠금을 해제한다.
중요한 점은 session_write_close()를 호출해도 메모리에 이미 로드된 $_SESSION Superglobal 변수 값은 스크립트 내에서 계속 읽을 수 있다는 사실이다. 단지 더 이상 $_SESSION 배열의 값을 수정하여 저장할 수 없을 뿐이다.
따라서 처리 흐름을 다음과 같이 설계해야 한다.
1) session_start()로 세션 데이터를 읽는다.
2) 필요한 사용자 정보(예: $_SESSION['user_id'])를 변수에 할당하거나 확인한다.
3) 더 이상 세션 값을 수정할 필요가 없다면 즉시 session_write_close()를 호출하여 잠금을 해제한다.
4) 이후 장시간 소요되는 데이터베이스 쿼리, 외부 API 호출, 파일 처리 작업을 진행한다.
실제 예제 코드를 통해 세션 블로킹이 발생하는 잘못된 코드와 이를 최적화한 올바른 코드를 확인해보자.
✗ 잘못된 코드: 스크립트가 끝날 때까지 세션 잠금이 유지되는 케이스
<?php
// session_blocking_bad.php
session_start();
// 사용자 인증 확인
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
exit('Unauthorized');
}
// 5초가 걸리는 무거운 로직 수행 (외부 API 연동 등)
sleep(5);
// 이 작업이 끝날 때까지 동일한 사용자의 다른 모든 요청은 멈추게 된다.
echo json_encode(['status' => 'success', 'data' => '처리 완료']);
?>위 코드는 sleep(5)가 실행되는 5초 동안 세션 파일 잠금이 계속 유지된다. 이때 같은 사용자가 다른 탭에서 페이지를 열거나 AJAX 요청을 보내면 최소 5초 이상 대기하게 된다.
✓ 올바른 코드: 세션 확인 직후 잠금을 즉시 해제하는 케이스
<?php
// session_blocking_good.php
session_start();
// 사용자 인증 확인 및 필요한 세션 정보 추출
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
exit('Unauthorized');
}
$userId = $_SESSION['user_id'];
// 세션 읽기가 끝났으므로 즉시 세션 파일을 저장하고 잠금을 해제한다.
session_write_close();
// 세션 잠금이 해제된 상태에서 5초 걸리는 무거운 로직 수행
sleep(5);
// $_SESSION 메모리 값은 여전히 읽을 수 있지만 수정사항은 저장되지 않는다.
echo json_encode(['status' => 'success', 'user' => $userId]);
?>결과 / 동작 방식:
올바른 코드에서는 session_write_close()가 호출되는 즉시 세션 파일의 배타적 잠금이 해제된다. 따라서 뒤이어 호출되는 sleep(5) 동안 사용자가 다른 페이지를 요청하더라도 대기 시간 없이 즉시 동시 처리가 완료된다.
session_write_close()를 적용할 때 개발자들이 자주 범하는 실수를 정리했다.
✗ 잘못된 작성 패턴: 잠금을 해제한 후 세션 값을 변경하려고 시도하는 경우
<?php
session_start();
$userId = $_SESSION['user_id'];
// 세션 잠금 해제
session_write_close();
// 무거운 작업 수행...
sleep(3);
// [실수] 세션이 이미 닫혔으므로 이 변경사항은 파일에 저장되지 않는다!
$_SESSION['last_access'] = time();
?>✓ 올바른 작성 패턴: 세션 변경이 필요하다면 잠금을 닫기 전에 수행하거나, 나중에 세션을 다시 열어서 수정
<?php
session_start();
$userId = $_SESSION['user_id'];
// 세션 변경사항을 먼저 반영
$_SESSION['last_access'] = time();
// 변경사항 저장 및 잠금 해제
session_write_close();
// 이후 무거운 작업 수행
sleep(3);
?>만약 작업이 끝난 후 반드시 세션 값을 업데이트해야 하는 상황이라면, 작업 완료 후에 session_start()를 다시 호출하여 세션을 재열고 값 수정 후 저장하는 방식을 사용할 수도 있다.
PHP 세션 블로킹 해결은 웹 애플리케이션의 응답 속도와 사용자 경험을 극적으로 개선하는 핵심 최적화 작업이다. session_write_close()를 적재적소에 호출하는 작은 습관이 모여서 동시 요청 처리 성능을 대폭 향상시킨다는 점을 잊지 말자. 이 글의 실전 예제 패턴을 참고해 장시간 실행되는 스크립트에 바로 적용하면, 세션 병목 없는 쾌적한 비동기 서버 환경을 구축할 수 있을 것이다.