OpenAI API 응답 스트리밍 처리 시 메모리 누수 해결
문제 상황
챗봇 서비스에서 OpenAI API의 스트리밍 응답을 처리하는 Node.js 서버를 운영 중이었다. 배포 후 며칠이 지나자 서버의 메모리 사용량이 지속적으로 증가하다가 결국 OOM으로 재시작되는 현상이 반복됐다.
원인 분석
openai 라이브러리(v4.52.0)로 스트리밍 요청을 처리할 때, 클라이언트가 연결을 끊어도 서버 측 스트림이 제대로 정리되지 않고 있었다.
// 문제가 있던 코드
app.post('/chat', async (req, res) => {
const stream = await openai.chat.completions.create({
model: 'gpt-4',
messages: req.body.messages,
stream: true,
});
for await (const chunk of stream) {
res.write(JSON.stringify(chunk));
}
res.end();
});
클라이언트가 중간에 연결을 끊으면 for await 루프는 계속 실행되고, 스트림 객체가 메모리에 남아있었다.
해결 방법
req.on('close') 이벤트를 활용해 클라이언트 연결 종료 시 스트림을 명시적으로 중단하도록 수정했다.
app.post('/chat', async (req, res) => {
const stream = await openai.chat.completions.create({
model: 'gpt-4',
messages: req.body.messages,
stream: true,
});
const controller = new AbortController();
req.on('close', () => {
controller.abort();
});
try {
for await (const chunk of stream) {
if (controller.signal.aborted) break;
res.write(JSON.stringify(chunk));
}
res.end();
} catch (err) {
if (!controller.signal.aborted) {
console.error('Stream error:', err);
}
}
});
결과
수정 후 3주간 모니터링한 결과, 메모리 사용량이 안정적으로 유지되는 것을 확인했다. 스트리밍 처리 시 클라이언트 생명주기를 반드시 고려해야 한다는 교훈을 얻었다.