지난 Consola 포스트에서 개발자 친화적인 로깅을 다뤘다면, 이번에는 Java 개발자들에게 친숙한 Log4js를 살펴보겠습니다.
Log4js는 Java의 Log4j를 JavaScript로 포팅한 로깅 라이브러리로, 엔터프라이즈 환경에서 검증된 로깅 패턴과 아키텍처를 제공합니다. Java 백그라운드를 가진 개발팀이나 기존 Java 시스템과의 통합이 필요한 프로젝트에서 특히 유용합니다.
Log4js란?
Log4js는 2010년부터 개발되기 시작한 Node.js 로깅 라이브러리로, Apache Log4j의 설계 철학과 구조를 JavaScript 환경에 맞게 구현했습니다. “Logger, Appender, Layout” 이라는 핵심 개념을 통해 체계적인 로깅 아키텍처를 제공합니다.
Log4js의 핵심 개념
Logger: 로그 메시지를 생성하는 주체 Appender: 로그를 출력하는 대상 (콘솔, 파일, 데이터베이스 등) Layout: 로그 메시지의 형식을 정의 Category: 로거를 논리적으로 그룹화하는 단위
이 구조는 Java 개발자들에게 매우 친숙하며, 대규모 애플리케이션에서 로깅을 체계적으로 관리할 수 있게 해줍니다.
설치 및 기본 설정
npm install log4js
# 또는
yarn add log4js
기본 설정 파일
Log4js는 설정 파일을 통해 로깅 동작을 제어합니다:
// config/log4js.config.js
export default {
appenders: {
// 콘솔 출력 설정
console: {
type: 'console'
},
// 파일 출력 설정
file: {
type: 'file',
filename: 'logs/app.log',
maxLogSize: 10485760, // 10MB
backups: 5,
compress: true
},
// 에러 전용 파일
errorFile: {
type: 'file',
filename: 'logs/error.log',
maxLogSize: 10485760,
backups: 3
},
// 날짜별 로그 파일
dateFile: {
type: 'dateFile',
filename: 'logs/app',
pattern: '-yyyy-MM-dd.log',
compress: true,
daysToKeep: 30
}
},
categories: {
// 기본 카테고리
default: {
appenders: ['console', 'file'],
level: 'info'
},
// API 전용 카테고리
api: {
appenders: ['console', 'dateFile'],
level: 'debug'
},
// 에러 전용 카테고리
error: {
appenders: ['console', 'errorFile'],
level: 'error'
},
// 데이터베이스 관련 카테고리
database: {
appenders: ['console', 'file'],
level: 'warn'
}
}
};
로거 초기화
// lib/logger.js
import log4js from 'log4js';
import config from '../config/log4js.config.js';
// 설정 적용
log4js.configure(config);
// 카테고리별 로거 생성
export const defaultLogger = log4js.getLogger();
export const apiLogger = log4js.getLogger('api');
export const errorLogger = log4js.getLogger('error');
export const dbLogger = log4js.getLogger('database');
// 기본 로거를 default export
export default defaultLogger;
로그 출력 형태와 사용법
기본 사용법
import { defaultLogger, apiLogger, errorLogger } from '../lib/logger';
// 기본 로거 사용
defaultLogger.trace('매우 상세한 추적 정보');
defaultLogger.debug('디버그 정보');
defaultLogger.info('일반 정보');
defaultLogger.warn('경고 메시지');
defaultLogger.error('오류 발생');
defaultLogger.fatal('치명적 오류');
// 카테고리별 로거 사용
apiLogger.info('API 요청 처리');
errorLogger.error('시스템 오류 발생');
출력 예시
[2024-01-15T10:30:45.123] [INFO] default - 서버가 시작되었습니다
[2024-01-15T10:30:46.456] [WARN] default - 메모리 사용량이 높습니다
[2024-01-15T10:30:47.789] [ERROR] error - 데이터베이스 연결 실패
[2024-01-15T10:30:48.123] [DEBUG] api - 사용자 조회 요청: userId=123
Next.js 특화 설정
환경별 설정 관리
// config/log4js.config.js 수정
const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';
const createConfig = () => {
const baseConfig = {
appenders: {
console: {
type: 'console',
layout: {
type: 'pattern',
pattern: '%d{yyyy-MM-dd hh:mm:ss.SSS} [%p] %c - %m'
}
}
},
categories: {
default: {
appenders: ['console'],
level: isDevelopment ? 'debug' : 'info'
}
}
};
// 개발 환경 설정
if (isDevelopment) {
baseConfig.appenders.console.layout.pattern =
'%d{hh:mm:ss.SSS} %[[%p]%] %c - %m';
baseConfig.categories.default.level = 'debug';
}
// 프로덕션 환경 설정
if (isProduction) {
// 파일 로깅 추가
baseConfig.appenders.file = {
type: 'file',
filename: 'logs/app.log',
maxLogSize: 50 * 1024 * 1024, // 50MB
backups: 10,
compress: true
};
baseConfig.appenders.errorFile = {
type: 'file',
filename: 'logs/error.log',
maxLogSize: 10 * 1024 * 1024, // 10MB
backups: 5
};
// 카테고리 업데이트
baseConfig.categories.default.appenders = ['console', 'file'];
baseConfig.categories.error = {
appenders: ['console', 'errorFile'],
level: 'error'
};
}
return baseConfig;
};
export default createConfig();
Next.js 미들웨어 통합
// lib/logging-middleware.js
import log4js from 'log4js';
import config from '../config/log4js.config.js';
log4js.configure(config);
const logger = log4js.getLogger('middleware');
export function createLoggingMiddleware() {
return (req, res, next) => {
const startTime = Date.now();
const originalSend = res.send;
// 요청 정보 로깅
logger.info('Request started', {
method: req.method,
url: req.url,
userAgent: req.get('User-Agent'),
ip: req.ip,
timestamp: new Date().toISOString()
});
// 응답 가로채기
res.send = function(data) {
const duration = Date.now() - startTime;
logger.info('Request completed', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: `${duration}ms`,
contentLength: data ? data.length : 0
});
originalSend.call(this, data);
};
// 에러 처리
res.on('error', (error) => {
logger.error('Response error', {
method: req.method,
url: req.url,
error: error.message,
stack: error.stack
});
});
next();
};
}
// Express.js 스타일 미들웨어를 Next.js에서 사용
export function withLogging(handler) {
return async (req, res) => {
const startTime = Date.now();
logger.info('API request', {
method: req.method,
url: req.url,
query: req.query,
body: req.method !== 'GET' ? req.body : undefined
});
try {
const result = await handler(req, res);
logger.info('API response', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: Date.now() - startTime
});
return result;
} catch (error) {
logger.error('API error', {
method: req.method,
url: req.url,
error: error.message,
stack: error.stack,
duration: Date.now() - startTime
});
throw error;
}
};
}
실제 프로젝트 적용 예시
1. API 라우트 로깅
// pages/api/users/[id].js
import log4js from 'log4js';
import { withLogging } from '../../../lib/logging-middleware';
const logger = log4js.getLogger('api.users');
async function handler(req, res) {
const { id } = req.query;
const { method } = req;
try {
switch (method) {
case 'GET':
logger.debug(`Fetching user with ID: ${id}`);
const user = await getUserById(id);
if (!user) {
logger.warn(`User not found: ${id}`);
return res.status(404).json({ error: 'User not found' });
}
logger.info(`User retrieved successfully: ${id}`, {
userId: id,
userName: user.name
});
return res.status(200).json(user);
case 'PUT':
logger.info(`Updating user: ${id}`, {
updatedFields: Object.keys(req.body)
});
const updatedUser = await updateUser(id, req.body);
logger.info(`User updated successfully: ${id}`, {
userId: id,
changes: Object.keys(req.body)
});
return res.status(200).json(updatedUser);
case 'DELETE':
logger.warn(`Deleting user: ${id}`, {
userId: id,
requestedBy: req.user?.id
});
await deleteUser(id);
logger.info(`User deleted successfully: ${id}`);
return res.status(204).end();
default:
logger.error(`Unsupported method: ${method}`, {
method,
endpoint: req.url
});
return res.status(405).json({
error: `Method ${method} not allowed`
});
}
} catch (error) {
logger.error('API handler error', {
method,
userId: id,
error: error.message,
stack: error.stack
});
throw error;
}
}
export default withLogging(handler);
2. 데이터베이스 로깅
// lib/database-logger.js
import log4js from 'log4js';
const dbLogger = log4js.getLogger('database');
export class DatabaseLogger {
static logQuery(query, params = [], executionTime) {
dbLogger.debug('Database query executed', {
query: query.replace(/\s+/g, ' ').trim(),
params,
executionTime: `${executionTime}ms`
});
}
static logSlowQuery(query, params = [], executionTime) {
dbLogger.warn('Slow query detected', {
query: query.replace(/\s+/g, ' ').trim(),
params,
executionTime: `${executionTime}ms`,
threshold: '1000ms'
});
}
static logConnection(action, database) {
dbLogger.info(`Database ${action}`, {
database,
timestamp: new Date().toISOString()
});
}
static logError(error, query = null, params = []) {
dbLogger.error('Database error', {
error: error.message,
code: error.code,
query: query ? query.replace(/\s+/g, ' ').trim() : null,
params,
stack: error.stack
});
}
static logTransaction(action, transactionId) {
dbLogger.debug(`Transaction ${action}`, {
transactionId,
timestamp: new Date().toISOString()
});
}
}
// 데이터베이스 함수에서 사용
export async function executeQuery(query, params = []) {
const startTime = Date.now();
try {
DatabaseLogger.logQuery(query, params, 0);
const result = await db.query(query, params);
const executionTime = Date.now() - startTime;
DatabaseLogger.logQuery(query, params, executionTime);
// 느린 쿼리 감지 (1초 이상)
if (executionTime > 1000) {
DatabaseLogger.logSlowQuery(query, params, executionTime);
}
return result;
} catch (error) {
DatabaseLogger.logError(error, query, params);
throw error;
}
}
3. 비즈니스 로직 로깅
// services/user-service.js
import log4js from 'log4js';
const serviceLogger = log4js.getLogger('service.user');
export class UserService {
static async createUser(userData) {
const correlationId = generateCorrelationId();
serviceLogger.info('User creation started', {
correlationId,
email: userData.email,
role: userData.role
});
try {
// 유효성 검사
serviceLogger.debug('Validating user data', { correlationId });
await this.validateUserData(userData);
// 중복 확인
serviceLogger.debug('Checking for duplicate email', {
correlationId,
email: userData.email
});
const existingUser = await User.findByEmail(userData.email);
if (existingUser) {
serviceLogger.warn('Duplicate email attempt', {
correlationId,
email: userData.email,
existingUserId: existingUser.id
});
throw new Error('Email already exists');
}
// 사용자 생성
serviceLogger.debug('Creating user in database', { correlationId });
const user = await User.create(userData);
// 환영 이메일 전송
serviceLogger.debug('Sending welcome email', {
correlationId,
userId: user.id,
email: user.email
});
await EmailService.sendWelcomeEmail(user);
serviceLogger.info('User creation completed', {
correlationId,
userId: user.id,
email: user.email
});
return user;
} catch (error) {
serviceLogger.error('User creation failed', {
correlationId,
email: userData.email,
error: error.message,
stack: error.stack
});
throw error;
}
}
static async validateUserData(userData) {
const logger = log4js.getLogger('service.user.validation');
logger.debug('Starting user data validation', {
fields: Object.keys(userData)
});
const validationErrors = [];
if (!userData.email || !isValidEmail(userData.email)) {
validationErrors.push('Invalid email format');
logger.warn('Invalid email format', { email: userData.email });
}
if (!userData.password || userData.password.length < 8) {
validationErrors.push('Password too short');
logger.warn('Password validation failed', {
reason: 'too short',
length: userData.password?.length
});
}
if (validationErrors.length > 0) {
logger.error('User data validation failed', {
errors: validationErrors,
userData: { ...userData, password: '[REDACTED]' }
});
throw new ValidationError(validationErrors);
}
logger.debug('User data validation passed');
}
}
function generateCorrelationId() {
return Math.random().toString(36).substr(2, 9);
}
4. 에러 경계 로깅
// components/ErrorBoundary.js
import React from 'react';
import log4js from 'log4js';
const errorLogger = log4js.getLogger('error.react');
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, errorId: null };
}
static getDerivedStateFromError(error) {
const errorId = `ERR_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
return { hasError: true, errorId };
}
componentDidCatch(error, errorInfo) {
const { errorId } = this.state;
errorLogger.fatal('React component error', {
errorId,
error: {
name: error.name,
message: error.message,
stack: error.stack
},
errorInfo: {
componentStack: errorInfo.componentStack
},
props: this.props,
url: typeof window !== 'undefined' ? window.location.href : 'SSR',
userAgent: typeof window !== 'undefined' ? navigator.userAgent : 'SSR',
timestamp: new Date().toISOString()
});
// 에러 추적 서비스에 전송
if (typeof window !== 'undefined' && process.env.NODE_ENV === 'production') {
this.sendErrorToTracking(error, errorInfo, errorId);
}
}
sendErrorToTracking(error, errorInfo, errorId) {
errorLogger.info('Sending error to tracking service', { errorId });
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
errorId,
error: {
name: error.name,
message: error.message,
stack: error.stack
},
errorInfo,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString()
})
}).catch(err => {
errorLogger.error('Failed to send error to tracking service', {
errorId,
trackingError: err.message
});
});
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>문제가 발생했습니다</h2>
<p>오류가 자동으로 보고되었습니다.</p>
<p>오류 ID: {this.state.errorId}</p>
<button onClick={() => window.location.reload()}>
페이지 새로고침
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
고급 기능 활용
1. 커스텀 Appender 생성
// lib/custom-appenders.js
import log4js from 'log4js';
// Slack 알림 Appender
function slackAppender(config, layout) {
return (loggingEvent) => {
const message = layout(loggingEvent);
// ERROR 레벨 이상만 Slack으로 전송
if (loggingEvent.level.isGreaterThanOrEqualTo(log4js.levels.ERROR)) {
sendToSlack({
text: `🚨 Production Error Alert`,
attachments: [{
color: 'danger',
fields: [{
title: 'Message',
value: message,
short: false
}, {
title: 'Category',
value: loggingEvent.categoryName,
short: true
}, {
title: 'Level',
value: loggingEvent.level.levelStr,
short: true
}, {
title: 'Timestamp',
value: loggingEvent.startTime.toISOString(),
short: true
}]
}]
});
}
};
}
async function sendToSlack(payload) {
try {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
} catch (error) {
console.error('Failed to send Slack notification:', error);
}
}
// 데이터베이스 Appender
function databaseAppender(config, layout) {
return async (loggingEvent) => {
try {
const logEntry = {
timestamp: loggingEvent.startTime,
level: loggingEvent.level.levelStr,
category: loggingEvent.categoryName,
message: layout(loggingEvent),
data: loggingEvent.data.length > 0 ? JSON.stringify(loggingEvent.data) : null,
hostname: require('os').hostname(),
pid: process.pid
};
await saveLogToDatabase(logEntry);
} catch (error) {
console.error('Failed to save log to database:', error);
}
};
}
// Appender 등록
log4js.configure({
appenders: {
slack: { type: slackAppender },
database: { type: databaseAppender }
},
categories: {
default: { appenders: ['console'], level: 'info' },
production: { appenders: ['console', 'slack', 'database'], level: 'warn' }
}
});
2. 동적 로그 레벨 변경
// lib/dynamic-logging.js
import log4js from 'log4js';
export class DynamicLoggingManager {
constructor() {
this.originalLevels = {};
this.setupRuntimeControls();
}
// 런타임에 로그 레벨 변경
setLogLevel(category, level) {
const logger = log4js.getLogger(category);
// 원래 레벨 백업
if (!this.originalLevels[category]) {
this.originalLevels[category] = logger.level;
}
logger.level = level;
log4js.getLogger('system').info('Log level changed', {
category,
oldLevel: this.originalLevels[category],
newLevel: level
});
}
// 모든 카테고리 로그 레벨 리셋
resetLogLevels() {
Object.keys(this.originalLevels).forEach(category => {
const logger = log4js.getLogger(category);
logger.level = this.originalLevels[category];
});
this.originalLevels = {};
log4js.getLogger('system').info('All log levels reset to original');
}
// 현재 로그 레벨 상태 조회
getLogLevels() {
const levels = {};
// 설정된 모든 카테고리 조회
const config = log4js.getConfiguration();
Object.keys(config.categories).forEach(category => {
const logger = log4js.getLogger(category);
levels[category] = {
current: logger.level.levelStr,
original: this.originalLevels[category]?.levelStr || logger.level.levelStr
};
});
return levels;
}
// HTTP API로 로그 레벨 제어
setupRuntimeControls() {
// Express.js 라우트 예시
if (process.env.NODE_ENV !== 'production') {
const express = require('express');
const app = express();
// 현재 로그 레벨 조회
app.get('/admin/log-levels', (req, res) => {
res.json(this.getLogLevels());
});
// 로그 레벨 변경
app.post('/admin/log-levels/:category', (req, res) => {
const { category } = req.params;
const { level } = req.body;
try {
this.setLogLevel(category, level);
res.json({ success: true, category, level });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// 로그 레벨 리셋
app.post('/admin/log-levels/reset', (req, res) => {
this.resetLogLevels();
res.json({ success: true });
});
}
}
}
// 전역 인스턴스
export const loggingManager = new DynamicLoggingManager();
3. 로그 필터링과 마스킹
// lib/log-filters.js
import log4js from 'log4js';
// 민감한 정보 마스킹 함수
function maskSensitiveData(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
const sensitiveFields = [
'password', 'token', 'apiKey', 'secret', 'authorization',
'creditCard', 'ssn', 'phoneNumber', 'email'
];
const masked = { ...obj };
Object.keys(masked).forEach(key => {
const lowerKey = key.toLowerCase();
if (sensitiveFields.some(field => lowerKey.includes(field))) {
if (typeof masked[key] === 'string') {
// 이메일은 부분 마스킹
if (lowerKey.includes('email')) {
masked[key] = masked[key].replace(/(.{2})(.*)(@.*)/, '$1***$3');
} else {
// 나머지는 완전 마스킹
masked[key] = '[REDACTED]';
}
} else {
masked[key] = '[REDACTED]';
}
} else if (typeof masked[key] === 'object') {
masked[key] = maskSensitiveData(masked[key]);
}
});
return masked;
}
// 커스텀 Layout
function secureLayout() {
return (loggingEvent) => {
const timestamp = loggingEvent.startTime.toISOString();
const level = loggingEvent.level.levelStr;
const category = loggingEvent.categoryName;
// 메시지와 데이터 마스킹
const message = loggingEvent.data[0];
const data = loggingEvent.data.slice(1).map(maskSensitiveData);
const logParts = [
`[${timestamp}]`,
`[${level}]`,
`${category} -`,
message
];
if (data.length > 0) {
logParts.push(JSON.stringify(data, null, 2));
}
return logParts.join(' ');
};
}
// 로그 필터 (특정 조건에서만 로그 출력)
function createConditionalAppender(appender, condition) {
return (loggingEvent) => {
if (condition(loggingEvent)) {
appender(loggingEvent);
}
};
}
// 설정에서 사용
log4js.configure({
appenders: {
console: {
type: 'console',
layout: { type: secureLayout }
},
// 에러 레벨만 파일에 저장
errorFile: createConditionalAppender(
log4js.appenders.file({ filename: 'logs/error.log' }),
(event) => event.level.isGreaterThanOrEqualTo(log4js.levels.ERROR)
),
// 특정 카테고리만 별도 파일에 저장
apiFile: createConditionalAppender(
log4js.appenders.file({ filename: 'logs/api.log' }),
(event) => event.categoryName.startsWith('api')
)
},
categories: {
default: { appenders: ['console'], level: 'info' }
}
});
성능 최적화와 모니터링
1. 로그 성능 모니터링
// lib/log-performance.js
import log4js from 'log4js';
class LogPerformanceMonitor {
constructor() {
this.stats = {
totalLogs: 0,
logsByLevel: {},
logsByCategory: {},
averageLogTime: 0,
slowLogs: []
};
this.startTime = Date.now();
this.setupMonitoring();
}
setupMonitoring() {
// 원본 로거 메서드 래핑
const originalGetLogger = log4js.getLogger;
log4js.getLogger = (categoryName) => {
const logger = originalGetLogger.call(log4js, categoryName);
return this.wrapLogger(logger, categoryName);
};
}
wrapLogger(logger, categoryName) {
const originalMethods = {};
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
levels.forEach(level => {
originalMethods[level] = logger[level].bind(logger);
logger[level] = (...args) => {
const startTime = process.hrtime.bigint();
// 원본 메서드 호출
const result = originalMethods[level](...args);
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1000000; // nanoseconds to milliseconds
// 통계 업데이트
this.updateStats(level, categoryName, duration, args);
return result;
};
});
return logger;
}
updateStats(level, category, duration, args) {
this.stats.totalLogs++;
// 레벨별 통계
this.stats.logsByLevel[level] = (this.stats.logsByLevel[level] || 0) + 1;
// 카테고리별 통계
this.stats.logsByCategory[category] = (this.stats.logsByCategory[category] || 0) + 1;
// 평균 로그 시간 계산
this.stats.averageLogTime = (this.stats.averageLogTime * (this.stats.totalLogs - 1) + duration) / this.stats.totalLogs;
// 느린 로그 감지 (1ms 이상)
if (duration > 1) {
this.stats.slowLogs.push({
level,
category,
duration,
message: args[0],
timestamp: new Date().toISOString()
});
// 최대 100개까지만 저장
if (this.stats.slowLogs.length > 100) {
this.stats.slowLogs.shift();
}
}
}
getStats() {
const uptime = Date.now() - this.startTime;
return {
...this.stats,
uptime: `${Math.round(uptime / 1000)}s`,
logsPerSecond: Math.round(this.stats.totalLogs / (uptime / 1000))
};
}
// 주기적 통계 로깅
startPeriodicReporting(interval = 60000) { // 기본 1분
setInterval(() => {
const logger = log4js.getLogger('performance');
logger.info('Log performance stats', this.getStats());
}, interval);
}
}
export const logMonitor = new LogPerformanceMonitor();
// 개발 환경에서만 모니터링 활성화
if (process.env.NODE_ENV === 'development') {
logMonitor.startPeriodicReporting();
}
2. 로그 배치 처리
// lib/batch-logger.js
import log4js from 'log4js';
class BatchLogger {
constructor(options = {}) {
this.batchSize = options.batchSize || 100;
this.flushInterval = options.flushInterval || 5000; // 5초
this.maxRetries = options.maxRetries || 3;
this.buffer = [];
this.retryQueue = [];
this.timer = null;
this.startBatching();
}
log(level, category, message, data = {}) {
this.buffer.push({
level,
category,
message,
data,
timestamp: new Date().toISOString(),
id: this.generateId()
});
// 버퍼가 가득 차면 즉시 플러시
if (this.buffer.length >= this.batchSize) {
this.flush();
}
}
startBatching() {
this.timer = setInterval(() => {
this.flush();
}, this.flushInterval);
}
async flush() {
if (this.buffer.length === 0 && this.retryQueue.length === 0) {
return;
}
// 현재 버퍼와 재시도 큐 합치기
const logsToProcess = [...this.buffer, ...this.retryQueue];
this.buffer = [];
this.retryQueue = [];
try {
await this.processBatch(logsToProcess);
} catch (error) {
// 실패한 로그들을 재시도 큐에 추가
logsToProcess.forEach(log => {
log.retryCount = (log.retryCount || 0) + 1;
if (log.retryCount <= this.maxRetries) {
this.retryQueue.push(log);
} else {
// 최대 재시도 횟수 초과 시 일반 로거로 출력
const logger = log4js.getLogger('batch.failed');
logger.error('Failed to process log after max retries', {
originalLog: log,
error: error.message
});
}
});
}
}
async processBatch(logs) {
// 외부 로그 서비스로 전송
const response = await fetch(process.env.LOG_SERVICE_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.LOG_SERVICE_TOKEN}`
},
body: JSON.stringify({ logs })
});
if (!response.ok) {
throw new Error(`Log service responded with ${response.status}`);
}
const logger = log4js.getLogger('batch.success');
logger.debug(`Successfully processed batch of ${logs.length} logs`);
}
generateId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
// 프로세스 종료 시 남은 로그 플러시
shutdown() {
if (this.timer) {
clearInterval(this.timer);
}
return this.flush();
}
}
export const batchLogger = new BatchLogger();
// 프로세스 종료 처리
process.on('SIGINT', async () => {
await batchLogger.shutdown();
process.exit(0);
});
process.on('SIGTERM', async () => {
await batchLogger.shutdown();
process.exit(0);
});
Log4js vs 다른 라이브러리 비교
Winston과의 상세 비교
특성 | Log4js | Winston |
---|---|---|
설정 방식 | 설정 객체 중심 | 코드 중심 |
카테고리 시스템 | 내장 지원 | 수동 구현 필요 |
Appender 종류 | 다양함 | Transport로 제공 |
Java 호환성 | 높음 | 낮음 |
커뮤니티 크기 | 중간 | 큼 |
성능 | 좋음 | 좋음 |
사용 사례별 추천
Log4js 추천 상황:
- Java 백그라운드 팀
- 카테고리 기반 로깅 필요
- 설정 파일 중심 관리 선호
- 엔터프라이즈 환경
Winston 추천 상황:
- 높은 유연성 필요
- 다양한 Transport 활용
- 복잡한 로그 포맷팅
- 대용량 로그 처리
실전 팁과 베스트 프랙티스
1. 카테고리 설계 전략
// 효과적인 카테고리 구조
const categories = {
// 레이어별 분류
'controller.user': { appenders: ['console', 'api'], level: 'info' },
'service.user': { appenders: ['console', 'business'], level: 'debug' },
'repository.user': { appenders: ['console', 'data'], level: 'debug' },
// 기능별 분류
'auth.login': { appenders: ['console', 'security'], level: 'info' },
'auth.logout': { appenders: ['console', 'security'], level: 'info' },
'payment.process': { appenders: ['console', 'financial'], level: 'info' },
// 외부 연동별 분류
'external.stripe': { appenders: ['console', 'external'], level: 'warn' },
'external.sendgrid': { appenders: ['console', 'external'], level: 'warn' },
// 환경별 분류
'system.health': { appenders: ['console', 'monitoring'], level: 'info' },
'system.performance': { appenders: ['console', 'monitoring'], level: 'debug' }
};
2. 구조화된 로깅 패턴
// 일관된 로그 구조 사용
const logger = log4js.getLogger('api.orders');
// Good: 구조화된 데이터
logger.info('Order processing started', {
orderId: order.id,
customerId: order.customerId,
amount: order.total,
items: order.items.length,
paymentMethod: order.paymentMethod
});
// Bad: 문자열 내 데이터 포함
logger.info(`Order ${order.id} for customer ${order.customerId} processing started`);
3. 에러 로깅 전략
// 계층별 에러 처리
try {
await processOrder(orderData);
} catch (error) {
if (error instanceof ValidationError) {
logger.warn('Order validation failed', {
orderId: orderData.id,
validationErrors: error.errors
});
} else if (error instanceof PaymentError) {
logger.error('Payment processing failed', {
orderId: orderData.id,
paymentMethod: orderData.paymentMethod,
error: error.message,
providerResponse: error.providerResponse
});
} else {
logger.fatal('Unexpected error in order processing', {
orderId: orderData.id,
error: error.message,
stack: error.stack
});
}
throw error;
}
마무리
Log4js는 Java의 검증된 로깅 패턴을 JavaScript 환경에 성공적으로 적용한 라이브러리입니다. 특히 엔터프라이즈 환경이나 Java 백그라운드를 가진 팀에서는 매우 친숙하고 효과적인 선택입니다.
카테고리 기반의 체계적인 로깅 관리와 다양한 Appender를 통한 유연한 출력 방식은 대규모 애플리케이션에서 진가를 발휘합니다. 하지만 최신 JavaScript 트렌드나 성능을 최우선으로 하는 환경에서는 다른 대안들도 고려해볼 필요가 있습니다.
다음 포스트에서는 시각적 임팩트에 특화된 Signale을 다뤄보겠습니다. CLI 도구나 개발 스크립트에서 사용자 경험을 극대화하는 로깅 방법을 알아보세요.
1 thought on “Log4js로 Next.js 엔터프라이즈 로깅 시스템 구축하기”