OpenAI API 스트리밍 응답 처리 시 메모리 누수 해결

문제 상황

사내 고객 지원 챗봇에 GPT-4 API를 연동했는데, 서버를 며칠 운영하니 메모리 사용량이 계속 증가했다. 재시작하면 정상화되지만 시간이 지나면 다시 올라가는 패턴이었다.

모니터링 결과 스트리밍 응답을 처리하는 엔드포인트에서 문제가 발생하고 있었다.

원인 분석

기존 코드는 다음과 같았다.

app.post('/api/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) {
    const content = chunk.choices[0]?.delta?.content;
    if (content) {
      res.write(`data: ${JSON.stringify({ content })}\n\n`);
    }
  }
  res.end();
});

클라이언트가 중간에 연결을 끊어도 서버는 계속 스트림을 읽고 있었다. OpenAI API 응답은 계속 수신되는데 처리할 곳이 없어 메모리에 쌓이는 상황이었다.

해결 방법

AbortController를 사용해 클라이언트 연결이 끊기면 OpenAI API 요청도 취소하도록 수정했다.

app.post('/api/chat', async (req, res) => {
  const abortController = new AbortController();

  req.on('close', () => {
    abortController.abort();
  });

  try {
    const stream = await openai.chat.completions.create({
      model: 'gpt-4',
      messages: req.body.messages,
      stream: true,
    }, {
      signal: abortController.signal
    });

    for await (const chunk of stream) {
      if (abortController.signal.aborted) break;
      
      const content = chunk.choices[0]?.delta?.content;
      if (content) {
        res.write(`data: ${JSON.stringify({ content })}\n\n`);
      }
    }
    res.end();
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Request aborted by client');
      return;
    }
    throw error;
  }
});

배포 후 메모리 사용량을 모니터링했고, 더 이상 증가하지 않는 것을 확인했다. 사용자가 페이지를 벗어나거나 새로고침해도 리소스가 정상적으로 정리되었다.

교훈

스트리밍 API를 사용할 때는 항상 연결 해제 시나리오를 고려해야 한다. 특히 장시간 연결이 유지되는 경우 클라이언트 상태 변화에 대한 처리가 필수적이다.