유성

AI와 Chaining으로 데이터 없는 검색 서비스 구현 본문

Architecture

AI와 Chaining으로 데이터 없는 검색 서비스 구현

백엔드 유성 2025. 12. 6. 18:02

신규 서비스에서의 검색 서비스

신규 서비스를 만들 때 가장 난감한 기능 중 하나가 검색(search)이다.
서비스는 보통 데이터를 기반으로 움직이는데, 신규 서비스는 애초에 데이터가 없다.

 

국가데이터포털이나 외부 API를 사용해도 데이터가 부족하거나, 정확하지 않거나, "껍데기 데이터"가 많다.

이런 상황에서 내가 선택한 해법은 AI + API + DB를 체이닝하는 방식이다.
데이터가 없어도 마치 모든 데이터를 알고 있는 서비스처럼 동작하게 만드는 구조다.

 

많은 신규 서비스의 경우 이러한 방식을 활용하는 것이 큰 도움이 될것이다.

 

데이터가 없다면?

보통 데이터가 없는 경우 유/무료 API 및 국가데이터처를 활용하게 된다.

그러나, 막상 사용해보면 형식은 제각각, 누락된 값은 많고 내 서비스에 최적화 하기까지 꽤 노동력이 소모된다.

(API 의 장애 여부도 나의 시스템에 영향을 준다)

 

다른 방법으로 AI를 활용하면 데이터를 충분히 가져올 수 있지만, 지연시간이 발생하고 빈번한 검색은 비용이 꽤 발생한다.

그래서 AI 단독으로는 답이 아니다.

 

대안: AI 와 API 그리고 DB 통합

그렇기에 DB를 메인으로 사용하되, API와 AI가 백업을 해주는 구조를 만들어야 사용자 입장에서 불편하지 않는 검색 기능이 작동하게 된다.

이를 활용하기 위해서 chain 방식이 가장 적합하다고 판단을 했고, 구조는 아래와 같다.

사용자 요청
→ DB 검색 (있으면 즉시 반환)
→ API 검색 (있으면 반환 + DB 적재)
→ AI 검색 (있으면 반환 + DB 적재)
→ 그래도 없으면 Outbox 로깅 후 관리자 검수
[DB Finder] → [API Finder] → [AI Finder]  
        |            |            |
      hit?         hit?         hit?
        ↓            ↓            ↓
     return       return       return

 

API/AI를 호출해도 결과는 다시 DB에 적재해 일명 '성장하는 데이터셋'을 만들게 된다.

(물론 데이터의 정확도는 계속해서 개선해야 할 문제가 되지만, 개별 수정을 진행한 적이 없을정도로 정확도가 높아서 놀랐다)

 

사실 로직 자체는 위와 같이 단순하다. 마치 Spring Security Filter Chain을 생각하면 정확히 동일하게 작동한다.

신규 API나 기능이 있으면 필터를 등록하기만 하면 된다.

 

기능 개선

이를 활용하면서 문제가 발생하는 것들이 있었다. 각 문제에 대한 개선 방법은 다음과 같다.

1. 부분 일치 검색의 문제

사용하는 조건은 다음과 같다.

  • 정확히 일치하는 문자열 검색
  • 단어가 포함되는 문자열 검색

정확히 일치하는 문자열 검색의 경우 위 체인 방식을 그대로 활용하면 된다.

 

그러나 단어가 포함되있는 문자열(like %슈프림 피자%) 의 경우 DB검색 시 "쉬림프 슈프림 피자" 가 검색이 되고 이 결과가 사용자에게 전달된다.

이는 정확히 일치하는 음식이 있음에도 불구하고 얼추 비슷한 데이터를 불러오는 것과 같다.

 

이를 막기위해 적절한 조건을 만들고 각 필터들의 interface에 등록했다.

필터 체인의 조건부 실행을 활용하는 것으로 했다.

 

2. 데이터 생성과 OutBox

API나 AI로 생성된 데이터는 재사용을 위해 DB에 저장한다.

저장은 메인 흐름과 분리하기 위해 비동기 이벤트로 발행했고, 잠재적 지연이나 실패가 사용자 요청에 영향을 주지 않도록 했다.

 

이또한 편의성을 위해 각 필터들의 interface에 등록하고 사용하면 된다.

fun doSave(); // 인터페이스
DbFinder => override fun doSave() : Boolean = false // 구현체
ApiFinder => override fun doSave() : Boolean = true
AiFinder => override fun doSave() : Boolean = true

 

 

AI까지 활용했음에도 데이터를 찾지 못한 경우 Outbox에 담아두고 사용자 검색어가 포함되도록 했다.

(추후 수기로 입력을 하거나 버리거나 하면 된다.)

 

3. 여러 단어 동시 검색

서비스에서 여러 단어를 동시에 검색하는 기능이 필요하여 기능을 추가하였다.

물론 List<검색어> 로 요청을 하고 Map<검색어, 검색 결과> 를 반환하도록 만들어야 하는것은 맞으나,

컴퓨터 가용량이 충분하고, AI의 응답이 커져 중간에 있을지 모를 끊김을 방지하기 위해 다음과 같이 만들었다.

 

5가지 음식 이름 검색
 -> (Thread-1) filter 실행
 -> (Thread-2) filter 실행
 -> (Thread-3) filter 실행
 -> (Thread-4) filter 실행
 -> (Thread-5) filter 실행
시퀀셜 적용 후 return

 

가용할 수 있는 컴퓨터 리소스가 있기에 일단을 형상을 유지할 계획이다.

 

4. 페이징 처리

이렇게 여려곳에서 데이터를 추출한 경우 limit를 적용할 수 있어도 다음 페이지를 가져오는데 꽤나 복잡한 부분이 있다.

 

이는 다음과 같이 처리가 가능하다.

사용자가 offset 0, limit 10 이라는 데이터를 요청했다고 하자.
1. DB에서 7개, API에서 2개, AI에서 1개 가져왔다면 offsetMeta = { name: "검색어", db:7, api:2, ai:1 } 생성
2. 이를 offsetId로 만들어 응답에 포함
3. 이후 요청에서 offsetId 기반으로 각 Finder의 offset을 분배

사용자는 offset, limit 가 아닌 offsetId, limit로 두번째 페이지를 검색하면 된다.

 

어떤 순서와 출처의 데이터든 안정적으로 다음 페이지를 제공할 수 있다.

 

offsetId는 서버, 쿠키, redis 어디든 들어갈 수 있으나, 장기적으로는 TTL을 포함하여 Redis에 저장하는 것이 좋겠다.

(기간/유지보수/편의성 에 따라서)

 

단, 개별적으로 db, api, ai를 httpResponse에 넣는것은 지양했다.

추후 데이터 검색 filter가 추가될 경우 Front도 같이 수정이 들어가야 하므로 더욱이 복잡해진다.

 

마무리하며

이 방식에서는 여러가지 장점이 있다.

API와 AI가 데이터 부족을 즉시 보완해주며, 비용 효율적이고, 추후 개인화 맞춤 데이터 검색도 사용 가능하다.

 

신규 서비스는 "데이터가 없다"는 핸디캡을 갖고 시작한다.
하지만 AI와 API를 적절히 조합해 스스로 성장하는 검색 시스템을 구축한다면
마치 이미 수십만 건의 데이터를 가진 서비스처럼 동작할 수 있다.

검색이라는 기능은 생각보다 사용자 경험에서 매우 크다.
이 체이닝 구조는 비용 효율성과 품질을 모두 잡을 수 있었고,
내 경험상 데이터 생성 능력과 정확도 모두 충분히 실용적이었다.