[프로젝트 회고] 모두의 후기 사용자 리뷰 조회와 AI 요약 기능

들어가며

모두의 후기 프로젝트에서는 사용자가 리뷰를 작성하고, 다른 사용자의 리뷰를 탐색하는 흐름이 중요했다.

내가 맡았던 작업 중 하나는 특정 사용자가 작성한 리뷰를 조회하는 기능이었다. 처음에는 단순히 사용자의 리뷰 목록을 가져오면 된다고 생각했지만, 실제로 구현하다 보니 정렬, 페이징, 응답 구조, 총 개수, 프론트 화면 요구사항까지 함께 고려해야 했다.

또 하나 기억에 남는 기능은 AI 요약 기능이다. 장문의 리뷰를 사용자가 모두 읽기 전에 핵심 내용을 빠르게 파악할 수 있도록 Gemini API를 연동했다.

이번 글에서는 사용자 리뷰 조회 기능과 AI 요약 기능을 구현하며 고민했던 내용을 정리해보려고 한다.

사용자 리뷰 조회 기능

처음 구현한 API는 특정 사용자가 작성한 리뷰 목록을 조회하는 기능이었다.

초기 엔드포인트는 다음과 같은 형태였다.

1
GET /users/{memberEmail}

이후 API 의미를 더 명확히 하기 위해 다음처럼 수정했다.

1
GET /users/{memberEmail}/reviews

이렇게 바꾸니 URL만 봐도 특정 사용자의 리뷰 목록을 조회한다는 의미가 더 분명해졌다.

REST API를 설계할 때는 기능이 동작하는 것도 중요하지만, URL을 처음 보는 사람도 무엇을 하는 API인지 알 수 있어야 한다는 점을 다시 느꼈다.

커서 기반 페이지네이션

리뷰 목록은 데이터가 계속 늘어날 수 있는 기능이다.

그래서 단순한 전체 조회보다는 페이징이 필요했다. 이때 offset 방식보다 커서 기반 페이지네이션을 적용했다.

커서 기반 방식은 현재 페이지의 마지막 데이터 ID를 기준으로 다음 데이터를 가져온다.

1
GET /users/{memberEmail}/reviews?cursor=100&sort=recent

이 방식은 데이터가 많아졌을 때 offset이 커질수록 성능이 떨어지는 문제를 줄일 수 있다. 또한 무한 스크롤 같은 UI와도 잘 맞는다.

정렬 옵션은 다음처럼 나누었다.

  • recent: 최신순
  • hotbookmarks: 북마크 많은 순
  • hotcomments: 댓글 많은 순

처음에는 단순히 최신순만 있으면 충분하지 않을까 생각했지만, 리뷰 서비스에서는 사용자가 인기 있는 글이나 댓글 반응이 많은 글도 보고 싶을 수 있다. 그래서 정렬 옵션을 Repository 쿼리와 Service 로직에 반영했다.

응답 DTO 정리

작업을 하면서 응답 DTO도 여러 번 바뀌었다.

초기에는 UserReviewsResponse를 사용했고, 이후에는 SliceBoardResponse, 다시 UserReviewsResponseCustomSlicePageResponse를 정리하는 흐름이 있었다.

이 과정에서 느낀 점은 응답 DTO 이름과 구조가 생각보다 중요하다는 것이다.

DTO는 단순히 데이터를 담는 객체가 아니라, API가 프론트엔드와 맺는 약속이다.
이름이 모호하거나 구조가 자주 바뀌면 프론트엔드와의 협업 비용이 커진다.

그래서 최종적으로는 다음을 신경 썼다.

  • 응답 DTO의 의미가 API 목적과 맞는지
  • 커서 페이징 정보가 충분히 포함되는지
  • 총 게시글 수가 필요한 화면에 전달되는지
  • Search, MyPage, UserReviews 응답 구조가 지나치게 제각각이 되지 않는지

이런 정리는 기능 구현보다 덜 화려하지만, 협업에서는 훨씬 중요할 수 있다고 느꼈다.

AI 요약 기능을 넣은 이유

모두의 후기 서비스는 리뷰를 중심으로 한다.

그런데 리뷰가 길어지면 사용자는 글을 읽기 전에 부담을 느낄 수 있다.
특히 처음 보는 게시글이라면 “이 글을 읽을 만한가?”를 빠르게 판단하고 싶을 수 있다.

그래서 AI 요약 기능을 도입했다.

목표는 단순했다.

1
장문의 리뷰를 읽기 전에 핵심 내용을 빠르게 보여주자.

이를 위해 Google GenAI, Gemini API를 연동했고, 요약 관련 책임은 SummarizationService가 가지도록 분리했다.

SummarizationService 분리

AI 기능을 구현할 때 가장 조심하고 싶었던 부분은 비즈니스 로직과 AI 호출 로직이 뒤섞이는 것이었다.

리뷰 저장, 조회, 검색 같은 기존 기능 안에 Gemini 호출 코드가 직접 들어가면 나중에 모델을 바꾸거나 프롬프트를 수정할 때 변경 범위가 커진다.

그래서 요약 기능은 별도 서비스로 분리했다.

1
2
Review 관련 서비스: 리뷰 도메인 흐름 담당
SummarizationService: AI 요약 호출과 프롬프트 구성 담당

이렇게 나누면 AI 기능은 독립적으로 테스트하고 수정하기 쉬워진다.

또한 프롬프트 문구도 너무 감성적인 표현보다는 리뷰 내용을 정확하고 간결하게 요약하도록 정리했다. 실제 운영에서는 프롬프트 한 줄이 응답 품질에 영향을 줄 수 있기 때문에, 단순히 API를 호출하는 것보다 프롬프트를 어떻게 구성할지도 중요했다.

구현하며 느낀 점

사용자 리뷰 조회 기능은 API 설계와 페이징, 응답 DTO 정리를 배운 작업이었다.

AI 요약 기능은 외부 API를 서비스 안에 어떻게 녹일지 고민한 작업이었다.

두 기능은 겉으로 보기에는 다르지만 공통점이 있었다.

1
프론트엔드가 사용하기 좋은 형태로 데이터를 만들어야 한다.

사용자 리뷰 조회는 정렬과 페이징 정보를 잘 내려줘야 했고, AI 요약은 사용자가 긴 글을 읽기 전에 판단할 수 있는 정보를 제공해야 했다.

결국 백엔드 API는 DB 데이터를 그대로 내보내는 것이 아니라, 사용자가 화면에서 자연스럽게 사용할 수 있는 형태로 정리해주는 역할을 한다는 생각이 들었다.

마무리

이번 작업을 통해 커서 기반 페이징, DTO 설계, 외부 AI API 연동을 함께 경험할 수 있었다.

처음에는 기능을 만드는 것에 집중했지만, 시간이 지나면서 API의 의미, 응답 구조, 변경 가능성, 프론트엔드 사용성까지 함께 봐야 한다는 것을 배웠다.

앞으로 비슷한 기능을 만들 때는 “데이터를 어떻게 가져올까?”뿐 아니라 “이 데이터를 사용하는 쪽은 어떤 형태를 기대할까?”를 먼저 생각해보려고 한다.

Leave a comment