OpenAI API 스트리밍 응답 처리 시 메모리 누수 해결
문제 상황
사내 챗봇 서비스에 GPT-4 API를 붙이고 스트리밍 응답을 구현했다. 초기 테스트는 문제없었지만, 일주일 정도 운영 후 Node.js 프로세스의 메모리 사용량이 계속 증가하는 현상이 발견됐다.
원인 분석
OpenAI의 스트리밍 API는 SSE(Server-Sent Events) 방식을 사용한다. 우리 코드는 fetch로 스트림을 받아 처리했는데, 에러 발생이나 클라이언트 연결 종료 시 스트림을 제대로 닫지 않았다.
// 문제가 있던 코드
async function streamChat(prompt) {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { /* ... */ },
body: JSON.stringify({ stream: true, /* ... */ })
});
const reader = response.body.getReader();
while (true) {
const {done, value} = await reader.read();
if (done) break;
// 처리 로직
}
}
클라이언트가 중간에 연결을 끊어도 reader가 계속 메모리에 남아있었다.
해결 방법
AbortController를 사용해 명시적으로 요청을 취소하고, finally 블록에서 리소스를 정리했다.
async function streamChat(prompt, signal) {
const controller = new AbortController();
let reader;
try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { /* ... */ },
body: JSON.stringify({ stream: true, /* ... */ }),
signal: controller.signal
});
reader = response.body.getReader();
while (true) {
const {done, value} = await reader.read();
if (done) break;
// 처리 로직
}
} catch (error) {
if (error.name === 'AbortError') {
console.log('Stream aborted');
}
throw error;
} finally {
if (reader) {
await reader.cancel();
}
controller.abort();
}
}
Express 미들웨어에서 클라이언트 연결 종료를 감지하도록 추가했다.
app.post('/api/chat', (req, res) => {
const controller = new AbortController();
req.on('close', () => {
controller.abort();
});
streamChat(req.body.prompt, controller.signal);
});
결과
수정 후 일주일간 모니터링한 결과 메모리 사용량이 안정적으로 유지됐다. 스트리밍 API 사용 시 리소스 정리는 필수였다.