Neoself의 기술 블로그

ChatGPT와 네이버 책검색 api로 도서매칭 시스템 구현하기(설계 과정 1/4) 본문

개발지식 정리/Swift

ChatGPT와 네이버 책검색 api로 도서매칭 시스템 구현하기(설계 과정 1/4)

Neoself 2025. 1. 23. 21:55

ChatGPT api를 활용해 사용자가 제시한 질문에 적합한 책을 추천하는 로직을 개발하면서 발생한 기술 도전과제의 해결과정을 정리한 글입니다.

ChatGPT 반환값의 후가공 과정에 대한 이해도를 높히고자 하는 개발자분들께 도움이 되었으면 좋겠습니다.

 

0. 배경

구현이 필요한 핵심로직은 아래 2개로 정리할 수 있습니다.

  • 책 추천: AI를 활용해 사용자의 질문에 답이 될 수 있는 책을 추천하고, 앱 내 서재 탭에 추가할 수 있도록 하기
  • 책 추가: 사용자가 책표지를 촬영할 경우, OCR촬영된 책 앱 내 서재 탭에 추가하기

위 기능을 구현하기 위해선 결국 아래와 같은 기술적 고민으로 이어졌습니다.

1. 사진촬영을 통해 책을 인식하는 로직

2. ChatGPT로 책을 추천받는 로직

1번과 2번 로직의 경우, ChatGPT만을 사용하는 방향을 처음에 고안했었습니다.

ChatGPT의 이미지 모달 인식률이 최근들어 매우 높아져 큰 개발비용없이 사진을 통핸 책인식 로직 구현이 가능했으며, 프롬프트 설계를 통해 인식된 책에 대한 상세정보 요청 또한 가능했기 때문입니다. 이러한 이점은 개발기간이 촉박했던 상황에도 무시하지 못할 메리트였습니다.

 

하지만, 이의 경우, gpt api가 부정확한 정보를 제공할때, 대응이 어렵다는 문제점이 있었습니다. 아무리 프롬프트를 체계적으로 설계하여도, 요구한 JSON 형식에 맞춰 데이터를 반환하지 않는 경우가 발생할 수 있으며, 설령 형식을 맞춰 반환하더라도 존재하지 않는 책에 대한 결과값을 반환할 수 있기 때문입니다.

 


특히, 1번과 2번 로직을 통해 추가되는 책들을 모두 앱 내부 서재 탭에서 확인할 수 있게 하기 위해선 두 플로우의 최종 반환값 통일이 필요했기에, 이러한 변수는 치명적이였습니다.

*ChatGPT 이미지 모달 유지보수 비용이 비싸다는 점도 무시못했습니다...

이를 해결하기 위해 gpt api를 통해 반환받는 데이터를 바로 사용하는 대신, 책 검색 api의 query에 대입해 검증하고, api로부터 최종 책 데이터를 반환받는 로직을 구상하게 되었습니다.

 

위 방식은 아래와 같은 장점이 있습니다.

  • AI가 존재하지 않는 책을 추천할 경우 검색 api로부터 책 데이터를 받지 않게 됨에 따라 자체 필터링 가능
  • 검색 api가 제공하는 공통 책 데이터모델을 최종사용하기 때문에 반환값 타입 불일치 문제 해결

따라서, 위 방향에 맞춰 책 추천 및 책 추가 플로우를 아래와 같이 구체화해볼 수 있었습니다.

 

책 추가와 책 추천의 플로우를 비교해보았을때, 데이터를 추가하는 방식은 상이하지만, 결국 책 검색 api의 검색값에 대입할 수 있는 텍스트를 추출한 후, 책 검색 api로 검증해 api가 반환하는 데이터모델을 최종모델로 사용한다는 점에서 일부 로직들이 공유될 수 있다는 것을 확인할 수 있었습니다.

따라서 저희는, 두 로직을 하나의 서비스 레이어로 통합해, 검색 api를 활용해 검증 및 최종 데이터 추출 로직을 재사용 가능한 모듈로 설계하기로 초기방향을 수립하게 되었습니다.

 

1. 구현에 필요한 기술 사전 리서치

본격적으로 구현하기에 앞서, 책 검색 api를 비교분석해보았습니다.

크게 네이버 책 검색 api와 카카오 책 검색 api를 두고 분석해본 결과, 아래와 같은 차이점을 파악했습니다.

1. 작가가 여러명일 경우 카카오는 배열로 작가를 나누어 반환하는 반면, 네이버는 단일 string으로 합쳐 반환

2. 네이버 api가 반환하는 이미지 크기가 카카오 api보다 일반적으로 더 큼

 

최종적으로 네이버 책 검색 api를 선정하였는데요. 이유는 내 서재 탭에서 각 책의 이미지를 통해 리스트를 표시하게 되는만큼 이미지 크기가 UI 구성에 핵심이였으며, 여러명의 작가가 하나의 String으로 묶여 반환될 경우, String.split(separator:",")을 통해 배열형태로 쉽게 변환이 가능할 것이라 판단했기 때문입니다.

 

선정 이후, 바로 네이버 책 검색 api를 사용하는 책 검색 데모 앱을 제작해 동작여부를 테스트해보았는데요. 그 과정에서 흥미롭게도 Swift에 i가 누락된 상태로 기입해도 의도했던 제목의 책이 반환되는 것을 볼 수 있습니다.

 

2. 구현방향 정의: 고려해야할 엣지 케이스 리스트업 및 시스템 플로우 설계

구현이 번복되는 일을 최소화하기 위해, 알고리즘 방향을 설계하기에 앞서 고려해야하는 엣지케이스들을 리스트업해봤습니다.

제목 관련 엣지케이스
1. 일부 글자 누락 (ex. Swft vs Swift)
2. 띄어쓰기 불일치 (ex. 클린 코드 vs 클린코드)
3. 영문/한글 혼용 (ex. Real 마케팅 vs 리얼 마케팅)
4. 부제 포함여부 불일치 (ex. 클린 코드 vs 클린코드: 애자일 소프트웨어 장인 정신)
5. 개정판 표기 (ex. "리팩터링 2판" vs "리팩터링")
6. 시리즈물 표기 방식 (ex. "해리포터 1" vs "해리포터와 마법사의 돌")

저자 관련 엣지케이스
1. 이름 순서(First name, Last name) (ex. 마틴 파울러 vs 파울러 마틴)
2. 한글/ 영문 표기 혼용 (ex 로버트 마틴 vs Robert C. Martin)

 

따라서, 이러한 엣지케이스들을 고려해 아래와 같은 로직을 초기에 구상하게 되었습니다.

  1. 책 추천에 대한 질문을 GPT api 인자로 전달
  2. GPT api가 반환한 (제목, 저자, 출판사) 데이터 중 제목을 네이버 책 검색 api query에 전달 후, 상위 20개의 검색결과들 요청
  3. 20개의 책 검색 결과 데이터에 대해
    1. GPT가 반환한 제목 vs 결과 데이터 제목 유사도 측정
    2. GPT가 반환한 저자 vs 결과 데이터 저자 유사도 측정
    3. GPT가 반환한 출판사 vs 결과 데이터 출판사 유사도 측정
    4. 3개 유사도 점수들에 대해 각각 중요도에 따른 가중치 부여
    5. 가중치 부여된 3가지 유사도 점수들을 누적합산하여 총 유사도 점수 저장
  4. 총 유사도 점수가 가장 높은 검색 결과 데이터를 선정

편의를 위해 위에 서술한 로직을 하이브리드 도서 매칭 시스템으로 칭하겠습니다.

 

3. 테스트환경 구축

3.1. 평가 기준 정의 : ChatGPT api 기반 도서 추천 적합성 검증 로직

구현을 시작하기에 앞서, 시스템의 정확도를 측정할 수 있는 테스트 평가기준을 정의하고자 했습니다.

이부분에서 많은 고민이 있었는데요. GPT가 제시한 도서 정보를 네이버 책 검색 API와 매칭시켜 실제 존재하는 도서로 변환되었는지 여부를 Contains 메서드나 정확히 일치하는지 여부로 확인하기에는 고려해야할 변수가 너무 많았기에 매칭이 성공되어도 테스트 실패로 고려될 가능성이 존재했습니다. 때문에, XCTest 케이스 평가를 위한 ChatGPT 프롬프트 및 메서드를 구현해 Test 케이스의 평가에 사용하기로 했습니다.

func accurancyTester(question: String, title:String, detail: String) async -> Int {
    let prompt = """
        질문: \(question)
        도서 제목: \(title)
        """

     let system = """
          당신은 전문 북큐레이터입니다. 도서의 제목과 상세정보를 보고, 질문에 적합한 도서인지 여부를 0이나 1로 표현해주세요:
            
              1. 입/출력 형식
              입력: 
              - 질문 (문자열)
              - 도서 제목: (문자열)
              - 도서 상세정보: (문자열)
            
              출력: 0 또는 1
              0: 책이 질문의 맥락이나 의도와 전혀 관련이 없는 경우에만 해당
              1: 다음 중 하나라도 해당되는 경우
              - 책이 질문과 직접적으로 관련된 경우
              - 책이 질문의 근본적인 감정이나 니즈를 간접적으로라도 충족시킬 수 있는 경우
              - 책이 질문자의 상황이나 심리상태에 위로나 통찰을 줄 수 있는 경우
              - 최근 판매량과 같이 객관적 확인이 어려운 질문의 경우
        """

    let requestBody: [String: Any] = [
        "model": "gpt-4o-mini",
        "messages": [
            ["role": "system", "content": system],
            ["role": "user", "content": prompt]
        ],
        "temperature": 0.01,
        "max_tokens": 100
    ]
    ...
}

보다 단순한 Task를 담당하고 있기 때문에, 책 추천 시와는 달리 GPT-4.0-mini api를 사용하기로 했으며, 도서의 제목과 상세정보를 통해 초기 사용자가 입력한 질문에 부합한지 여부를 0과 1중 하나를 반환하는 것을 목표로 프롬프트를 설계하여 메서드를 구현했습니다.

3.2. 테스트 케이스 구성 데이터 리스트업

먼저 책 추가 플로우의 시작점인 사용자가 질문하는 시점부터 생각해보았습니다.

사용자가 책 추천 시 생각할 수 있는 질문들을 나열해보았습니다.

장르/주제 관련:
"심리학 입문서 추천해주세요""
경영/리더십 도서 중 베스트셀러는?"
"SF 소설 중 우주를 배경으로 한 작품 있나요?"

독자 수준:
"프로그래밍 초보자가 읽기 좋은 파이썬 책은?"
"고등학생이 이해하기 쉬운 철학책 추천해주세요"
"영어 중급자에게 적합한 원서 추천해주세요"

상황/목적:
"우울할 때 읽으면 좋은 책 알려주세요"
"면접 준비하는데 도움될 만한 책은?"
"주말에 하루 만에 읽을 수 있는 가벼운 책 추천해주세요"

 

GPT가 매칭 적합성을 판단하는 것을 고려해, 최근 한달간 가장 많이 팔린 책과 같이 GPT가 적합성을 판단하기 모호한 질문들은 제외하였습니다.

4. 기초 토대 구현: 테스트를 고려한  클래스 구조 설계

검색된 도서와 타겟 도서 간의 정확도를 위해, 다양한 유사도 계산 알고리즘을 구현하고 테스트를 통해 알고리즘들을 동적으로 주입해 정확도를 비교검증해보는 것이 필수라고 판단했습니다. 때문에 최적의 알고리즘 조합을 위해 Strategy 패턴을 도입했습니다.

protocol CalculationStrategy {
    func calculateSimilarity(_ source: String, _ target: String) -> Double
}

전체 시스템의 경우 클래스 타입으로 구현하게 되었는데요. 이유는 GPT 호출과 네이버 책 검색 api 호출과 같은 다수 비동기 작업들이 필수적인 시스템으로서, 유사도 측정을 위한 Strategy, 유사도에 대한 가중치 등 초기화 시점에 의존성 주입을 통해 설정된 데이터들이 계속 유지되고 관리되어야 했기 때문입니다.

class EnhancedBookSearchManager {
    ...
}

구현을 하기에 앞서, 유사도 측정 로직뿐만 아니라, 제목-저자-출판사 간의 가중치, 검색 api 호출 시 요청할 검색결과 개수가 매칭 정확도를 높히는 데에 핵심 수치가 될 것이라 생각했습니다. 하지만 LLM 모델과 네이버 책 검색 api의 동작방식을 완전히 이해하지 못하기에, 이러한 핵심 수치값들을 미리 정하고 구현하기 보단 이러한 수치값들을 변경해가며 가장 높은 정확도를 지니는 방향으로 최대한 모든 변수들을 테스트 결과를 통해 비교해보고 검증해볼 필요가 있었습니다.

 

때문에, 당장은 시간이 오래 걸리더라도 연산에 필요로하는 매개변수들을 최대한 외부에서 주입받도록 설계해 테스트를 선행한 후, 방향이 확정되면 의존성 주입을 줄이는 방향으로 클래스를 설계하기로 했습니다.

init(
    titleStrategy: CalculationStrategy, // 제목 유사도 계산에 사용될 알고리즘
    authorStrategy: CalculationStrategy, // 저자 유사도 계산...
    publisherStrategy: CalculationStrategy, // 출판사 유사도 계산...
    weights: [Double], // 3개 유사도 값들에 대한 가중치값들
    initialSearchCount: Int // 최초 책 검색 시 가져올 상위 책 데이터 개수
)

 

감사합니다.