대규모 웹 서비스를 운영하다 보면 서비스의 목적에 따라 여러 대의 서버를 구성하거나 서브도메인을 분리하는 경우가 많습니다. 예를 들어 메인 서비스는 www.example.com, 사용자 마이 페이지는 mypage.example.com, 게시판은 community.example.com과 같이 운영하는 방식입니다. 하지만 이 경우 사용자가 하나의 도메인에서 로그인하더라도 다른 서브도메인으로 이동하면 세션이 유지되지 않아 다시 로그인해야 하는 불편함이 발생합니다.
이러한 현상이 발생하는 이유는 크게 두 가지입니다. 첫째, 브라우저가 쿠키를 저장할 때 기본적으로 현재 도메인으로 범위를 제한하기 때문입니다. 둘째, PHP의 기본 세션 관리 방식은 각 서버의 로컬 파일 시스템에 세션 데이터를 저장하므로 물리적으로 분리된 서버 간에는 데이터를 참조할 수 없기 때문입니다.
이 문제를 해결하기 위해서는 모든 서브도메인이 동일한 세션 ID(PHPSESSID)를 공유하도록 설정하고, 세션 데이터를 공유 데이터베이스(DB)에 저장하여 관리해야 합니다.
1. 쿠키 도메인 범위 확장
서로 다른 서브도메인에서 동일한 세션 ID를 공유하려면 PHP 설정 중 session.cookie_domain을 수정해야 합니다. 기본적으로 이 값은 현재 호스트명으로 설정되어 있지만, 이를 상위 도메인 형식으로 변경하면 모든 서브도메인에서 해당 쿠키에 접근할 수 있게 됩니다.
// 모든 서브도메인에서 세션 쿠키를 공유하도록 설정
ini_set('session.cookie_domain', '.example.com');
2. 세션 저장을 위한 데이터베이스 스키마 설계
세션 데이터를 파일이 아닌 공통 데이터베이스에 저장하기 위해 아래와 같은 테이블 구조가 필요합니다. 모든 서버가 접근 가능한 중앙 DB 서버에 생성합니다.
CREATE TABLE app_sessions (
sess_id VARCHAR(128) NOT NULL,
sess_expiry INT(11) UNSIGNED NOT NULL,
sess_data TEXT,
PRIMARY KEY (sess_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. 커스텀 세션 핸들러 구현
PHP의 session_set_save_handler() 함수를 사용하면 세션의 읽기, 쓰기, 삭제 등의 동작을 직접 정의할 수 있습니다. 아래 코드는 데이터베이스를 활용하여 세션을 관리하는 로직의 예시입니다.
class DatabaseSessionHandler {
private $db;
private $table = 'app_sessions';
public function __construct($host, $user, $pass, $name) {
$this->db = mysqli_connect($host, $user, $pass, $name);
if (!$this->db) {
die("Connection failed: " . mysqli_connect_error());
}
}
public function open($path, $name) {
return true;
}
public function close() {
return mysqli_close($this->db);
}
public function read($id) {
$id = mysqli_real_escape_string($this->db, $id);
$sql = "SELECT sess_data FROM {$this->table} WHERE sess_id = '$id' AND sess_expiry > " . time();
$result = mysqli_query($this->db, $sql);
if ($row = mysqli_fetch_assoc($result)) {
return $row['sess_data'];
}
return '';
}
public function write($id, $data) {
$id = mysqli_real_escape_string($this->db, $id);
$data = mysqli_real_escape_string($this->db, $data);
$expiry = time() + (int)ini_get('session.gc_maxlifetime');
$sql = "REPLACE INTO {$this->table} (sess_id, sess_expiry, sess_data) VALUES ('$id', $expiry, '$data')";
return mysqli_query($this->db, $sql);
}
public function destroy($id) {
$id = mysqli_real_escape_string($this->db, $id);
$sql = "DELETE FROM {$this->table} WHERE sess_id = '$id'";
return mysqli_query($this->db, $sql);
}
public function gc($maxlifetime) {
$old = time() - $maxlifetime;
$sql = "DELETE FROM {$this->table} WHERE sess_expiry < $old";
return mysqli_query($this->db, $sql);
}
}
// 설정 적용
$handler = new DatabaseSessionHandler('localhost', 'db_user', 'db_password', 'db_name');
session_set_save_handler(
array($handler, 'open'),
array($handler, 'close'),
array($handler, 'read'),
array($handler, 'write'),
array($handler, 'destroy'),
array($handler, 'gc')
);
// 세션 시작 전 쿠키 도메인 및 환경 설정
ini_set('session.use_cookies', 1);
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_domain', '.example.com');
ini_set('session.cookie_path', '/');
session_start();
4. 요약 및 주의사항
위와 같이 설정하면 사용자가 a.example.com에서 생성한 세션 정보가 데이터베이스에 저장되고, b.example.com으로 이동했을 때 브라우저가 동일한 세션 ID를 전송하므로 데이터베이스에서 기존 세션 정보를 그대로 불러올 수 있습니다.
이 방식을 구현할 때 주의할 점은 다음과 같습니다.
- 모든 서버의 시간(NTP)이 동기화되어야 세션 만료 시간을 정확하게 계산할 수 있습니다.
- 트래픽이 매우 높은 사이트의 경우 관계형 데이터베이스(MySQL 등)보다는 Redis나 Memcached와 같은 메모리 기반 저장소를 사용하는 것이 성능상 유리합니다.
- 보안을 위해
session.cookie_httponly옵션을 활성화하여 스크립트를 통한 세션 탈취를 방지하는 것이 권장됩니다.