OpenAI API 스트리밍 응답 처리 시 메모리 누수 해결
문제 상황
사용자 대시보드에서 OpenAI API를 활용한 실시간 분석 기능을 운영 중이었다. 배포 후 며칠간 서버 메모리 사용량이 계속 증가하다가 결국 OOM으로 인스턴스가 재시작되는 문제가 반복됐다.
모니터링 결과 스트리밍 응답을 처리하는 엔드포인트에서 메모리가 해제되지 않고 있었다.
원인 분석
기존 코드는 다음과 같았다.
app.post('/api/analyze', async (req, res) => {
const stream = await openai.chat.completions.create({
model: 'gpt-4o',
messages: req.body.messages,
stream: true
});
for await (const chunk of stream) {
res.write(JSON.stringify(chunk));
}
res.end();
});
클라이언트가 중간에 연결을 끊어도 서버 측 스트림은 계속 동작하고 있었다. AbortController를 사용하지 않아 OpenAI API 요청이 취소되지 않았고, 이벤트 리스너도 정리되지 않았다.
해결 방법
app.post('/api/analyze', async (req, res) => {
const abortController = new AbortController();
req.on('close', () => {
abortController.abort();
});
try {
const stream = await openai.chat.completions.create({
model: 'gpt-4o',
messages: req.body.messages,
stream: true
}, {
signal: abortController.signal
});
for await (const chunk of stream) {
if (abortController.signal.aborted) break;
res.write(JSON.stringify(chunk));
}
res.end();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request aborted by client');
} else {
throw error;
}
}
});
클라이언트 연결이 끊기면 즉시 OpenAI API 요청을 취소하도록 수정했다. 배포 후 메모리 사용량이 안정화됐고, 불필요한 API 비용도 절감됐다.
교훈
스트리밍 API를 다룰 때는 항상 클린업 로직을 함께 구현해야 한다. 특히 외부 API 호출이 포함된 경우 비용과 직결되므로 더욱 주의가 필요하다.