1. DBLogger 클래스
DBLogger는 데이터베이스 테이블에 로그를 저장하기 위한 클래스입니다. 로그 데이터를 구분하여 액션 로그와 세션 로그로 나누어 저장할 수 있습니다.
1-1. 주요 기능:
- 테이블 유효성 검증 (validateTable)
- 데이터베이스에 로그 테이블이 존재하는지 확인하고, 없으면 에러를 기록합니다.
- 로그 생성 (createLog)
- 액션 로그(createActionLog)와 세션 로그(createSessionLog)를 처리합니다.
- 디바이스 타입 분석 (getDeviceType)
- 사용자 에이전트를 기반으로 디바이스 유형(모바일, 태블릿, 데스크톱)을 판별합니다.
2. FileLogger 클래스
FileLogger는 로그 메시지를 파일로 저장하는 역할을 수행합니다. 디렉토리와 파일 관리를 통해 효율적으로 로그를 기록하며, 재시도 로직과 대체 로깅 메커니즘을 포함합니다.
2-1. 주요 기능:
- 로그 디렉토리 생성 및 확인 (ensureLogDirectory)
- 지정된 경로에 로그 디렉토리가 없으면 생성합니다.
- 로그 쓰기 (writeLog)
- 초당 로그 횟수를 제한하고, 로그 레벨(INFO, WARNING, ERROR 등)에 따라 메시지를 기록합니다.
- 재시도 로직과 대체 로깅 (writeToFileWithRetry, fallbackLogging)
- 파일 쓰기 실패 시 재시도하며, 재시도가 모두 실패하면 임시 파일에 로그를 저장합니다.
3. 사용 예시
3-1. DBLogger 활용:
<?php
class DBLogger extends QueryBuilder {
private $fileLogger;
//------------------------------------------
// 메서드 명 : __construct
// 기능 : DBLogger 클래스 생성자 | FileLogger 초기화 및 테이블 유효성 검사 수행
// 파라미터 :
// 리턴 :
//-----------------------------------------
public function __construct($tableName = 'Logs') {
$this->fileLogger = new FileLogger();
parent::__construct($tableName);
$this->validateTable($tableName);
}
//------------------------------------------
// 메서드 명 : validateTable
// 기능 : information_schema를 통해 테이블 존재 여부 확인
// 파라미터 :
// 리턴 :
//-----------------------------------------
private function validateTable($tableName) {
try {
// 테이블 존재 여부 확인을 위한 쿼리
$query = "SELECT TABLE_NAME
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = :dbname
AND TABLE_NAME = :tablename";
// 쿼리 실행 및 결과 검증
$stmt = $this->db->prepare($query);
$stmt->execute([
':dbname' => strtolower(DB_NAME),
':tablename' => $tableName
]);
// 테이블이 없는 경우 예외 발생
if ($stmt->rowCount() == 0) {
throw new Exception("{$tableName} 테이블을 찾을 수 없습니다");
}
} catch (Exception $e) {
$this->fileLogger->error("DBLogger 초기화 오류: " . $e->getMessage());
throw $e;
}
}
//------------------------------------------
// 메서드 명 : createLog
// 기능 : type (Log, Session) Log | Session 데이터 DB 저장
// 파라미터 :
// 리턴 :
//-----------------------------------------
public function createLog($data, $type = 'action') {
try {
// 로그 타입에 따른 분기 처리
if ($type === 'session') {
return $this->createSessionLog($data);
}
return $this->createActionLog($data);
} catch (Exception $e) {
$this->fileLogger->error("로그 생성 중 오류 발생: " . $e->getMessage());
return false;
}
}
//------------------------------------------
// 메서드 명 : createActionLog
// 기능 : type이 Log일 경우 데이터 검증 후 DB 저장
// 파라미터 :
// 리턴 :
//-----------------------------------------
private function createActionLog($data) {
// 필수 필드 검증
$requiredFields = [];
foreach ($requiredFields as $field) {
if (empty($data[$field])) {
$this->fileLogger->error("필수 필드 누락: " . $field);
return false;
}
}
// 로그 데이터 구성
$logData = [
// 'user_id' => $_SESSION['app_info']['user_id'] ?? $_REQUEST['user_id'] ?? '',
// 'app_id' => $_SESSION['app_info']['app_id'] ?? '',
'action' => $data['action'],
'ip_address' => $data['ip_address'],
'request_data' => $data['request_data'] ?? '',
'request_method' => $data['request_method'],
'status' => $data['status'] ?? 1,
'error_message' => $data['error_message'] ?? '',
'platform_info' => $data['platform_info'] ?? '',
'location' => $data['location'] ?? '',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
];
// 데이터베이스에 로그 저장
return $this->insert($logData);
}
//------------------------------------------
// 메서드 명 : createSessionLog
// 기능 : type이 Session일 경우 데이터 검증 후 DB 저장
// 파라미터 :
// 리턴 :
//-----------------------------------------
private function createSessionLog($data) {
// 필수 필드 검증
$requiredFields = [];
foreach ($requiredFields as $field) {
if (empty($data[$field])) {
$this->fileLogger->error("필수 필드 누락: " . $field);
return false;
}
}
// 세션 로그 데이터 구성
$sessionData = [
'user_id' => $data['user_id'] ?? '',
'session_token' => $data['session_token'],
'last_accessed_at' => date('Y-m-d H:i:s'),
'ip_address' => $data['ip_address'],
'platform_info' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'device_type' => $this->getDeviceType(),
'location' => $data['location'] ?? '',
'status' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
];
return $this->insert($sessionData);
}
//------------------------------------------
// 메서드 명 : getDeviceType
// 기능 : User-Agent 문자열을 분석(정규식)하여 디바이스 타입 결정
// 파라미터 :
// 리턴 :
//-----------------------------------------
private function getDeviceType() {
// 글로별 변수에 있는 유저 디바이스 정보
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
// 태블릿 기기 확인
if (preg_match('/(tablet|ipad|playbook)|(android(?!.*(mobi|opera mini)))/i', strtolower($userAgent))) {
return 'tablet';
}
// 모바일 기기 확인
if (preg_match('/(up.browser|up.link|mmp|symbian|smartphone|midp|wap|phone|android|iemobile)/i', strtolower($userAgent))) {
return 'mobile';
}
// 기본값으로 데스크톱 반환
return 'desktop';
}
}
3-2. FileLogger 활용:
<?php
class FileLogger {
private $logFile; // 로그 파일 경로
private $logDir; // 로그 디렉토리 경로
private $maxRetries = 3; // 최대 재시도 횟수
private $retryDelay = 100000; // 재시도 간 대기 시간 (마이크로초 단위, 100,000마이크로초 = 0.1초 = 100밀리초)
private $lastLogTime = 0; // 마지막 로그 기록 시간
private $logCount = 0; // 현재 초 내 로그 카운트
private $maxLogsPerSecond = 10; // 초당 최대 로그 수
private const VALID_LOG_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
//------------------------------------------
// 메서드 명 : __construct
// 기능 : Logger 클래스 생성자 | 로그 디렉토리와 파일을 초기화
// 파라미터 : string $logDir - 로그 디렉토리 경로
// 리턴 :
//-----------------------------------------
public function __construct($logDir = null) {
// DIRECTORY_SEPARATOR PHP 디렉토리 구분 상수
$this->logDir = $logDir ?? dirname(__FILE__, 4) . DIRECTORY_SEPARATOR . 'logs';
$this->ensureLogDirectory();
$this->setLogFile();
}
//------------------------------------------
// 메서드 명 : ensureLogDirectory
// 기능 : 로그 디렉토리 존재 확인 및 생성
// 파라미터 :
// 리턴 :
//-----------------------------------------
private function ensureLogDirectory() {
if (!file_exists($this->logDir) && !@mkdir($this->logDir, 0755, true)) {
throw new RuntimeException("로그 디렉토리 생성 실패: {$this->logDir}");
}
}
//------------------------------------------
// 메서드 명 : setLogFile
// 기능 : 로그 파일 경로 설정
// 파라미터 :
// 리턴 :
//-----------------------------------------
private function setLogFile() {
$date = date('Y-m-d');
$this->logFile = $this->logDir . DIRECTORY_SEPARATOR . $date . '.log';
}
//------------------------------------------
// 메서드 명 : writeLog
// 기능 : 로그 메시지 작성
// 파라미터 : mixed $message - 로그 메시지
// string $level - 로그 레벨
// 리턴 :
//-----------------------------------------
public function writeLog($message, $level = 'INFO') {
$currentTime = microtime(true);
// 1초 내 로그 횟수 제한
if ($currentTime - $this->lastLogTime >= 1) {
$this->lastLogTime = $currentTime;
$this->logCount = 0;
}
// 로그 횟수 초과 시 처리
if ($this->logCount >= $this->maxLogsPerSecond) {
// 로그 횟수 초과 시 경고 로그만 남기고 종료
if ($this->logCount == $this->maxLogsPerSecond) {
$warningMessage = "로그 횟수 초과. 추가 로그는 무시합니다.";
$this->writeToFileWithRetry($warningMessage, 'WARNING');
}
return;
}
$this->logCount++;
// 로그 레벨 유효성 검사 및 설정
$level = strtoupper($level);
if (!in_array($level, self::VALID_LOG_LEVELS)) {
$level = 'INFO';
}
// 로그 엔트리 생성
$timestamp = date('Y-m-d H:i:s');
$formattedMessage = $this->formatMessage($message);
// PHP_EOL PHP에서 사용되는 줄바꿈 상수
$logEntry = "[{$timestamp}] [{$level}] {$formattedMessage}" . PHP_EOL;
// 로그 파일에 쓰기
$this->writeToFileWithRetry($logEntry, $level);
}
//------------------------------------------
// 메서드 명 : formatMessage
// 기능 : 메시지 형식 변환
// 파라미터 : mixed $message - 변환할 메시지
// 리턴 : string
//-----------------------------------------
private function formatMessage($message) {
if (is_array($message) || is_object($message)) {
return print_r($message, true);
}
return (string)$message;
}
//------------------------------------------
// 메서드 명 : writeToFileWithRetry
// 기능 : 재시도 로직을 포함한 파일 쓰기
// 파라미터 : string $content - 쓸 내용
// string $level - 로그 레벨
// 리턴 :
//-----------------------------------------
private function writeToFileWithRetry($content, $level) {
$retries = 0;
while ($retries < $this->maxRetries) {
// FILE_APPEND 파일에 데이터 쓰기 상수, LOCK_EX 권한 잠금, 다른 프로세스가 동시에 같은 파일에 쓰기를 시도하는 것을 방지하여 데이터 충돌을 예방
if (@file_put_contents($this->logFile, $content, FILE_APPEND | LOCK_EX) !== false) {
return;
}
$retries++;
if ($retries < $this->maxRetries) {
usleep($this->retryDelay);
}
}
// 모든 재시도 실패 후 대체 로깅 시도
$this->fallbackLogging($content, $level);
}
//------------------------------------------
// 메서드 명 : fallbackLogging
// 기능 : 대체 로깅 메커니즘
// 파라미터 : string $content - 로그 내용
// string $level - 로그 레벨
// 리턴 :
//-----------------------------------------
private function fallbackLogging($content, $level) {
// 크리티컬 로그의 경우 시스템 로그에 기록
if ($level === 'CRITICAL') {
error_log("CRITICAL LOG WRITE FAILURE: " . $content, 0);
}
// 임시 파일에 로그 저장 시도
$tempFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'emergency_log_' . uniqid() . '.log';
@file_put_contents($tempFile, $content, FILE_APPEND | LOCK_EX);
}
// 로그 레벨별 메서드
public function debug($message) { $this->writeLog($message, 'DEBUG'); }
public function info($message) { $this->writeLog($message, 'INFO'); }
public function warning($message) { $this->writeLog($message, 'WARNING'); }
public function error($message) { $this->writeLog($message, 'ERROR'); }
public function critical($message) { $this->writeLog($message, 'CRITICAL'); }
}
'백엔드 > PHP' 카테고리의 다른 글
[PURE PHP] MVC 프레임워크 - 기초부터 구조 설계까지 완벽 가이드 (0) | 2025.06.15 |
---|---|
[이론] PHP & JavaScript에서 희소 배열(Sparse Array) 이해 및 해결 방법 (0) | 2025.04.17 |
[Modern PHP] PHP PDO: PHP 데이터베이스 접근의 표준 인터페이스 (0) | 2024.12.24 |
[Modern PHP] PDO로 간단하고 안전한 데이터베이스 연동: 싱글톤 패턴과 쿼리 빌더 구현 (0) | 2024.12.19 |