별찌 - 완벽한 설계보다 1주 POC, 빠르게 만들며 짓는 중
목차
프로젝트 개요
별찌(Byeolchi) 는 한국형 AI 패션 쇼핑 어시스턴트예요. 사용자가 이미 보고 있는 상품을 기준으로, 다른 쇼핑몰의 가격과 중고 대안을 한 화면에서 같이 보여줘서 구매 결정을 더 쉽게 만들어 주는 게 목표예요. 더 많은 상품을 보여주는 서비스가 아니라, 더 적은 시간 안에 더 나은 선택을 하도록 돕는 쪽을 지향해요.
이름은 밤하늘을 가르며 떨어지는 별똥별을 뜻하는 순우리말 “별찌”에서 가져왔어요. 수많은 쇼핑 선택지 속에서 합리적인 답 하나를 별빛처럼 콕 짚어 준다는 의미를 담았어요.
- 기간: 2026.05 — 진행 중 (POC / MVP 단계)
- 형태: 백엔드 풀스택 2인 부트스트랩 스타트업, 비공개 저장소
- 내 역할: 2인 중 주 커밋 담당(현재 전체 커밋의 약 84%). Identity 도메인(소셜 로그인·자체 JWT·PIPA 동의·데이터 권리) 단독 오너 + 인프라(OCI·Ansible·CI/CD) + Discovery 검색 서브모듈 lead. 비교 매칭(catalog)은 동료가 lead고, 외부 연동·법규·성능 같은 Hard 결정은 둘이 공동으로 정해요.
- 수익 모델: 사용자에겐 완전 무료, 외부 쇼핑몰 구매 완료 시 발생하는 어필리에이트 수수료로만 수익. 자체 결제는 처리하지 않아요.
이 글은 완성 회고가 아니에요. 아직 만들고 있는 중이라, “여기까지 무엇을 어떻게 정했고, 무엇을 아직 안 정했는지”를 그대로 적은 현재진행형 기록이에요. 이제 막 첫 실측(POC smoke) 하나가 나왔고, 나머지는 아직 결정·설계 단계예요. 그래서 이 글의 중심은 측정값이 아니라 왜 이렇게 정했는가예요. 측정 안 한 건 “측정 안 했다”고 그대로 적었어요.
만들려는 모습 (MVP)
가장 먼저 검증할 핵심 흐름은 단순해요.
화이트리스트에 등록된 쇼핑몰 상품 페이지를 열면, 사이드바가 자동으로 떠서 다른 곳의 가격과 중고 대안을 비교해 보여준다.
- 확장 프로그램: Chrome only MVP (WXT + Vite). Safari·삼성 인터넷은 Phase 2 이후.
- 모바일 앱: React Native 0.81 + Expo SDK 54. MVP는 확장 설치를 안내하는 가이드 역할.
- 웹 대시보드: Next.js 16.
- 대상 쇼핑몰: 무신사·쿠팡·네이버·크림·당근 (신상품 비교 먼저, 중고는 MVP 검증 후 별도 POC).
시작 전에 정해둔 것들
별찌는 2인 팀이고, 초반 예산은 사실상 0원이에요. 그래서 기술을 고르기 전에 제약부터 못 박아 뒀어요. 이 제약이 거의 모든 결정의 기준이 됐어요.
- 인프라 비용은 초반에 0원으로 맞춘다. 매니지드 SaaS는 나중 문제로 미룬다.
- 2인이 운영할 수 있어야 한다. 학습 곡선이 큰 스택은 MVP 단계에서 안 들인다.
- 법적 안전이 기능보다 먼저다. 크롤링·개인정보·표시광고는 Phase 2 일이 아니라 Day 1 일이다.
- 복잡도는 데이터가 필요하다고 말할 때 들인다. Elasticsearch, 백그라운드 크롤링, 마이크로서비스는 전부 그때 가서.
- 단계마다 통과 기준을 두고, 못 넘으면 다음 단계로 안 간다.
마지막 줄이 별찌에서 제일 중요한 원칙이에요. 그리고 그 원칙 때문에, 한 번 갈아엎은 이야기부터 해야 해요.
1. 완벽한 설계보다 1주 POC — 빠르게 만들고 검증한다 (ADR)
별찌의 개발 방식은 처음부터 하나로 잡았어요. 완벽하게 설계한 다음 만드는 게 아니라, 빠르게 만들어 배포하고 POC로 검증한 뒤 키운다. 2인·0원 부트스트랩에선 잘 정리된 설계 문서보다 “돌아가는 1주짜리 검증”이 훨씬 값지거든요.
여기서 “빠르게”가 “대충 던진다”는 뜻은 아니에요. 앞선 발루노와 빌려조잉은 끝까지 출시해 사람이 쓰게 만들었고(빌려조잉은 삼성전자 우수상), 별찌는 지금 그 출시 전 — 핵심부터 POC로 검증하는 단계예요.
솔직히 초반엔 설계 욕심이 잠깐 앞서기도 했어요. 4일 만에 ADR 13건·ERD 17개·이슈 27개까지 문서가 불어났는데, 정작 “이 비교 매칭이 기술적으로 되긴 하나?”는 한 번도 확인 안 한 상태였거든요. 그래서 거기서 멈추고, “POC부터”로 방식을 또렷하게 못 박았어요. 결정은 이거였어요.
MVP를 바로 짓지 않는다. 먼저 1주짜리 POC 하나로 단 하나의 질문에만 답한다 — “확장 프로그램이 상품 1건의 가격 비교를 실제로 띄울 수 있는가?”
POC의 통과 기준도 미리 숫자로 박아 뒀어요.
| 항목 | 통과 기준 |
|---|---|
| 상품 매칭 정확도 | 80% 이상 |
| 비교 결과 응답 시간 | 3초 이내 |
| 봇 차단 | 핵심 사이트에서 차단되지 않을 것 |
- POC가 통과하면: 얼려 둔 ADR 13건과 백로그 23건을 그때 풀어서 MVP를 단계별로 짓는다.
- POC가 실패하면(매칭 80% 미달이거나 봇에 막히면): 3개월을 쏟기 전에 모델 자체를 다시 본다.
핵심은 순서예요. 검증되지 않은 핵심 가설 위에 설계를 쌓으면, 그 설계는 자산이 아니라 부채가 돼요. POC가 통과해야 그 위에 설계를 푸는 거고, 13건의 ADR은 버린 게 아니라 “POC 통과 후 활성화”로 미뤄 둔 거예요. 빠르게 만들어 검증하는 흐름을 지키려고, 일부러 설계를 뒤로 세운 거죠.
첫 POC smoke — 매칭 89%, 그리고 남은 11%의 모양
말로만 “POC부터”가 아니라, 비교 매칭 첫 smoke를 실제로 돌렸어요. (비교/catalog 구현은 동료가 lead고, 저는 Discovery 공동 결정과 인프라·실행 쪽이에요. 그래서 이 숫자는 제 단독 성과가 아니라 팀의 첫 측정값이에요.)
- 대상:
/api/compare, 무신사 랭킹 상품 100개 표본 (2026-06-01, 로컬localhost:8080) - 매칭 정의: 입력 상품의 브랜드·핵심 토큰이 비교 결과 상위와 의미 있게 겹치면 PASS, 안 겹치면 FAIL
- 결과: PASS 89 / PARTIAL 3 / FAIL 8 → 89%. POC 게이트(매칭 80%)는 일단 넘겼어요.
중요한 건 89%라는 숫자가 아니라 남은 11%의 모양이었어요. 실패를 유형으로 쪼개니 다음에 뭘 해야 하는지가 보였거든요.
| 실패 유형 | 건수 | 뜻 |
|---|---|---|
| category_noise | 6 | 상위 결과에 브랜드·제목 겹침이 없음 (엉뚱한 카테고리) |
| too_broad | 3 | 매칭은 됐지만 너무 느슨함 |
| no_result | 1 | 결과 0건 |
| wrong_accessory | 1 | 가방 같은 액세서리 토큰에 낮은 겹침으로 오매칭 |
category_noise가 실패의 절반이라, 다음 개선 후보는 “검색 엔진을 갈아엎자”가 아니라 카테고리·액세서리 감점 정책 조정과 브랜드 alias 분리로 좁혀졌어요. 게이트를 넘었다고 끝난 게 아니라, 이 실패 분포가 그대로 다음 작업 목록이 됐어요. (참고로 “응답 3초 이내” 게이트는 이 매칭 품질 smoke가 아니라 별도 부하 테스트에서 봐야 해요 — 7번에서 다뤄요.)
2. 데이터·검색 스택 — 지금은 PostgreSQL 18 + pgvector, Elasticsearch는 Phase 2로 (ADR)
별찌의 검색은 한국어가 핵심이에요. “베이지 정장” 같은 질의를 제대로 받으려면 형태소 분석(Nori)이 거의 필수예요. 그래서 처음엔 자연스럽게 “Elasticsearch + Nori + 벡터 검색을 본 스택으로 깔자”는 그림이 나왔어요.
여기서 다시 제약을 떠올렸어요. 2인이고, 예산은 0원이에요.
- Elasticsearch는 자체 호스팅하면 추가 비용을 $0까지 낮출 수 있지만(매니지드는 월 $95 수준), MVP 20명 단계에 클러스터 운영·학습 부담을 같이 데려와요 — 2인한테는 그게 더 비싼 비용이에요.
- 사용자 측 추천·가중치 검색은 사용자 데이터와 JOIN이 필요한데, 이건 관계형 DB가 훨씬 자연스러워요.
- 별도 검색 엔진을 두면 DB와의 이중 쓰기 동기화가 또 일이 돼요.
그래서 질문을 바꿨어요. “글로벌에서 제일 좋은 검색 엔진이 뭔가?”가 아니라, “20명 알파 단계에서 우리 둘이 운영 가능한 가장 단순한 검색이 무엇인가?” 로요.
답은 PostgreSQL 18 한 곳에 다 담는 거였어요.
| 단계 | 검색 스택 | 진입 조건 |
|---|---|---|
| Phase 1 (지금) | PostgreSQL 18 — FTS(tsvector) 키워드 + pgvector 0.8(HNSW) 벡터 | MVP 기본값 |
| Phase 2 (조건부) | Elasticsearch + Nori (BM25 + dense vector 하이브리드) | 알파 인터뷰에서 “검색 부정확” 지적이 30% 이상일 때 |
- PostgreSQL 18은 모든 PK를 UUID v7로 네이티브 지원하고, 트랜잭션·벡터를 한 DB에서 같이 처리해요.
- pgvector면 비교 API·추천·가중치 검색에 필요한 벡터를 따로 엔진 없이 굴릴 수 있어요.
- Pinecone은 평가 후 명시적으로 제외했어요. 한국어 형태소를 못 다루고, JOIN이 안 돼서 사용자 가중 추천에 안 맞고, 벤더 락인에 월 $70+까지 — 별찌한테 고유 가치가 없었어요.
여기서 한 가지는 정직하게 짚어야 해요. pgvector는 Nori를 대체하지 않아요. 둘은 다른 축이에요 — pgvector는 임베딩으로 의미를 보는 벡터 검색이고, “베이지 정장” 같은 키워드 매칭은 Postgres FTS(tsvector)가 맡아요. 그런데 PG FTS엔 Nori 같은 한국어 형태소 분석기가 없어서, 한국어 키워드 품질은 ES+Nori보다 분명히 약해요. 그래서 Phase 1 알파의 목적이 바로 “PG FTS만으로 한국어 검색이 버틸 만한가”를 직접 측정하는 거예요. 부족하다는 신호(부정확 지적 ≥30%)가 나오면, 그때 카탈로그 측에 ES+Nori를 도입하고요.
핵심은 “Elasticsearch를 안 쓴다”가 아니라 “ES가 필요한지를 Phase 1 데이터로 판정한다” 예요. 그 트리거를 ADR에 숫자로 박아 둬서, 나중에 감이 아니라 신호로 결정하게 했어요.
3. 쇼핑몰 여러 곳을 동시에 — Java 25 가상 스레드 + Spring Boot 4 (ADR)
별찌의 백엔드 워크로드는 한 줄로 요약돼요. “한 요청에 외부를 여러 번 두드린다.” 상품 하나를 비교하려면 쇼핑몰 여러 곳의 가격을 동시에 가져오고, 어필리에이트 API를 호출하고, LLM도 부르거든요. 전형적인 I/O 바운드 + 팬아웃이에요.
이게 정확히 가상 스레드(Virtual Threads) 가 빛나는 자리예요.
- 쇼핑몰 5곳을 동시에 호출할 때, 전통적인 스레드 풀이면 풀 크기 튜닝과 스레드 고갈을 늘 신경 써야 해요.
- 가상 스레드는 I/O 대기 중에 캐리어 스레드를 잡지 않아서, 4 OCPU 위에서도 수많은 동시 요청을 OS 스레드 고갈 없이 받아요.
- 거기에 Structured Concurrency(JEP 505) 로 “5곳을 병렬 호출하고 다 모이면 합친다”를 깔끔하게 표현하고, Scoped Values(JEP 506) 로 요청 컨텍스트를 안전하게 전달해요.
단, 가상 스레드가 만능은 아니에요. 진짜 천장은 스레드가 아니라 DB 커넥션 풀과 외부 사이트의 레이트 리밋이에요. 가상 스레드를 아무리 많이 띄워도 HikariCP가 20개면 DB 앞에서 줄을 서고, 쇼핑몰도 분당 호출 상한이 있어요. 그래서 정확히 말하면 “스레드 고민이 사라진다”가 아니라 “병목이 스레드에서 커넥션·외부 한도로 옮겨간다” 예요. 그 한도는 캐싱(1시간 가격 캐시)과 레이트 리밋(5번)으로 관리해요.
그래서 런타임은 Java 25 LTS, 프레임워크는 Spring Boot로 갔어요. 버전을 고를 때 한 번 더 따졌는데(ADR), Spring Boot 4.0 을 골랐어요.
- Boot 4는 Java 25를 1급으로 지원해서 가상 스레드 pinning 이슈가 정리돼 있어요.
- Boot 4 + Spring Modulith 2.0 + Spring Security 7이 같은 시점에 GA라, 버전 엇갈림 리스크가 없어요.
- Boot 3.5는 무료 지원이 2026-06-30에 끝나요. MVP 한창일 때 강제 업그레이드를 맞느니, 처음부터 4.0으로 시작해 이중 작업을 피했어요.
- 결정적으로 코드를 아직 안 썼어요. 마이그레이션 비용이 0이라, 최신으로 시작 안 할 이유가 없었어요.
Node.js는 Java 25급 동시성 이점이 없어서, Kotlin은 초기 채용 풀이 좁아서 후보에서 내렸어요.
4. 2인이 경계를 지키는 법 — Spring Modulith 모듈러 모놀리스 (ADR)
2인 팀에서 제일 무서운 건 트래픽이 아니라 코드가 서로 엉키는 거예요. 도메인끼리 내부 패키지를 직접 import하기 시작하면, 몇 주 만에 “어디를 고치면 어디가 터지는지 모르는” 상태가 돼요.
마이크로서비스로 처음부터 쪼개는 건 2인한테 운영 부담이 과해요. 그래서 중간을 골랐어요 — 모듈러 모놀리스.
- Spring Modulith의
@ApplicationModule로 도메인을 나누고, 모듈 간 내부 패키지 직접 참조를 컴파일·CI 단계에서 막아요(ApplicationModuleTestverify). - 경계는 지키되 배포는 하나라, 2인이 운영 가능한 단순함을 유지해요.
- 나중에 정말 쪼개야 하면, 모듈이 그대로 서비스 분리의 이음매가 돼요.
도메인은 셋으로 나눴어요(ADR).
| 도메인 | 소유 | 비고 |
|---|---|---|
| Identity | 1인 단독 | 인증·계정 |
| Discovery | 2인 공동 소유 | 비교·메타서치·자연어/이미지 검색·AI 라우팅·임베딩·추천 |
| Engagement | 1인 단독 | 알림·사용자 활동 |
Discovery가 유독 무거워요. 외부 API, 법적 이슈, 성능, 비용이 전부 여기 몰려 있거든요. 그래서 “1인 단독 리드”인 척하지 않고 아예 공동 소유로 못 박았어요. 난이도가 Hard인 작업(외부 연동·법규·성능)은 둘이 같이 결정(동기 허들 또는 RFC), Easy는 단독. 이렇게 “언제 같이 결정하고 언제 혼자 가는지”를 미리 그어 둬서 커뮤니케이션 비용을 줄였어요.
업계에서도 처음부터 마이크로서비스로 갔다가 다시 모놀리스로 합치는 사례가 늘고 있는데, 별찌는 2인 규모에서 그 왕복을 아예 건너뛰고 모듈러 모놀리스에서 시작하는 셈이에요.
5. “번 만큼만 크롤링한다” — 3-path 전략과 법적 안전 (ADR)
크롤링은 별찌에서 기술 문제이기 전에 법 문제예요. 비교 서비스의 선례를 보면, 파트너십 없이 전체 카탈로그를 긁는 순간 저작권·부정경쟁방지법 리스크가 확 올라가요. 그래서 데이터를 가져오는 경로를 셋으로 나누고, MVP에서는 가장 안전한 하나만 쓰기로 했어요.
| 경로 | 방식 | 저장 | 시점 |
|---|---|---|---|
| Path 1 | 공식 어필리에이트 데이터 피드 | 영구 저장 | Phase 2+ |
| Path 2 | 사용자가 상품을 볼 때 실시간 메타서치 | 1시간 캐시 후 폐기 | MVP (지금) |
| Path 3 | 인기 카테고리 한정 백그라운드 크롤 | 일 1회 배치 | Phase 2 이후 결정 |
MVP는 Path 2만 써요. 사용자가 직접 상품을 연 행동이 트리거라, 배경에서 카탈로그를 통째로 긁는 것보다 법적으로 훨씬 단순해요. 어필리에이트 API 키 관리도, 일배치 인프라도, 데이터 피드 스키마도 아직 필요 없고요. 그리고 알파 사용자 데이터가 “어디에 공식 연동이 진짜 필요한지” 알려준 다음에 Path 1·3을 짓기로 했어요.
사이트별로도 보수성을 다르게 뒀어요.
- 무신사: 공식 API가 없어서 MVP는 사이드카 비교만, 프로덕션 크롤 전 법적 검토 필수.
- 크림(중고): 폐쇄 플랫폼이라 MVP 검증 후 별도 POC.
- 당근(P2P): 개인 간 거래라 개인정보 보호를 최우선으로, 레이트 리밋을 분당 10회로 가장 빡빡하게.
데이터를 받을 때도 안전장치를 깔았어요.
- 가격·상품명·URL·이미지 URL 같은 사실 필드만 가져오고, HTML 전체는 복사하지 않아요.
- 이미지는 핫링크만, 파일을 내려받지 않아요(저작권 리스크 ↓).
- ContentExtractor 단계에서 카드 번호·주민번호·이메일·전화번호·실명 패턴을 자동 마스킹해요.
- 레이트 리밋은 Redis 토큰 버킷으로 도메인별 상한(이커머스 60/분, 중고 30/분, P2P 10/분)을 강제해요.
- CAPTCHA 우회·VPN 로테이션·헤더 위장은 하지 않아요. robots.txt를 100% 따라요.
핵심 원칙은 하나예요. “아직 못 번 데이터는 긁지 않는다.” 속도와 법적 안전을 둘 다 챙기는 길이라고 봤어요.
무엇을 DB에 저장하고, 무엇을 안 하나
“1시간 캐시 후 폐기”만 보면 “아무것도 안 남기나?” 싶은데, 그렇진 않아요. 별찌도 일부는 DB에 저장해요 — 다만 무엇을 저장하느냐의 경계가 법적 안전의 핵심이에요. 셋으로 갈라요.
| 구분 | 무엇 | 어떻게 |
|---|---|---|
| TTL 캐시 (DB에 두되 자동 삭제) | 가격(price_snapshots), 사용자가 본 상품 메타(products_cache) | 가격은 expires_at 1시간 + cron 삭제, 상품 메타는 30일 미사용 파기. 영구화를 DB 레벨에서 막아요 |
| 사용자 스코프 영구 (OK) | 위시리스트에 담은 상품의 가격 이력(price_history) | 사용자가 “추적해줘”라고 담은 것만. 위시 삭제 시 cascade |
| 금지 | 페이지 전체 HTML, 리뷰 전문, 이미지 파일, 로그인 영역, 대량 카탈로그 | 아예 안 가져오거나 안 저장 |
법적 위험은 “남의 데이터를 가져오는 것” 자체가 아니라 “사용자 행동과 무관하게, 대량으로, 영구히 쌓아 남의 카탈로그를 복제하는 것” 이에요(저작권법 §93 DB제작자 권리·부정경쟁방지법). 그래서 가격 같은 시점 정보는 DB에 두더라도 expires_at로 영구화를 막고, 영구 저장은 “사용자가 위시에 담아 추적을 요청한 것”처럼 사용자가 명시적으로 요청한 범위로만 한정했어요. 이미지는 끝까지 핫링크(URL 참조)고 파일은 안 받고요.
즉 “가져오면 다 저장”도, “절대 저장 안 함”도 아니에요. “사용자가 만진 만큼만, 시점 정보는 캐시로, 영구는 사용자 스코프로” 가 경계예요.
6. 인증 SaaS 대신 자체 OAuth (ADR)
여기는 제가 단독으로 맡은 Identity 도메인이라, 결정도 제가 직접 내렸어요. 인증은 외부 인증 SaaS를 쓰면 UX가 편하죠. 그런데 별찌가 필요한 건 소셜 로그인뿐이에요(카카오·구글, 이메일 가입 없음). SaaS가 자랑하는 비밀번호·MFA·이메일 검증 같은 기능은 거의 안 쓰는 거예요.
그래서 따져 보니 자체 구현이 합리적이었어요.
- 비용: 인증 SaaS 무료 티어는 보통 1만 MAU 안팎에서 끝나요. 별찌는 Phase 2에 그 지점을 지나서 월 $25+가 붙는데, Spring Security OAuth면 $0이고요.
- 개인정보: 해외 SaaS를 쓰면 동의서에 “개인정보 국외 이전(미국 등)” 항목이 생겨요. 자체 구현이면 그 줄이 없어져서 사용자 마찰이 줄어요.
- 표준: 카카오·구글 연동은 Spring Security에 잘 정리돼 있어서, SaaS만의 고유 가치가 없었어요.
그래서 Spring Security OAuth 2.0 클라이언트 + 자체 발급 JWT로 갔어요. Access 15분 / Refresh 7일에, refresh token 로테이션과 탈취 감지(family_id)를 위해 refresh_tokens 테이블을 따로 뒀어요. “유명한 도구”가 아니라 “우리 제약에 맞는 도구”를 고른 거예요.
이 회전·탈취 감지를 실제 코드로 어떻게 구현했는지, 그리고 표준(RFC 9700·Auth0)도 답을 안 주는 “정상 사용자가 동시에 갱신할 때 family가 오탐으로 꺼지는 레이스”를 어떻게 진단하고 푸는지는 훔친 refresh token은 두 번째 사용에서 들킨다 글에 따로 깊게 적었어요. 별찌에서 제가 단독으로 가장 깊이 판 부분이에요.
7. 인프라 0원에 맞추기 — OCI Always Free (ADR)
가상 스레드를 쓰려면 Java 25가 돌 곳이 필요한데, Vercel Functions 같은 서버리스와는 잘 안 맞아요. 그렇다고 AWS·GCP·Render로 가면 부트스트랩 흑자 전에 월 $20~$100이 먼저 나가요.
그래서 OCI(Oracle Cloud) Always Free 로 갔어요.
- Ampere A1 Flex(4 OCPU, 24GB RAM)를 사실상 $0으로 써요. Phase 1~2는 단일 호스트 Docker Compose로 버티고, 규모가 커지면 수직 확장 → 그래도 부족하면 OCI Kubernetes Engine으로 수평 확장하는 경로예요. “한 대로 몇 만 명”은 측정한 적 없는 가정이라, 어디서 깨지는지는 부하 테스트로 봐야 해요.
- 리전은 서울이라 한국 사용자 지연이 낮아요.
- 백엔드뿐 아니라 프론트도 Vercel을 빼고 OCI nginx 정적 호스팅 + Cloudflare CDN으로 통일했어요(ADR). 단일 벤더라 운영이 단순하고, egress 비용이 없어요.
모니터링은 Sentry(에러) + PostHog(제품 분석) + OCI Monitoring(기본 지표)으로, 객체 스토리지는 egress가 무료인 Cloudflare R2로 맞췄어요. 전부 “초반 0원” 제약에서 역산한 선택이에요.
예상 트래픽과 서버 가정 (검증 전 가정치)
“$0으로 ~10만 명까지 버틴다”는 말은 가정이지 측정값이 아니에요. 그래서 그 가정을 숫자로 적어 두고, POC·MVP에서 직접 부하 테스트로 깨 보려고 해요.
- 워크로드 성격: 비교 1건 = 쇼핑몰 여러 곳을 동시에 호출하는 팬아웃 + LLM 1회. CPU보다 외부 I/O 대기가 지배적이에요. 그래서 코어 수보다 동시 대기 처리량이 병목이고, 가상 스레드를 택한 이유와 맞물려요.
- 단계별 동시성 가정: MVP는 알파 20명이라 동시 비교 요청이 한 자릿수예요. Phase 2(1K~10K)에서도 “상품 페이지를 여는 순간”에만 요청이 트리거되니, 피크라도 단일 인스턴스가 받을 수 있는 수준으로 가정했어요. 1시간 가격 캐시(Redis)가 같은 상품 재조회를 LLM·외부 호출 없이 받아내서 실효 부하를 더 낮춰요.
- 서버 가정: OCI Ampere A1 Flex(4 OCPU, 24GB) 단일 인스턴스. 여기서 막히면 먼저 캐시 TTL·히트율을 손보고, 그 다음 수직 확장, 그래도 안 되면 그때 인스턴스를 늘려요. 처음부터 멀티 인스턴스로 가지 않아요.
이 가정들은 k6로 부하 테스트해서 검증할 예정이에요. POC에서 “비교 응답 3초 이내”가 부하 상태에서도 지켜지는지, 캐시 히트율이 가정대로 나오는지, 외부 호출이 가상 스레드로 정말 안 막히는지 — 숫자가 나오면 다음 글에서 가정과 실측을 나란히 적을게요. 지금은 “측정 전 가정”이라고 분명히 적어 두는 게 맞다고 봤어요.
8. AI 비용을 통제하는 전략 (AI_STRATEGY)
별찌는 AI를 쓰지만, AI 비용에 휘둘리면 안 되는 구조예요(사용자 무료, 수익은 어필리에이트뿐). 그래서 모델도 비용에서 역산했어요.
- 기본 모델: Google Gemini 2.5 Flash 무료 티어. 임베딩은 gemini-embedding-001(1536차원).
- 모델 라우팅: 작업 난이도로 모델을 가르는 ModelRouter를 둬요 — 단순 분류는 가장 싼 모델로, 복잡한 큐레이션만 상위 모델로. 무료 티어(Gemini)를 1순위로 쓰고, Claude Haiku·GPT-4.1 Nano·DeepSeek는 폴백·유료 구간 후보예요.
- 캐싱 2단: 자주 쓰는 시스템 프롬프트는 컨텍스트 캐싱으로, 같은 질의+필터는 Redis로 30분~1시간 캐시해서 LLM 호출 자체를 건너뛰어요.
- 비용 목표(아직 목표치, 실측 아님): 문서상 라우팅으로 평균 호출 비용 60%+ 절감, 프롬프트 캐싱으로 시스템 프롬프트 최대 90% 절감, 배치 가능 작업은 Batch API 50% 할인을 잡아 뒀어요. 실제 절감은 트래픽이 붙은 뒤
ai_cost_cents로 확인할 거예요. - 비용 추적:
search_history.ai_cost_cents에 호출당 비용을 기록해서, 과사용·어뷰징을 신호로 잡아요.
개인정보는 Gemini에 보내기 전에 자동 마스킹하고, 상품 메타데이터와 (동의 후) 검색 의도만 전달해요. 무료 티어는 학습 리스크가 있어서, 유료 전환 시 opt-out하는 걸 전제로 깔아 뒀어요.
아직 안 정한 것들 (정직하게)
만드는 중이라, 비워 둔 칸도 분명히 있어요.
- 실측이 이제 막 시작됐어요. 비교 매칭은 첫 smoke(89%) 하나가 나왔지만, 가상 스레드 동시성·3초 응답·PG FTS(+pgvector) 한국어 검색 품질은 아직 숫자가 없어요. 부하 테스트와 알파 데이터로 확인해야 진짜가 돼요.
- 중고(크림·당근) 는 MVP 핵심에서 빼고 별도 POC로 미뤘어요. 신상품 비교가 먼저 증명돼야 해요.
- 표시광고법 상 가격 시점 표기 형식은 MVP에서 확정 예정이에요.
- Path 1·3 크롤링, Elasticsearch, 추천 시스템, 마이크로서비스 분리는 전부 데이터가 필요하다고 말할 때 켜요.
MVP 통과 기준도 미리 박아 뒀어요 — 알파 20명 중 50% 이상이 일주일 뒤 재방문, 사이드바 클릭률 25% 이상, 어필리에이트 클릭 발생, “다시 쓰겠다” 60% 이상, P0 버그 0, 법규 위반 0. 이 칸들이 채워지면 다음 글은 회고가 될 거예요.
마무리
별찌를 만들면서 가장 많이 한 일은 코드가 아니라 “이건 지금 안 한다”를 정하는 일이었어요. Elasticsearch도, 백그라운드 크롤링도, 마이크로서비스도, 외부 인증 SaaS도 — 좋은 도구지만 지금 우리 제약(2인·0원·법 먼저)에는 과했어요.
2인 부트스트랩에서 배운 건, 복잡도는 자랑이 아니라 부채라는 거예요. 설계가 잠깐 앞서갔을 때 거기서 멈추고 “POC부터”로 다잡은 게, 지금까지 가장 잘한 판단이었다고 생각해요. 검증된 만큼만 짓고, 데이터가 말할 때 다음 칸을 켜는 것 — 그게 지금 별찌를 만드는 방식이에요.
결정은 코드보다 ADR로 먼저 합의하고(지금 17건), POC 게이트로 검증한 뒤 푸는 식이에요. AI를 많이 쓰는 개발일수록 이 “계획 먼저, 검증 먼저” 순서가 안전벨트가 되더라고요 — 코드를 빨리 뽑는 것보다, 무엇을 안 만들지 먼저 정하는 게 더 중요했어요.
첫 POC smoke로 매칭 80% 게이트는 넘겼으니, 다음 숫자는 거기서 이어져요. 남은 11%(category_noise)를 감점 정책으로 잡으면 매칭이 얼마나 오르는지, 부하를 걸어도 응답이 3초 안에 들어오는지, PG FTS만으로 한국어 검색이 버티는지, 그리고 알파 20명이 실제로 다시 오는지 — 가정에 박아 둔 칸들을 하나씩 실측으로 채워서 돌아올게요.
댓글
댓글 수정/삭제는 GitHub Discussions에서 가능합니다.