Tiny Star

프로젝트/뉴스 요약 (AI)

[뉴스 요약] AI 중복 요청 분기 처리 (Database 구성)

하얀지 2025. 6. 23. 15:36

뉴스 요약 결과 재사용을 위한 캐싱 설계

OpenAI API를 연동해 뉴스를 요약해주는 기능을 구현하다 보니, 같은 기사를 반복 요청하는 경우가 자주 발생했다. 처음엔 단순하게 매 요청마다 API를 호출했지만, 비용과 응답 속도 측면에서 비효율이 느껴졌다.

“같은 기사에 같은 프롬프트로 요청했다면, 이전에 요약했던 결과를 재사용할 수 없을까?” 이런 고민에서 시작된 캐싱 구조를 정리해본다.

 

문제 인식

OpenAI API는 호출 횟수에 따라 비용이 발생한다.
또, 뉴스 요약은 길이에 따라 평균 3초 이상의 응답 지연이 발생하는 경우도 많다.
같은 기사에 대해 여러 번 요약을 요청하는 구조는 비효율적이었고,
결국 캐싱을 고려하게 되었다.

 

 

설계 방향

요약 결과를 캐싱하려면, “어떤 기사”에 “어떤 프롬프트로” 요약했는지 구분할 수 있어야 한다.
또, 프롬프트가 바뀌면 같은 기사라도 요약 결과가 달라질 수 있기 때문에, 프롬프트 버전에 따라 관리할 수 있어야 했다.

그래서 다음과 같은 방식으로 설계를 진행했다:

  • 같은 기사에 대해 같은 프롬프트 요청이면, 이전 요약 결과를 반환
  • 프롬프트가 변경되면, 새롭게 요약하고 히스토리에 추가
  • 요약 이력 전체는 `news_summary_history`, 최신 결과만 캐싱한 테이블은 `news_summary`로 분리

 

테이블 구조 요약

테이블 용도
`site` 뉴스 출처 (네이버, 다음 등)를 구분
`news_summary_history` 프롬프트별 요약 결과 전체 이력 저장
`news_summary` 최신 프롬프트 결과만 저장 (빠른 조회용 캐시)

 

사이트마다 기사 URI 구조가 다를 수 있기 때문에, 사이트를 구분해서 관리하는 게 낫다고 판단했고, 기사는 `site_id + url_num`을 기준으로 구분했다. 이 조합을 `news_summary`의 PK로 사용했다.

 

주요 로직 정리

요약 캐시 조회

Optional<NewsSummaryMetaProjection> newsSummaryOptional = newsSummaryRepository.findSummaryIdById(siteId, urlNum);
if (newsSummaryOptional.isEmpty()) return null;
if (newsSummary.getPromptVersion().equals(gptClient.promptVersion)) {
    return newsSummary.getSummaryId();
}
return null;

 

  • 요약 결과가 존재하고, 프롬프트 버전도 현재와 일치하면 바로 사용
  • 없거나 버전이 다르면 → OpenAI API 호출 → 새 결과 저장

 

저장 및 업데이트

if (isNew) {
    // 존재하면 UPDATE
    // 없으면 INSERT
}

 

  • 결과는 항상 `news_summary_history`에 저장
  • 만약 최신 결과라면 `news_summary` 테이블도 함께 갱신

 

성능 효과

최초 조회 시에는 OpenAI API를 호출하므로 평균 3초 이상 소요된다.
하지만 결과가 저장된 이후에는 단순 DB 조회로 처리되기 때문에 캐싱된 경우 평균 응답 속도는 약 91ms 수준으로 감소했다.

 

마무리

처음에는 단순히 중복 호출을 줄이기 위한 목적이었지만, 프롬프트 버전까지 고려한 구조로 설계하고 나니 관리와 확장 측면에서도 유리했다. 향후 프롬프트 개선이나 사용자 커스터마이징 기능이 들어가더라도, 버전별 결과를 구분해서 저장할 수 있으니 데이터를 다시 생성하지 않아도 된다.

요약 결과를 단순히 캐싱하는 수준을 넘어, 요약의 진화 과정을 저장하고 관리할 수 있는 구조가 되었다는 점에서 꽤 만족스러웠다.

top