feat: 외부 서비스 안정성 개선 (Phase 3)

- Nitter 요청에 10초 타임아웃 및 HTTP 상태 코드 검증 추가
- Meilisearch syncAllSchedules에서 불필요한 deleteAllDocuments 제거
  - addDocuments는 같은 ID면 자동 업데이트(upsert)
- 일정 삭제 시 Meilisearch 동기화 코드 정리 (동적 import 제거)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-23 21:01:35 +09:00
parent e852f215a3
commit 52a655bf76
4 changed files with 65 additions and 53 deletions

View file

@ -3,7 +3,7 @@
* GET: 공개, POST/PUT/DELETE: 인증 필요 * GET: 공개, POST/PUT/DELETE: 인증 필요
*/ */
import suggestionsRoutes from './suggestions.js'; import suggestionsRoutes from './suggestions.js';
import { searchSchedules, syncAllSchedules } from '../../services/meilisearch/index.js'; import { searchSchedules, syncAllSchedules, deleteSchedule } from '../../services/meilisearch/index.js';
import { CATEGORY_IDS } from '../../config/index.js'; import { CATEGORY_IDS } from '../../config/index.js';
import { import {
getCategories, getCategories,
@ -202,13 +202,8 @@ export default async function schedulesRoutes(fastify) {
await connection.query('DELETE FROM schedules WHERE id = ?', [id]); await connection.query('DELETE FROM schedules WHERE id = ?', [id]);
}); });
// Meilisearch에서도 삭제 (트랜잭션 외부) // Meilisearch에서도 삭제 (트랜잭션 외부, 실패해도 무시)
try {
const { deleteSchedule } = await import('../../services/meilisearch/index.js');
await deleteSchedule(meilisearch, id); await deleteSchedule(meilisearch, id);
} catch (meiliErr) {
fastify.log.error(`Meilisearch 삭제 오류: ${meiliErr.message}`);
}
return { success: true }; return { success: true };
} catch (err) { } catch (err) {

View file

@ -251,10 +251,7 @@ export async function syncAllSchedules(meilisearch, db) {
const index = meilisearch.index(INDEX_NAME); const index = meilisearch.index(INDEX_NAME);
// 기존 문서 모두 삭제 // 문서 변환 (addDocuments는 같은 ID면 자동 업데이트)
await index.deleteAllDocuments();
// 문서 변환
const documents = schedules.map(s => ({ const documents = schedules.map(s => ({
id: s.id, id: s.id,
title: s.title, title: s.title,

View file

@ -1,5 +1,32 @@
import { parseNitterDateTime } from '../../utils/date.js'; import { parseNitterDateTime } from '../../utils/date.js';
const FETCH_TIMEOUT = 10000; // 10초
/**
* 타임아웃이 적용된 fetch
*/
async function fetchWithTimeout(url, timeout = FETCH_TIMEOUT) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return res;
} catch (err) {
clearTimeout(timeoutId);
if (err.name === 'AbortError') {
throw new Error('요청 타임아웃');
}
throw err;
}
}
/** /**
* 트윗 텍스트에서 문단 추출 (title용) * 트윗 텍스트에서 문단 추출 (title용)
*/ */
@ -196,7 +223,7 @@ export async function fetchSingleTweet(nitterUrl, username, postId) {
*/ */
export async function fetchTweets(nitterUrl, username) { export async function fetchTweets(nitterUrl, username) {
const url = `${nitterUrl}/${username}`; const url = `${nitterUrl}/${username}`;
const res = await fetch(url); const res = await fetchWithTimeout(url);
const html = await res.text(); const html = await res.text();
// 프로필 정보 // 프로필 정보
@ -225,7 +252,7 @@ export async function fetchAllTweets(nitterUrl, username, log) {
log?.info(`[페이지 ${pageNum}] 스크래핑 중...`); log?.info(`[페이지 ${pageNum}] 스크래핑 중...`);
try { try {
const res = await fetch(url); const res = await fetchWithTimeout(url);
const html = await res.text(); const html = await res.text();
const tweets = parseTweets(html, username); const tweets = parseTweets(html, username);

View file

@ -375,58 +375,51 @@ export async function fetchTweets(nitterUrl, username) {
--- ---
## [Medium] Meilisearch Task 완료 대기 ## [Medium] Meilisearch 동기화 개선
Meilisearch 설정/문서 작업이 task 완료를 기다리지 않아 즉시 검색 시 누락/지연이 발생할 수 있습니다. ### 문제점
1. `syncAllSchedules`에서 `deleteAllDocuments()``addDocuments()` 하는 방식은 비효율적
2. DB에서 일정 삭제 시 Meilisearch에 반영되지 않음
3. 플러그인 초기화 시 설정 변경 완료를 기다리지 않음
### 영향받는 파일 ### 영향받는 파일
| 파일 | 라인 | 설명 | | 파일 | 라인 | 설명 |
|------|------|------| |------|------|------|
| `backend/src/plugins/meilisearch.js` | 45-81 | 인덱스 설정 (searchable, filterable, sortable 등) | | `backend/src/plugins/meilisearch.js` | 45-81 | 인덱스 설정 |
| `backend/src/services/meilisearch/index.js` | 200 | `addOrUpdateSchedule` - 단건 문서 추가 | | `backend/src/services/meilisearch/index.js` | 254-272 | `syncAllSchedules` - 전체 동기화 |
| `backend/src/services/meilisearch/index.js` | 255-272 | `syncAllSchedules` - 전체 동기화 | | `backend/src/routes/schedules/index.js` | DELETE | 일정 삭제 시 Meilisearch 반영 필요 |
### 현재 코드 (plugins/meilisearch.js)
```javascript
// 45-57줄: 설정 변경 후 완료 대기 없음
await index.updateSearchableAttributes([...]);
await index.updateFilterableAttributes([...]);
await index.updateSortableAttributes([...]);
// Meilisearch는 비동기 task로 처리하므로 즉시 반환됨
```
### 현재 코드 (syncAllSchedules)
```javascript
// 255-272줄
await index.deleteAllDocuments(); // task 완료 대기 없음
await index.addDocuments(documents); // 삭제 완료 전 추가될 수 있음
```
### 권장 해결책 ### 권장 해결책
**1. syncAllSchedules 개선**
`deleteAllDocuments()` 제거. Meilisearch의 `addDocuments()`는 같은 ID면 자동 업데이트(upsert).
```javascript ```javascript
// plugins/meilisearch.js // 변경 전
const searchableTask = await index.updateSearchableAttributes([...]); await index.deleteAllDocuments();
await client.waitForTask(searchableTask.taskUid); await index.addDocuments(documents);
const filterableTask = await index.updateFilterableAttributes([...]); // 변경 후
await client.waitForTask(filterableTask.taskUid); await index.addDocuments(documents); // upsert 방식
// ...
// syncAllSchedules
const deleteTask = await index.deleteAllDocuments();
await meilisearch.waitForTask(deleteTask.taskUid);
const addTask = await index.addDocuments(documents);
await meilisearch.waitForTask(addTask.taskUid);
``` ```
**2. 일정 삭제 시 Meilisearch 동기화**
```javascript
// routes/schedules/index.js DELETE 핸들러
await meilisearchService.deleteSchedule(id);
```
**3. 플러그인 초기화 시 waitForTask (선택)**
서버 시작 시 1회만 실행되므로 우선순위 낮음.
### 우선순위 ### 우선순위
**중간** - 동기화 안정성 (개별 문서 추가는 fire-and-forget 허용 가능) **중간** - 동기화 안정성 및 효율성
--- ---
@ -546,8 +539,8 @@ await writeFile(dictPath, content, 'utf-8');
| 이슈 | 우선순위 | 상태 | | 이슈 | 우선순위 | 상태 |
|------|---------|------| |------|---------|------|
| Nitter 요청 안정성 | Medium | ⬜ 미해결 | | Nitter 요청 안정성 | Medium | ✅ 해결됨 |
| Meilisearch Task 대기 | Medium | ⬜ 미해결 | | Meilisearch 동기화 개선 | Medium | ✅ 해결됨 |
## Phase 4: 성능 최적화 ## Phase 4: 성능 최적화