게임 서버에서 로깅은 단순히 콘솔에 메시지를 출력하는 기능이 아니다. 운영 환경에서 발생한 오류를 추적하고, 요청 처리 과정을 분석하며, 장애의 원인을 파악하기 위한 핵심 관측 시스템이다.
특히 실시간 멀티플레이어 게임 서버는 다수의 클라이언트가 동시에 연결되고, API 요청과 WebSocket 이벤트가 비동기적으로 처리된다. 로그가 일관된 형식으로 기록되지 않으면 특정 사용자의 요청부터 데이터베이스 오류, 서버 간 통신 문제까지 연결해서 분석하기 어렵다.
따라서 로그를 설계할 때는 구조화된 로그, 로그 레벨, 로그 로테이션, Request ID, Correlation ID를 함께 고려해야 한다.
구조화된 로그
구조화된 로그(Structured Logging)는 로그를 자유로운 문자열이 아니라 일정한 필드와 값으로 구성된 데이터로 기록하는 방식이다.
일반적인 로그는 다음과 같다.
2026-10-24 18:30:12 ERROR 결제 처리 실패 userId=abc123
사람이 읽기에는 어렵지 않지만, 로그 수집 시스템에서 오류 유형이나 사용자 ID를 기준으로 검색하려면 문자열을 분석해야 한다. 메시지 형식이 변경되면 파싱 규칙도 영향을 받는다.
구조화된 로그는 같은 정보를 JSON으로 표현한다.
{
"level": "error",
"time": "2026-10-24T09:30:12.000Z",
"event": "payment.failed",
"userId": "user-abc",
"requestId": "req-xyz",
"error": {
"message": "Database connection failed"
}
}
이렇게 기록하면 로그 수집 시스템에서 level, event, userId, requestId를 각각 독립적인 필드로 검색하고 집계할 수 있다.
구조화된 로그의 설계 원칙
일관된 필드 이름
같은 의미의 데이터는 모든 서비스에서 동일한 필드 이름을 사용한다.
userId: 사용자 식별자requestId: 개별 요청 식별자correlationId: 여러 작업을 연결하는 식별자event: 발생한 사건의 종류durationMs: 처리 시간error: 오류 정보
서비스마다 user_id, userId, uid를 혼용하면 검색과 대시보드 구성의 복잡도가 증가한다.
이벤트 중심 설계
로그 메시지를 자연어 문장에만 의존하지 않고, 사건을 식별할 수 있는 이벤트 이름을 정의한다.
{
"event": "matchmaking.completed",
"userId": "user-abc",
"roomId": "room-def",
"durationMs": 245
}
이벤트 이름이 일관되면 매칭 성공률, 처리 시간, 실패율 등을 로그 기반으로 집계할 수 있다.
필요한 정보만 기록
로그에 사용자 비밀번호, 인증 토큰, 개인정보, 결제 인증 정보 등을 기록해서는 안 된다. 장애 분석에 필요한 최소한의 정보만 남기고, 식별 정보도 접근 권한과 보존 정책을 고려해 관리한다.
Pino
Pino는 Node.js 환경에서 사용하는 고성능 로깅 라이브러리다. JSON 기반 구조화된 로그를 지원하며, 게임 서버와 API 서버에서 운영 로그를 기록하는 데 적합하다.
Express 기반 서버에서는 HTTP 요청 로그, 비즈니스 이벤트, 오류 로그를 동일한 로거를 통해 기록할 수 있다.
설치
npm install pino pino-http
pino는 로그를 기록하는 핵심 라이브러리이며, pino-http는 HTTP 요청과 응답에 대한 로깅 기능을 제공한다.
기본 로거 구성
// logger.js
const pino = require("pino");
const logger = pino({
level: process.env.LOG_LEVEL || "info",
timestamp: pino.stdTimeFunctions.isoTime,
redact: {
paths: [
"password",
"token",
"authorization",
"req.headers.authorization"
],
censor: "[REDACTED]"
}
});
module.exports = logger;
이 구성은 로그 레벨을 환경변수로 제어하고, 시간을 ISO 형식으로 기록하며, 민감한 필드를 마스킹한다.
단, redact는 실제 로그 객체의 필드 경로와 일치해야 한다. 인증 정보가 다른 경로에 들어 있다면 해당 경로도 추가해야 한다.
비즈니스 이벤트 기록
const logger = require("./logger");
async function completeMatch(match) {
try {
await saveMatchResult(match);
logger.info(
{
event: "match.completed",
matchId: match.id,
roomId: match.roomId,
playerCount: match.players.length
},
"Match completed"
);
} catch (error) {
logger.error(
{
event: "match.failed",
matchId: match.id,
err: error
},
"Failed to complete match"
);
throw error;
}
}
Pino는 객체를 첫 번째 인자로 전달해 구조화된 필드를 기록할 수 있다. 오류 객체는 err처럼 오류 전용 필드로 전달하면 스택 트레이스 등 오류 정보를 일관되게 기록할 수 있다.
실제 운영에서는 사용자 ID나 매치 ID가 로그에 필요한지, 그리고 해당 식별 정보가 개인정보나 보안 정책에 저촉되지 않는지 함께 검토해야 한다.
Express 요청 로깅
const express = require("express");
const pinoHttp = require("pino-http");
const logger = require("./logger");
const app = express();
app.use(
pinoHttp({
logger,
customLogLevel(req, res, error) {
if (error || res.statusCode >= 500) {
return "error";
}
if (res.statusCode >= 400) {
return "warn";
}
return "info";
}
})
);
app.get("/health", (req, res) => {
res.json({ status: "ok" });
});
app.listen(3000);
이 설정은 HTTP 요청과 응답을 자동으로 기록하고, 서버 오류나 클라이언트 오류에 따라 로그 레벨을 구분한다.
단, HTTP 요청 로그만으로 게임 서버의 모든 동작을 추적할 수 있는 것은 아니다. WebSocket 메시지, 매칭, 룸 생성, 보상 지급 등의 비즈니스 이벤트는 별도로 기록해야 한다.
또한 요청 로그는 요청량에 비례해 증가하므로 헬스 체크 요청을 제외하거나 로그 수집 단계에서 필터링하는 방법도 고려할 수 있다.
로그 레벨
로그 레벨은 이벤트의 중요도와 운영상 대응 필요성을 구분하는 기준이다.
일관된 로그 레벨을 사용하면 운영자는 심각한 오류를 우선 확인하고, 불필요한 로그로 인한 비용과 노이즈를 줄일 수 있다.
| 레벨 | 용도 | 예시 |
|---|---|---|
trace | 매우 상세한 내부 흐름 | 세부 프로토콜 처리 단계 |
debug | 개발 및 문제 분석 | 매칭 조건 평가 결과 |
info | 정상적인 주요 이벤트 | 로그인 성공, 룸 생성 |
warn | 주의가 필요한 상황 | 재시도 발생, 요청 제한 초과 |
error | 작업 실패 및 오류 | DB 저장 실패, 외부 서비스 호출 실패 |
fatal | 프로세스 지속이 어려운 치명적 오류 | 필수 초기화 실패로 인한 서버 종료 |
Pino는 이러한 레벨을 지원하며, 로그 레벨을 설정해 지정된 중요도 이상의 로그만 출력할 수 있다.
예를 들어 info로 설정하면 trace와 debug 로그는 출력되지 않지만 info, warn, error, fatal은 출력된다.
const pino = require("pino");
const logger = pino({
level: process.env.LOG_LEVEL || "info"
});
logger.debug({ event: "matchmaking.check" }, "Checking matchmaking");
logger.info({ event: "room.created", roomId: "room-abc" }, "Room created");
logger.warn({ event: "database.retry" }, "Retrying database operation");
logger.error({ event: "payment.failed" }, "Payment failed");
운영 환경의 로그 레벨 전략
개발 환경에서는 debug를 사용해 내부 동작을 자세히 확인할 수 있다. 운영 환경에서는 일반적으로 info를 기본값으로 사용하고, 문제가 발생한 구간에서만 일시적으로 상세 로깅을 활성화한다.
단, 로그 레벨을 높이는 것만으로 모든 문제가 해결되지는 않는다. 결제나 보상 지급과 같이 중요한 비즈니스 이벤트는 일반적인 디버깅 로그와 별개로 누락 없이 기록되도록 설계해야 한다.
또한 fatal 로그가 출력되었다고 해서 프로세스가 자동으로 종료되는 것은 아니다. 실제 종료와 복구 동작은 애플리케이션의 오류 처리 및 프로세스 관리 정책에 따라 결정된다.
로그 로테이션
로그 로테이션(Log Rotation)은 로그 파일이 무한정 증가하지 않도록 일정한 기준에 따라 파일을 분리하고, 오래된 로그를 압축하거나 삭제하는 관리 방식이다.
로그 로테이션이 없다면 장시간 실행되는 서버에서 로그 파일이 디스크를 가득 채울 수 있다. 디스크 공간 부족은 단순히 로그 기록 실패에 그치지 않고 데이터베이스, 애플리케이션, 컨테이너의 정상 동작까지 위협할 수 있다.
주요 로테이션 기준
- 시간 기준: 매일 또는 매시간 로그 파일 분리
- 크기 기준: 파일 크기가 임계값을 넘으면 새 파일 생성
- 보존 기간 기준: 일정 기간이 지난 로그 삭제
- 압축: 오래된 로그를 압축해 저장 공간 절약
예를 들어 매일 로그를 분리하고 일정 기간 보관한 뒤 오래된 파일을 삭제하도록 구성할 수 있다.
Pino와 로그 파일 관리
Pino는 기본적으로 로그를 표준 출력이나 지정된 스트림에 기록하는 역할에 집중한다. 파일 회전과 장기 보존은 별도의 시스템에 맡기는 것이 일반적이다.
전통적인 VM 환경에서는 logrotate와 같은 시스템 도구를 사용할 수 있다. 애플리케이션에서 직접 파일을 관리해야 한다면 Pino의 스트림 구성이나 관련 도구를 검토할 수 있다.
컨테이너 환경에서는 애플리케이션이 로그를 표준 출력과 표준 오류로 내보내고, Docker 런타임이나 로그 수집 에이전트가 파일 관리와 수집을 담당하는 구조가 일반적이다.
Kubernetes 환경에서도 애플리케이션이 컨테이너 내부에 로그 파일을 무제한으로 쌓기보다는 표준 출력으로 로그를 내보내고, 노드의 로그 관리 체계와 중앙 로그 수집 시스템을 활용하는 편이 관리하기 쉽다.
로그 보존 정책
로그를 오래 보관할수록 장애 분석에 활용할 수 있는 범위는 넓어지지만 저장 비용과 보안 위험도 증가한다.
따라서 로그 유형별로 보존 기간을 구분하는 것이 합리적이다.
- 일반적인 요청 로그는 운영 분석에 필요한 기간만 보관한다.
- 오류 로그는 장애 조사와 재발 방지에 필요한 기간을 고려한다.
- 보안 감사 로그는 관련 정책과 법적 요구사항을 따른다.
- 디버그 로그는 필요한 구간에서만 활성화하고 장기 보관을 제한한다.
로그 로테이션은 로그 수집과 백업을 대체하지 않는다. 파일이 회전되거나 삭제되기 전에 중앙 저장소로 전송되어야 하는 로그라면 수집 성공 여부와 장애 시 재처리 정책도 함께 설계해야 한다.
Request ID와 Correlation ID
분산 환경에서는 하나의 사용자 요청이 여러 서버와 서비스에 걸쳐 처리될 수 있다. 이때 각 로그가 서로 연결되지 않으면 장애 원인을 파악하기 어렵다.
Request ID와 Correlation ID는 로그를 연결하기 위한 식별자다.
Request ID
Request ID는 개별 HTTP 요청이나 메시지 처리 단위를 식별한다.
클라이언트가 요청을 보내면 서버는 해당 요청에 고유한 ID를 부여하고, 요청 처리 중 생성되는 로그에 동일한 ID를 포함한다.
{
"event": "api.request",
"requestId": "req-a12",
"path": "/matches",
"method": "POST"
}
{
"event": "matchmaking.started",
"requestId": "req-a12",
"userId": "user-abc"
}
두 로그에 같은 requestId가 포함되어 있으므로 특정 요청이 매칭 처리로 이어지는 과정을 추적할 수 있다.
Correlation ID
Correlation ID는 서로 다른 요청이나 서비스 작업을 하나의 상위 작업 흐름으로 연결하기 위한 식별자다.
예를 들어 게임 결과를 처리하는 과정에서 API 서버가 메시지 큐에 이벤트를 발행하고, 정산 서버가 이를 소비한 뒤 알림 서버가 결과를 전송할 수 있다.
이 과정에서 각 서비스의 요청 ID는 달라질 수 있지만, 전체 작업 흐름을 나타내는 Correlation ID는 동일하게 유지할 수 있다.
{
"event": "match.result.saved",
"requestId": "req-a12",
"correlationId": "flow-m42",
"service": "game-api"
}
{
"event": "reward.granted",
"requestId": "req-b73",
"correlationId": "flow-m42",
"service": "reward-worker"
}
{
"event": "notification.sent",
"requestId": "req-c91",
"correlationId": "flow-m42",
"service": "notification-worker"
}
각 서비스의 requestId는 다르지만 correlationId가 같으므로 매치 결과 저장부터 보상 지급과 알림 전송까지 하나의 흐름으로 분석할 수 있다.
두 식별자의 역할은 프로젝트마다 달라질 수 있다. 중요한 것은 개별 처리 단위와 전체 비즈니스 흐름을 구분하고, 시스템 전반에서 동일한 규칙을 적용하는 것이다.
Express에서 Request ID와 Correlation ID 적용하기
미들웨어에서 요청 식별자를 생성하고 로거에 연결하면 요청 처리 중 발생하는 로그에 식별자를 일관되게 포함할 수 있다.
const express = require("express");
const { randomUUID } = require("node:crypto");
const pino = require("pino");
const pinoHttp = require("pino-http");
const app = express();
const logger = pino({
level: process.env.LOG_LEVEL || "info"
});
app.use(
pinoHttp({
logger,
genReqId(req, res) {
return randomUUID();
},
customProps(req) {
return {
correlationId: req.correlationId
};
}
})
);
app.use((req, res, next) => {
// 외부에서 전달된 값을 무조건 신뢰하지 않고
// 서버에서 검증하거나 새로 생성한다.
req.correlationId = randomUUID();
next();
});
app.post("/matches", async (req, res, next) => {
try {
req.log.info(
{
event: "matchmaking.started",
correlationId: req.correlationId
},
"Matchmaking started"
);
res.json({ status: "accepted" });
} catch (error) {
next(error);
}
});
app.listen(3000);
이 예시는 개념을 설명하기 위한 구성이다. pino-http의 customProps가 요청 속성을 읽는 시점과 앞선 미들웨어의 실행 순서를 고려해야 한다. 위 코드에서는 customProps가 correlationId를 읽기 전에 해당 속성이 설정되지 않을 수 있으므로, 실제 서비스에서는 식별자 초기화 미들웨어를 pino-http보다 먼저 등록하거나 genReqId에서 식별자를 함께 초기화하도록 구성해야 한다.
또한 클라이언트가 보낸 X-Request-ID나 X-Correlation-ID를 사용할 경우 형식과 길이를 검증하고, 외부 입력을 그대로 신뢰하지 않아야 한다. 요청 ID를 서버에서 새로 생성할지, 검증된 전달 값을 수용할지도 서비스 경계에 따라 결정해야 한다.
서비스 간 ID 전파
HTTP 요청에서 생성한 Correlation ID를 다른 서비스로 전달하려면 HTTP 헤더나 메시지 본문에 포함해야 한다.
메시지 큐를 사용하는 경우에는 이벤트 메타데이터에 식별자를 포함한다.
await messageQueue.publish("match.completed", {
matchId: match.id,
correlationId: req.correlationId
});
소비자는 메시지의 Correlation ID를 읽어 후속 작업 로그에 기록한다.
async function handleMatchCompleted(message) {
const log = logger.child({
correlationId: message.correlationId,
matchId: message.matchId
});
log.info(
{ event: "reward.processing.started" },
"Reward processing started"
);
await grantRewards(message.matchId);
}
이렇게 하면 비동기 작업이 원래 HTTP 요청과 다른 프로세스에서 실행되더라도 같은 비즈니스 흐름으로 추적할 수 있다.
단, 식별자를 로그에 기록하는 것과 요청의 실행 맥락을 전파하는 것은 별개의 작업이다. 비동기 작업의 재시도나 작업 분기에서도 동일한 Correlation ID를 유지하도록 메시지 계약을 정의해야 한다.
실시간 게임 서버에서의 로깅 전략
실시간 게임 서버에서는 HTTP 요청뿐 아니라 WebSocket 연결, 룸 이동, 매칭, 게임 종료, 서버 간 통신 등 다양한 이벤트가 발생한다.
모든 패킷을 로그로 남기면 로그 양이 급증하고 디스크 및 수집 비용이 증가할 수 있다. 따라서 운영에 필요한 이벤트와 상세 디버깅 정보를 구분해야 한다.
WebSocket 연결 로그
연결과 해제는 접속 장애와 세션 문제를 분석하는 데 유용하다.
logger.info({
event: "websocket.connected",
sessionId,
userId
});
logger.info({
event: "websocket.disconnected",
sessionId,
userId,
reason: "client_disconnect"
});
세션 식별자는 연결 단위의 상태를 추적하는 데 사용하고, 사용자 식별자는 접근 정책에 따라 기록한다. 인증 토큰이나 세션 비밀값 자체를 기록해서는 안 된다.
매칭 및 룸 로그
logger.info({
event: "room.created",
roomId,
playerCount
});
logger.info({
event: "match.completed",
matchId,
roomId,
durationMs
});
이벤트 로그를 집계하면 룸 생성 빈도, 평균 경기 시간, 매칭 실패율 등 운영 지표를 분석할 수 있다.
오류와 성능 로그
logger.warn({
event: "websocket.reconnect",
sessionId,
attempt,
delayMs
});
logger.error({
event: "room.state_sync.failed",
roomId,
err: error
});
재접속이나 일시적인 통신 문제는 warn으로 기록할 수 있고, 상태 동기화 작업의 실패는 상황에 따라 error로 기록할 수 있다. 단순히 오류가 발생했다는 사실만 남기지 말고, 재시도 여부와 최종 처리 결과도 기록해야 한다.
고빈도 게임 틱이나 개별 이동 패킷은 기본적으로 매번 기록하기보다 샘플링, 집계, 이상 상황 중심 로깅을 적용하는 것이 효율적이다.
로그 수집과 모니터링
애플리케이션에서 로그를 기록하는 것만으로 운영 관측 체계가 완성되지는 않는다. 로그를 수집하고 검색하며, 오류 발생 시 빠르게 확인할 수 있는 환경이 필요하다.
일반적인 구성은 다음과 같다.
- 애플리케이션: Pino로 구조화된 로그 생성
- 로그 수집: Fluent Bit, Grafana Alloy 등으로 로그 수집 및 전달
- 로그 저장 및 검색: Loki 또는 Elasticsearch 계열 시스템
- 시각화 및 분석: Grafana 등의 대시보드
- 알림: 오류율 급증, 특정 이벤트 증가, 서비스 장애에 대한 경보
운영 환경에서는 로그뿐 아니라 메트릭과 분산 추적도 함께 활용하는 것이 좋다.
로그는 개별 사건의 상세 내용을 설명하고, 메트릭은 오류율과 지연 시간의 추세를 보여주며, 분산 추적은 서비스 간 요청의 실행 경로를 분석한다. Correlation ID는 로그를 연결하는 데 유용하지만, 그 자체가 완전한 분산 추적 시스템을 대체하지는 않는다.