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주간 모니터링한 결과, 메모리 사용량이 안정적으로 유지되는 것을 확인했다. 스트리밍 처리 시 클라이언트 생명주기를 반드시 고려해야 한다는 교훈을 얻었다.