From 7b227a6c56307488f2634ccbdf6654cb75169928 Mon Sep 17 00:00:00 2001 From: caadiq Date: Wed, 21 Jan 2026 14:20:32 +0900 Subject: [PATCH] =?UTF-8?q?refactor(backend):=20=EB=A1=9C=EA=B1=B0=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - utils/logger.js 생성 (createLogger) - 서비스 레이어: logger 유틸리티 사용 - 라우트 레이어: fastify.log 사용 - console.error/log → 구조화된 로깅으로 변경 Co-Authored-By: Claude Opus 4.5 --- backend/src/routes/albums/photos.js | 2 +- backend/src/routes/schedules/index.js | 2 +- backend/src/routes/schedules/suggestions.js | 4 +- backend/src/services/image.js | 5 ++- backend/src/services/meilisearch/index.js | 16 ++++---- backend/src/services/suggestions/index.js | 22 +++++----- backend/src/services/suggestions/morpheme.js | 23 ++++++----- backend/src/utils/logger.js | 43 ++++++++++++++++++++ docs/refactoring.md | 25 ++++++------ 9 files changed, 98 insertions(+), 44 deletions(-) create mode 100644 backend/src/utils/logger.js diff --git a/backend/src/routes/albums/photos.js b/backend/src/routes/albums/photos.js index a068d8f..a1cd529 100644 --- a/backend/src/routes/albums/photos.js +++ b/backend/src/routes/albums/photos.js @@ -195,7 +195,7 @@ export default async function photosRoutes(fastify) { reply.raw.end(); } catch (error) { await connection.rollback(); - console.error('사진 업로드 오류:', error); + fastify.log.error(`사진 업로드 오류: ${error.message}`); reply.raw.write(`data: ${JSON.stringify({ error: '사진 업로드 중 오류가 발생했습니다.' })}\n\n`); reply.raw.end(); } finally { diff --git a/backend/src/routes/schedules/index.js b/backend/src/routes/schedules/index.js index 5dfa4d8..086d832 100644 --- a/backend/src/routes/schedules/index.js +++ b/backend/src/routes/schedules/index.js @@ -261,6 +261,6 @@ async function saveSearchQueryAsync(fastify, query) { const service = new SuggestionService(fastify.db, fastify.redis); await service.saveSearchQuery(query); } catch (err) { - console.error('[Search] 검색어 저장 실패:', err.message); + fastify.log.error(`[Search] 검색어 저장 실패: ${err.message}`); } } diff --git a/backend/src/routes/schedules/suggestions.js b/backend/src/routes/schedules/suggestions.js index 1c765f4..2a089c3 100644 --- a/backend/src/routes/schedules/suggestions.js +++ b/backend/src/routes/schedules/suggestions.js @@ -15,7 +15,7 @@ export default async function suggestionsRoutes(fastify) { suggestionService = new SuggestionService(db, redis); // 비동기 초기화 (형태소 분석기 로드) suggestionService.initialize().catch(err => { - console.error('[Suggestions] 서비스 초기화 실패:', err.message); + fastify.log.error(`[Suggestions] 서비스 초기화 실패: ${err.message}`); }); } @@ -186,7 +186,7 @@ export default async function suggestionsRoutes(fastify) { return { message: '사전이 저장되었습니다.' }; } catch (error) { - console.error('[Suggestions] 사전 저장 오류:', error.message); + fastify.log.error(`[Suggestions] 사전 저장 오류: ${error.message}`); return reply.code(500).send({ error: '사전 저장 중 오류가 발생했습니다.' }); } }); diff --git a/backend/src/services/image.js b/backend/src/services/image.js index 3d6fb26..b012548 100644 --- a/backend/src/services/image.js +++ b/backend/src/services/image.js @@ -1,6 +1,9 @@ import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; import sharp from 'sharp'; import config from '../config/index.js'; +import { createLogger } from '../utils/logger.js'; + +const logger = createLogger('S3'); // S3 클라이언트 생성 const s3Client = new S3Client({ @@ -61,7 +64,7 @@ async function deleteFromS3(key) { Key: key, })); } catch (err) { - console.error(`S3 삭제 오류 (${key}):`, err.message); + logger.error(`삭제 오류 (${key}): ${err.message}`); } } diff --git a/backend/src/services/meilisearch/index.js b/backend/src/services/meilisearch/index.js index 547126e..76c0dab 100644 --- a/backend/src/services/meilisearch/index.js +++ b/backend/src/services/meilisearch/index.js @@ -7,8 +7,10 @@ */ import Inko from 'inko'; import config, { CATEGORY_IDS } from '../../config/index.js'; +import { createLogger } from '../../utils/logger.js'; const inko = new Inko(); +const logger = createLogger('Meilisearch'); const INDEX_NAME = 'schedules'; const MIN_SCORE = config.meilisearch.minScore; @@ -111,7 +113,7 @@ export async function searchSchedules(meilisearch, db, query, options = {}) { hasMore: offset + paginatedHits.length < total, }; } catch (err) { - console.error('[Meilisearch] 검색 오류:', err.message); + logger.error(`검색 오류: ${err.message}`); return { hits: [], total: 0, offset: 0, limit, hasMore: false }; } } @@ -183,9 +185,9 @@ export async function addOrUpdateSchedule(meilisearch, schedule) { }; await index.addDocuments([document]); - console.log(`[Meilisearch] 일정 추가/업데이트: ${schedule.id}`); + logger.info(`일정 추가/업데이트: ${schedule.id}`); } catch (err) { - console.error('[Meilisearch] 문서 추가 오류:', err.message); + logger.error(`문서 추가 오류: ${err.message}`); } } @@ -196,9 +198,9 @@ export async function deleteSchedule(meilisearch, scheduleId) { try { const index = meilisearch.index(INDEX_NAME); await index.deleteDocument(scheduleId); - console.log(`[Meilisearch] 일정 삭제: ${scheduleId}`); + logger.info(`일정 삭제: ${scheduleId}`); } catch (err) { - console.error('[Meilisearch] 문서 삭제 오류:', err.message); + logger.error(`문서 삭제 오류: ${err.message}`); } } @@ -249,11 +251,11 @@ export async function syncAllSchedules(meilisearch, db) { // 일괄 추가 await index.addDocuments(documents); - console.log(`[Meilisearch] ${documents.length}개 일정 동기화 완료`); + logger.info(`${documents.length}개 일정 동기화 완료`); return documents.length; } catch (err) { - console.error('[Meilisearch] 동기화 오류:', err.message); + logger.error(`동기화 오류: ${err.message}`); return 0; } } diff --git a/backend/src/services/suggestions/index.js b/backend/src/services/suggestions/index.js index fcea5d2..d7874bd 100644 --- a/backend/src/services/suggestions/index.js +++ b/backend/src/services/suggestions/index.js @@ -8,8 +8,10 @@ import Inko from 'inko'; import { extractNouns, initMorpheme, isReady } from './morpheme.js'; import { getChosung, isChosungOnly, isChosungMatch } from './chosung.js'; +import { createLogger } from '../../utils/logger.js'; const inko = new Inko(); +const logger = createLogger('Suggestion'); // 설정 const CONFIG = { @@ -42,9 +44,9 @@ export class SuggestionService { async initialize() { try { await initMorpheme(); - console.log('[Suggestion] 서비스 초기화 완료'); + logger.info('서비스 초기화 완료'); } catch (error) { - console.error('[Suggestion] 서비스 초기화 실패:', error.message); + logger.error(`서비스 초기화 실패: ${error.message}`); } } @@ -79,7 +81,7 @@ export class SuggestionService { if (this.isEnglishOnly(normalizedQuery)) { const korean = this.convertEnglishToKorean(normalizedQuery); if (korean) { - console.log(`[Suggestion] 한글 변환: "${normalizedQuery}" → "${korean}"`); + logger.debug(`한글 변환: "${normalizedQuery}" → "${korean}"`); normalizedQuery = korean; } } @@ -131,9 +133,9 @@ export class SuggestionService { } } - console.log(`[Suggestion] 저장: "${normalizedQuery}" → 명사: [${nouns.join(', ')}]`); + logger.debug(`저장: "${normalizedQuery}" → 명사: [${nouns.join(', ')}]`); } catch (error) { - console.error('[Suggestion] 저장 오류:', error.message); + logger.error(`저장 오류: ${error.message}`); } } @@ -171,7 +173,7 @@ export class SuggestionService { return await this.getPrefixSuggestions(searchQuery.trim(), koreanQuery?.trim(), limit); } } catch (error) { - console.error('[Suggestion] 조회 오류:', error.message); + logger.error(`조회 오류: ${error.message}`); return []; } } @@ -200,7 +202,7 @@ export class SuggestionService { return rows.map(r => `${prefix} ${r.word2}`); } catch (error) { - console.error('[Suggestion] Bi-gram 조회 오류:', error.message); + logger.error(`Bi-gram 조회 오류: ${error.message}`); return []; } } @@ -236,7 +238,7 @@ export class SuggestionService { return rows.map(r => r.query); } catch (error) { - console.error('[Suggestion] Prefix 조회 오류:', error.message); + logger.error(`Prefix 조회 오류: ${error.message}`); return []; } } @@ -258,7 +260,7 @@ export class SuggestionService { return rows.map(r => r.word); } catch (error) { - console.error('[Suggestion] 초성 검색 오류:', error.message); + logger.error(`초성 검색 오류: ${error.message}`); return []; } } @@ -293,7 +295,7 @@ export class SuggestionService { return result; } catch (error) { - console.error('[Suggestion] 인기 검색어 조회 오류:', error.message); + logger.error(`인기 검색어 조회 오류: ${error.message}`); return []; } } diff --git a/backend/src/services/suggestions/morpheme.js b/backend/src/services/suggestions/morpheme.js index 05d092c..46b551d 100644 --- a/backend/src/services/suggestions/morpheme.js +++ b/backend/src/services/suggestions/morpheme.js @@ -5,6 +5,9 @@ import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; +import { createLogger } from '../../utils/logger.js'; + +const logger = createLogger('Morpheme'); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -47,7 +50,7 @@ export async function initMorpheme() { initPromise = (async () => { try { - console.log('[Morpheme] kiwi-nlp 초기화 시작...'); + logger.info('kiwi-nlp 초기화 시작...'); // kiwi-nlp 동적 import (ESM) const { KiwiBuilder } = await import('kiwi-nlp'); @@ -69,7 +72,7 @@ export async function initMorpheme() { try { modelFiles[filename] = new Uint8Array(readFileSync(filepath)); } catch (err) { - console.warn(`[Morpheme] 모델 파일 로드 실패: ${filename}`); + logger.warn(`모델 파일 로드 실패: ${filename}`); } } @@ -78,18 +81,18 @@ export async function initMorpheme() { try { modelFiles[USER_DICT] = new Uint8Array(readFileSync(userDictPath)); userDicts = [USER_DICT]; - console.log('[Morpheme] 사용자 사전 로드 완료'); + logger.info('사용자 사전 로드 완료'); } catch (err) { - console.warn('[Morpheme] 사용자 사전 없음, 기본 사전만 사용'); + logger.warn('사용자 사전 없음, 기본 사전만 사용'); } // Kiwi 인스턴스 생성 kiwi = await builder.build({ modelFiles, userDicts }); isInitialized = true; - console.log('[Morpheme] kiwi-nlp 초기화 완료'); + logger.info('kiwi-nlp 초기화 완료'); } catch (error) { - console.error('[Morpheme] 초기화 실패:', error.message); + logger.error(`초기화 실패: ${error.message}`); // 초기화 실패해도 서비스는 계속 동작 (fallback 사용) } })(); @@ -114,7 +117,7 @@ export async function extractNouns(text) { // kiwi가 초기화되지 않았으면 fallback if (!kiwi) { - console.warn('[Morpheme] kiwi 미초기화, fallback 사용'); + logger.warn('kiwi 미초기화, fallback 사용'); return fallbackExtract(text); } @@ -141,7 +144,7 @@ export async function extractNouns(text) { return nouns.length > 0 ? nouns : fallbackExtract(text); } catch (error) { - console.error('[Morpheme] 형태소 분석 오류:', error.message); + logger.error(`형태소 분석 오류: ${error.message}`); return fallbackExtract(text); } } @@ -167,12 +170,12 @@ export function isReady() { * 형태소 분석기 리로드 (사전 변경 시 호출) */ export async function reloadMorpheme() { - console.log('[Morpheme] 리로드 시작...'); + logger.info('리로드 시작...'); isInitialized = false; kiwi = null; initPromise = null; await initMorpheme(); - console.log('[Morpheme] 리로드 완료'); + logger.info('리로드 완료'); } /** diff --git a/backend/src/utils/logger.js b/backend/src/utils/logger.js new file mode 100644 index 0000000..1e75bcd --- /dev/null +++ b/backend/src/utils/logger.js @@ -0,0 +1,43 @@ +/** + * 로거 유틸리티 + * 서비스 레이어에서 사용할 수 있는 간단한 로깅 유틸리티 + */ + +const PREFIX = { + info: '[INFO]', + warn: '[WARN]', + error: '[ERROR]', + debug: '[DEBUG]', +}; + +function formatMessage(level, context, message) { + const timestamp = new Date().toISOString(); + return `${timestamp} ${PREFIX[level]} [${context}] ${message}`; +} + +/** + * 로거 생성 + * @param {string} context - 로깅 컨텍스트 (예: 'Meilisearch', 'Suggestions') + * @returns {object} 로거 객체 + */ +export function createLogger(context) { + return { + info: (message, ...args) => { + console.log(formatMessage('info', context, message), ...args); + }, + warn: (message, ...args) => { + console.warn(formatMessage('warn', context, message), ...args); + }, + error: (message, ...args) => { + console.error(formatMessage('error', context, message), ...args); + }, + debug: (message, ...args) => { + if (process.env.DEBUG) { + console.debug(formatMessage('debug', context, message), ...args); + } + }, + }; +} + +// 기본 로거 (컨텍스트 없음) +export default createLogger('App'); diff --git a/docs/refactoring.md b/docs/refactoring.md index 22b5546..86f52d0 100644 --- a/docs/refactoring.md +++ b/docs/refactoring.md @@ -86,18 +86,19 @@ --- -### 10단계: 로거 통일 -- [ ] `src/utils/logger.js` 생성 -- [ ] 모든 `console.error/log` → logger 사용 +### 10단계: 로거 통일 ✅ 완료 +- [x] `src/utils/logger.js` 생성 +- [x] 모든 `console.error/log` → logger 또는 fastify.log 사용 -**대상 파일 (30개 이상):** -- `services/image.js:61` -- `services/meilisearch/index.js:112, 188, 201, 256` -- `services/suggestions/index.js:47, 136, 174, 203, 239, 261, 296` -- `services/suggestions/morpheme.js:92, 144` -- `routes/albums/photos.js:199` -- `routes/schedules/index.js:264` -- `routes/schedules/suggestions.js:18, 190` +**수정된 파일:** +- `src/utils/logger.js` - 로거 유틸리티 생성 (createLogger) +- `src/services/image.js` - logger 사용 +- `src/services/meilisearch/index.js` - logger 사용 +- `src/services/suggestions/index.js` - logger 사용 +- `src/services/suggestions/morpheme.js` - logger 사용 +- `src/routes/albums/photos.js` - fastify.log 사용 +- `src/routes/schedules/index.js` - fastify.log 사용 +- `src/routes/schedules/suggestions.js` - fastify.log 사용 --- @@ -120,7 +121,7 @@ | 7단계 | 순차→병렬 쿼리 | ✅ 완료 | | 8단계 | meilisearch 카테고리 ID | ✅ 완료 | | 9단계 | 응답 형식 통일 | ✅ 완료 | -| 10단계 | 로거 통일 | 대기 | +| 10단계 | 로거 통일 | ✅ 완료 | | 11단계 | 대형 핸들러 분리 | 대기 | ---