Post

(Sparta_WIL_04) 스프링 AI

(Sparta_WIL_04) 스프링 AI

1. Log

1. New Keyword

  • Agentic -> LLM을 사용하는 방법론
  • VectorDB -> 벡터 디비 데이터 저장!

2. 문제의 기록

  • Agentic / tocken
    • 계속 고민이 되는 부분이고 생각해야하는 부분
    • “토큰을 적게 사용해야 경제적이고 효과적”이다라는 말이 너무 어려움
    • 같은 상황에서 한번의 질문으로 답이 나오면 좋지만,
      • LLM이 그렇게 쉽게 답을 주지 않기 때문에 반복적으로 질문을 해야하는데.
      • 사용자 입장에서는 똑같은걸 여러번 요구하기가 애매한 상황이고
      • 그렇다면 처음부터 단계를 거쳐서 명확한 답변을 얻어내는 것이 현명.
      • 하지만 한번에 답을내는게 훨씬 좋기는 하다.
      • 어렵다!!!!!!!
  • 옵셔널 / 람다 관련해서 쪼금더 메모가 필요할듯.
    • 프로젝트 진행하면서 별도의 해석이나 검색없이 읽는 것까지는 감을 잡은것 같은데,
    • 직접 사용하려니 막막함.
    • 이 부분에 대한 기본기가 부족한듯!
  • VectorDB
    • 간단한 것 같으면서도 복잡한 듯함.
    • 테이블에 벡터화시켜서 데이터 넣는 것과 메타데이터를 넣는 것이 거의 전부
    • 조회할때 유사도 판단은 0으로 조회해서 그 사이가 어디인지 판단하는게 현명할듯
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      
      -> Q) 자전거 용품 조회할때
      -> VectorDB 결과 값
      가민 엣지 530 / 자전거 카테고리        distance=0.52  →  similarity=0.48
      자이언트 탈론 / 자전거 카테고리         distance=0.53  →  similarity=0.47
      삼천리 로드바이크 / 자전거 카테고리      distance=0.54  →  similarity=0.46
      신지케이트 자물쇠 / 자전거 카테고리      distance=0.56  →  similarity=0.44
      카스크 헬멧 / 자전거 카테고리           distance=0.62  →  similarity=0.38
        
                 >>> 이 사이가 자전거 조회의 기준점 0.37 ~ 0.23 <<<   
      
      가민 인스팅트2 / 등산 카테고리          distance=0.76  →  similarity=0.24
      헬리녹스 체어원 / 등산 카테고리         distance=0.80  →  similarity=0.20
      살로몬 X울트라4 / 등산 카테고리 등산화   distance=0.81   →  similarity=0.19
      

3. 4주차 피드백

전체 파이프라인을 끝까지 연결하려는 시도가 좋아 보였고, 특히 RDB 조회와 벡터 검색을 하나의 사용자 경험으로 묶어낸 점이 인상적입니다. 다만 지금 단계에서는 동작 구조는 갖췄지만 핵심 조건 추출의 정합성 문제가 있어서, 약간의 보완이 필요합니다.

📢 주요 피드백

  • [도메인 설계와 연관관계] ✔️: Product-Category, Product-Option 관계를 JPA로 자연스럽게 풀었고, cascade와 orphanRemoval까지 사용해 상품-옵션 생명주기를 잘 묶었습니다.
  • [상품 저장과 벡터 동기화] ✔️: 상품 등록 시 RDB 저장 후 임베딩용 content를 만들고 metadata와 함께 Vector Store에 넣는 흐름이 구현되어 있어 Storage 단계의 핵심 의도를 잘 반영했습니다.
  • [QueryDSL 기반 동적 조회] ✔️: 가격 범위와 카테고리 조건을 null 여부에 따라 유연하게 조합하고 Page로 반환한 점이 요구사항과 잘 맞습니다.
  • [RAG 검색 파이프라인 구성] ✔️: 벡터 검색 상위 50개 후보 추출, 후보 정보를 바탕으로 검색 조건 생성, 이후 RDB 정밀 조회로 이어지는 흐름이 분리되어 있어 구조 이해가 좋습니다.
  • [LLM 조건 추출과 DTO 정합성] ❌ : 프롬프트에서는 keyword를 요구하는데 실제 조회 DTO는 productName을 사용하고 있어, 모델이 프롬프트를 그대로 따르면 상품명 필터가 비어 버릴 가능성이 큽니다.
  • [추천 상품 선정의 근거성] ❌ : 현재 추천 상품은 조회 결과의 첫 번째 상품을 그대로 사용하고 있어, 사용자의 의도인 ‘가성비’, ‘추천 이유’를 반영한 선정 로직이라고 보기에는 근거가 약합니다.
  • [API 계약과 응답 일관성] ❌ : /search/rag는 Pageable 기본값을 그대로 받아 size 기본값이 10이 아닌 20으로 동작할 수 있고, ok와 okWithResult가 혼용되어 응답 형식도 일관되지 않습니다.

👨‍🏫 개선 포인트

  • [LLM 조건 추출] 개선: 지금 코드에서 가장 먼저 고쳐야 할 부분은 프롬프트와 DTO 필드명이 서로 다르다는 점입니다. ProductSearchCondition은 productName을 기대하는데 프롬프트는 keyword를 반환하도록 되어 있어, AI가 아무리 잘 답해도 QueryDSL 필터에 제대로 전달되지 않을 수 있습니다. RAG용 프롬프트에는 minPrice가 두 번 들어가 있고 maxPrice가 빠져 있어서 가격 상한도 안정적으로 추출되기 어렵습니다. 프롬프트의 JSON 키를 DTO와 완전히 같게 맞추고, ‘2만원대’, ‘3만 원 이하’ 같은 케이스를 요청-응답 예시로 고정해 두면 훨씬 안정적입니다.
  • [추천 상품 선정] 개선: 현재는 조회된 첫 상품을 추천 상품으로 삼기 때문에, 추천 문구는 그럴듯해도 왜 이 상품이 대표로 뽑혔는지 설명력이 약합니다. 요구사항의 핵심은 단순 검색 결과 반환이 아니라, 컨텍스트를 바탕으로 설득력 있는 추천을 만드는 것입니다. 지금 모델 범위에서는 평점이 없더라도 ‘가성비’가 들어오면 낮은 가격 우선, ‘프리미엄’이 들어오면 높은 가격 우선처럼 간단한 우선순위 규칙을 서비스 계층에 두고, 그 결과를 바탕으로 문구를 생성하면 훨씬 근거 있는 응답이 됩니다.
  • [벡터 동기화와 API 계약] 개선: 등록 시점의 벡터 저장은 잘 되어 있지만, 상품명·설명·카테고리가 수정되어도 벡터가 갱신되지 않아 검색 결과와 실제 상품 정보가 어긋날 수 있습니다. RAG에서는 이 작은 불일치가 곧 검색 품질 저하로 이어지기 때문에 수정 로직에도 재임베딩 또는 재동기화가 들어가는 편이 안전합니다. 또한 /search/rag에 @PageableDefault(size = 10)를 주고 ApiResponse 포맷을 한 가지로 통일하면, 요구사항 문서와 실제 API가 더 정확히 맞아떨어집니다.

🛎️ 추가 팁

  • 프롬프트를 설계할 때는 ‘자연어 문장’보다 ‘DTO 필드명과 1:1로 대응되는 JSON 계약’이라고 생각하면 훨씬 안정적입니다. ProductSearchCondition 기준으로 productName, categoryName, minPrice, maxPrice만 반환하도록 고정해 보세요.
  • QueryDSL은 지금처럼 잘 시작하셨고, 이후 조건이 더 늘어나면 BooleanBuilder와 BooleanExpression 메서드 분리를 함께 연습해 보시면 좋습니다. 이렇게 해 두면 검색 조건이 많아져도 서비스 코드는 훨씬 읽기 쉬워집니다.
  • JPA 연관관계는 현재 기본기가 좋습니다. 다음 단계에서는 @EntityGraph, FetchType, 그리고 조회 전용 DTO 프로젝션을 함께 비교해 보면서 ‘언제 엔티티를 그대로 읽고, 언제 DTO로 바로 조회할지’를 익히면 실무 감각이 더 빨리 올라옵니다.
This post is licensed under CC BY 4.0 by the author.