o2o-negosium-original/lps
민헌 7b9d895ead fix(lps): 풀 고갈 시 남의 IP 를 빌려 쓰던 문제 — 임대 없으면 크롤하지 않는다
동시 다상품 검색 점검 중 발견. 워커 3개(소유자 6)가 포트 2개/게이트웨이를 두고 경합하는
상황을 실제 코드로 돌리니, 임대를 못 받은 워커가 **남이 쥔 포트를 그대로 집어 같은 IP 로
동시에 요청**했다:

  coupang-w1   사용=70002 임대=70002
  coupang-w2   사용=70002 임대=None    ← 같은 IP 를 둘이 사용

원인은 _port() 의 계산식 폴백이다. 장부 모드에서 acquire 가 None 을 줘도 시간창 계산으로
포트를 하나 골라 돌려줬다. 포트 장부가 존재하는 이유("워커 N개가 같은 IP 에 요청을 몰면 그 IP 가
빨리 탄다" — port_registry.py 도입 배경)를 정면으로 무너뜨리는 경로다. 게다가 하필 **풀이 마른
상태 = IP 가 가장 귀할 때** 발동해, 남은 IP 를 두 배 속도로 태우는 악순환을 만든다.

→ 장부 모드에선 임대한 포트만 쓴다(없으면 None). 못 받으면 AdapterError 로 실패하고 잡이
   백오프 후 재시도한다 — 그 사이 쿨다운이 풀린다. 풀 고갈 자체는 proxy_ports_low 가 이미 운다.
→ playwright_proxy() 도 임대가 없으면 예외. 여기서 None 을 돌려주면 **프록시 없이** 브라우저가
   떠 서버 공인 IP 로 크롤하게 되는데, 그 IP 가 타면 회전으로 복구할 수 없다.

동시성 점검 결과(포트 20개/게이트웨이, 워커 3개):
  정상 24건 동시     성공 24 · 포트 중복 보유 0
  풀 고갈           성공 4/6(2건은 정상적으로 실패) · **같은 IP 공유 0**
  전면 차단          소각이 어댑터당 2개에서 멈춤(게이트웨이당 6/20) · 브레이커 6/6 트립

테스트 3건 추가(고갈 시 None 반환·남의 포트 미사용 / 임대 없는 playwright_proxy 예외 /
검색이 깔끔히 실패), 전체 256 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 12:01:15 +09:00
..
common feat(lps): 배송 표기 원문 기록 — 순위는 상품가 유지, 배송은 근거로 남긴다 2026-08-05 14:40:00 +09:00
config fix(lps): sticky 만료 회전이 저트래픽에서 안 먹던 문제 + prod 템플릿에 kr_host 누락 2026-08-06 11:50:08 +09:00
crud feat(lps): 배송 표기 원문 기록 — 순위는 상품가 유지, 배송은 근거로 남긴다 2026-08-05 14:40:00 +09:00
docs fix(lps): IP 로테이션 안정성 검수 — 예산이 안 먹던 근본 원인 + 차단 시 풀 소각 차단 2026-08-05 17:09:01 +09:00
loadtest refactor(lps): 설정을 TOML 단일 소스로 통합 — env/.env 이중 관리 제거 2026-07-13 21:11:31 +09:00
migrations feat(lps): 배송 표기 원문 기록 — 순위는 상품가 유지, 배송은 근거로 남긴다 2026-08-05 14:40:00 +09:00
router [fix] lps: 네거티브 캐시 히트 시 이력 누락 수정 + force 재검색 지원 2026-07-28 11:55:23 +09:00
services fix(lps): 풀 고갈 시 남의 IP 를 빌려 쓰던 문제 — 임대 없으면 크롤하지 않는다 2026-08-06 12:01:15 +09:00
tests fix(lps): 풀 고갈 시 남의 IP 를 빌려 쓰던 문제 — 임대 없으면 크롤하지 않는다 2026-08-06 12:01:15 +09:00
worker feat(lps): 배송 표기 원문 기록 — 순위는 상품가 유지, 배송은 근거로 남긴다 2026-08-05 14:40:00 +09:00
.dockerignore fix(lps): Docker 이미지 무시크릿화 + 배포 견고화 — 리뷰 발견사항 일괄 적용 2026-07-10 11:53:47 +09:00
.gitignore feat(lps): 로컬 워커·부하테스트(GUI) 실행 스크립트 추가 (대화형) 2026-07-10 08:33:11 +09:00
conftest.py feat(lps): 인터넷 최저가 검색 솔루션 프레임워크 골격 추가 2026-07-08 15:18:27 +09:00
Dockerfile fix(lps): Docker 이미지 무시크릿화 + 배포 견고화 — 리뷰 발견사항 일괄 적용 2026-07-10 11:53:47 +09:00
Dockerfile.worker fix(lps): 워커 재시작 시 Xvfb 가 stale lock 으로 죽어 모든 크롤이 실패하던 문제 2026-08-04 16:30:38 +09:00
loadtest.py test(lps): 부하테스트 상품셋 100종 카탈로그 분리 — 실제 입력 분포 재현 2026-07-10 10:50:28 +09:00
pytest.ini feat(lps): 인터넷 최저가 검색 솔루션 프레임워크 골격 추가 2026-07-08 15:18:27 +09:00
README.md docs(lps): README 에 크롤·매칭·IP 로테이션 실제 방식 추가 + 낡은 서술 정정 2026-08-05 17:23:23 +09:00
requirements-api.txt fix(lps): Docker 이미지 무시크릿화 + 배포 견고화 — 리뷰 발견사항 일괄 적용 2026-07-10 11:53:47 +09:00
requirements.txt feat(lps): 실시간 관측 대시보드 추가 — 큐 추이·코어별 CPU·프로세스 그룹 사용률 2026-07-10 09:50:14 +09:00
run_docker.sh feat(lps): docker-compose 에 lps-admin 추가 + 환경설정 config.local.toml 단일화 2026-07-16 11:54:32 +09:00
run_loadtest_gui.sh feat(lps): 로컬 워커·부하테스트(GUI) 실행 스크립트 추가 (대화형) 2026-07-10 08:33:11 +09:00
run_local_server.sh refactor(lps): 설정을 TOML 단일 소스로 통합 — env/.env 이중 관리 제거 2026-07-13 21:11:31 +09:00
run_local_worker.sh refactor(lps): 설정을 TOML 단일 소스로 통합 — env/.env 이중 관리 제거 2026-07-13 21:11:31 +09:00
run_monitor.sh feat(lps): 실시간 관측 대시보드 추가 — 큐 추이·코어별 CPU·프로세스 그룹 사용률 2026-07-10 09:50:14 +09:00
web_main.py feat(lps): process_count 기반 커넥션 풀 자동 산정 — 멀티코어 풀 오버서브스크립션 방지 2026-07-09 23:59:05 +09:00
worker_main.py fix(lps): IP 로테이션 안정성 검수 — 예산이 안 먹던 근본 원인 + 차단 시 풀 소각 차단 2026-08-05 17:09:01 +09:00

LPS — 인터넷 최저가 검색 솔루션

상품 정보를 넣으면 네이버·쿠팡을 뒤져 "같은 상품"의 최저가를 찾아 돌려주고, 그 가격을 시간에 따라 기록해 그래프로 볼 수 있는 시스템입니다.


🧭 이게 뭔가요? (비개발자용 3줄 요약)

  1. "맥심 커피 (1박스, 160개입)" 같은 상품 정보를 보내면,
  2. 시스템이 네이버·쿠팡을 실제로 검색하고, AI가 "진짜 같은 상품"만 골라 최저가를 알려줍니다. (빨대·커버 같은 엉뚱한 액세서리는 걸러냅니다)
  3. 같은 상품을 여러 번 조회하면 가격 변화가 쌓여서, 네이버/쿠팡/최종 최저가를 그래프로 볼 수 있습니다.

왜 유용한가? 사람이 일일이 검색·비교하지 않아도, 필요한 상품만(조회할 때만) 자동으로 최저가를 찾고 가격 추이를 남깁니다.


⚙️ 어떻게 동작하나요? (워크플로우)

 [1] 검색 요청           [2] 대기줄(큐)         [3] 일꾼(워커)가 처리
  상품 정보 전송   ─────▶  순서대로 쌓임   ─────▶  네이버 + 쿠팡 동시 검색
 (POST /search)          (즉시 접수번호 반환)         │
                                                     ▼
                                        [4] 걸러내기 + AI 판정
                                        가격 이상치 제거 → "같은 상품"만 선별
                                                     │
                                                     ▼
                                        [4-1] 오픈마켓 폴백 (옵션 · 기본 꺼짐)
                                        네이버가 못 덮은 몰(G마켓·옥션·11번가)만 크롤
                                                     │
                                                     ▼
                                        [5] 최저가 확정 + 기록
                                        네이버/쿠팡/최종 + 몰별 최저가 저장 → 이력 적재
                                                     │
                                                     ▼
                                        [6] 완료(결과 + 검색 원가 조회 가능)

핵심 포인트

  • 즉시 응답 + 나중 처리: 요청하면 바로 "접수번호(job_id)"를 주고, 실제 검색은 뒤에서 진행됩니다. (검색은 몇 초~수십 초 걸림)
  • 못 찾으면 검색어를 바꿔 재시도: "맥심 커피"로 안 나오면 "맥심 모카골드 커피믹스"처럼 AI가 검색어를 다듬어 다시 시도하고, 그래도 없으면 "없음"으로 정리합니다. (무한 재시도 안 함)
  • 차단 대응: IP당 요청 예산(쿠팡 3회·네이버 10회)에 닿으면 차단당하기 전에 IP를 선제 교체하고(평판 보존 — 그 IP는 쉬었다가 복귀), 그래도 감지되면 그 포트를 쿨다운 격리 후 다른 IP로 재시도합니다. 회전해도 소용없는 차단(환경·설정 문제)은 따로 알아보고 IP를 태우지 않습니다. 시작 시 챌린지를 미리 풀어(웜업) 실 작업을 빠르게 합니다.
  • 원가 투명: 검색 1건이 쓴 AI 비용·프록시 대역폭·시간을 함께 기록합니다.

🔬 안을 열어보면 — 크롤 · 매칭 · IP 로테이션

1) 소스별 크롤링 방식

네이버·쿠팡 둘 다 실제 Chrome(patchright)으로 긁습니다. 막는 방식이 달라서 세부 전략은 정반대입니다.

네이버 쿠팡
경로 모바일 msearch.shopping.naver.com www.coupang.com/np/search
왜 이 경로? 쇼핑 검색 오픈API가 2026-07-31 영구 종료(404 SE05, 대체 없음). PC 경로는 405+캡차, 내부 API는 418 → 모바일만 열려 있음 애초에 공개 API 없음
막는 주체 WTM 캡차 Akamai Bot Manager JS 행동 챌린지
리소스 차단 ❌ 끈다 — 이미지만 막아도 즉시 캡차. '무엇을 막느냐'가 아니라 요청 가로채기(CDP Fetch) 자체가 탐지 신호다(검색당 ~3MB 감수) ✅ 이미지·미디어·폰트·CSS 차단 — 파싱·챌린지에 불필요해서 대역폭만 줄어든다
필수 조건 한국 IP(해외면 2.6KB 하드차단) + 브라우저 로케일 ko-KR·Asia/Seoul 프로필 재사용으로 챌린지 쿠키 유지(한 번만 풀면 됨)
결과 수집 무한스크롤 — 카드가 더 안 늘 때까지 바닥으로 내린다(횟수가 아니라 '안 늘어남'이 종료 조건, 상한 6회) → 상위 40건 listSize 파라미터로 한 번에 → 상위 40건

로케일 두 줄이 캡차를 가릅니다 — 같은 한국 IP·같은 브라우저에서 en-US면 캡차, ko-KR이면 정상이었습니다(실측). 스크롤도 횟수로 세면 안 됩니다: 프록시 지연이 있으면 아직 아무것도 안 그려진 화면을 스크롤하고 끝나 40건 나올 페이지에서 14건만 건집니다.

카드 셀렉터는 클래스명이 webpack 해시(product_price__O3ZGH)라 prefix 매칭으로만 잡습니다 — 해시가 바뀌어도 안 깨집니다. 한 페이지에 광고·슈퍼적립·브랜드블록 카드가 유기 검색결과와 섞여 나오므로 유기 결과만 골라냅니다.

2) "같은 상품" 판정

수집한 카드를 그대로 쓰면 최저가가 오염됩니다 — 빨대·커버 같은 액세서리가 끼거나, 60롤 가격을 30롤 최저가로 쓰는 수량 왜곡이 생깁니다. 4단계로 좁힙니다.

수집 N건 ──▶ ① 몰 필터 ──▶ ② 가격 밴드 ──▶ ③ 이상치 제거 ──▶ ④ AI 같은상품 판정 ──▶ 최저가
                            (기준가 대비)       (IQR)         (gpt-4o-mini)

①~③은 규칙이고, 판단은 ④가 합니다. AI에게 주는 기준은 무시할 차이와 불일치로 볼 차이로 갈라놓았습니다:

무시한다 (같은 상품) 불일치로 본다 (다른 상품)
판매자·스토어, 색상/향, 사은품, 배송 문구, 상품명 수식어('무형광'·'프리미엄') 종류(프라이팬 ≠ 볶음팬 ≠ 웍팬), 브랜드·모델, 용량·크기, 수량(30롤 1팩 ≠ 30롤 2팩), 액세서리·호환부품

예전엔 "포장 차이는 같은 상품"과 "규격이 다르면 불일치"가 같이 있어 모델이 어느 쪽으로도 답할 수 없었습니다. 최저가 관점에선 수량이 다르면 다른 상품입니다.

후보는 반드시 10건씩 쪼개서 묻습니다. 37건을 한 번에 넣으면 gpt-4o-mini가 전 항목에 같은 점수를 매기고 전부 불일치로 답합니다(temperature=0에서 3회 재현). 10건씩 나누면 같은 모델·같은 입력으로 12건이 매칭됐습니다. 배치는 병렬로 던지므로 지연은 1개분입니다.

매칭이 0건이면 검색어를 바꿔 최대 3라운드(원본 → 정밀 → 광역) 돌고, 그래도 없으면 '없음'으로 확정해 일정 시간 캐시합니다(무한 재시도 방지).

3) IP 프록시 로테이션

DECODO residential 프록시를 씁니다. 여기선 포트 1개 = sticky 세션 1개라, IP를 바꾼다 = 포트를 바꾼다 입니다.

gate.decodo.com:10001-10100   국가 무지정  →  쿠팡
kr.decodo.com  :10001-10100   한국 전용    →  네이버 (해외 IP면 하드차단)

같은 포트 번호라도 게이트웨이가 다르면 다른 IP입니다(실측: port 10061 → gate=인도네시아 / kr=한국). 그래서 장부의 키는 (게이트웨이, 포트)입니다.

누가 어떤 IP를 쓰는지는 DB(proxy_port)가 관리합니다 — 워커 프로세스가 여러 개여도 한 계정을 나눠 쓰기 때문입니다. 잡 큐와 같은 방식으로 FOR UPDATE SKIP LOCKED를 써서 후보 선택과 임대를 한 문장에서 끝냅니다(두 프로세스가 같은 IP를 동시에 잡을 수 없음). 배정은 가장 오래 안 쓴 IP(LRU) 순이라 프로세스가 몇 개든 알아서 골고루 돕니다.

포트는 세 가지 상태로 묶입니다:

상태 언제 기간
임대 지금 누가 쓰는 중 sticky 수명(10분). 프로세스가 죽어도 만료로 자동 회수 — 별도 정리 프로세스 불필요
휴식 예산 도달로 선제 교체한 IP sticky 수명. 탄 게 아니라 쉬는 것(곧바로 재사용되면 예산의 의미가 없어짐)
쿨다운 차단이 확인된 IP max(sticky, 30분). 누가 태웠든 전역으로 적용

IP를 바꾸는 계기는 넷입니다:

계기 처리
요청 예산 도달 (쿠팡 3회 / 네이버 10회) 차단당하기 전에 선제 교체 — 평판 보존이 목적. 태우지 않고 휴식만 준다
차단 감지 그 포트를 쿨다운 격리하고 다른 IP로 인라인 재시도
프록시 전송오류 (407·터널 실패) 사이트가 아니라 포트가 죽은 것 → 교체 후 재시도
sticky 수명 만료 제공자 쪽 세션도 끝났으므로 임대를 놓아주고 새 IP를 받는다

예산은 브라우저가 아니라 IP를 기준으로 셉니다. 유휴 브라우저 정리(120초)는 브라우저만 닫고 같은 IP로 돌아오기 때문에, 브라우저 기준으로 세면 카운터가 매번 초기화돼 예산이 영영 발화하지 않습니다.

태우지 않는 경우가 두 가지 있습니다. 회전해도 소용없는데 태우면 원인은 그대로인 채 풀만 마르기 때문입니다.

  • 구조적 차단 — 해외 IP로 네이버에 접근한 경우처럼 IP를 바꿔도 결과가 같은 차단. 즉시 실패시키고 "설정을 고치라"고 알립니다.
  • 환경 차단(서킷브레이커) — 서로 다른 IP가 연속 3개 모두 첫 요청부터 막히면 IP로 설명되지 않습니다(평판 문제라면 몇 개는 통과하고, 과사용이라면 첫 요청이 아니라 뒤쪽에서 막힙니다). 소각을 멈추고 알린 뒤, 검색이 한 번 성공하면 자동으로 풀립니다.

이 판정이 없던 때는 전면 차단 상태에서 잡 16건이면 100포트가 전부 30분 쿨다운에 묶였습니다(웜업만으로 워커당 6포트). 지금은 판정 근거로 2개를 쓰고 멈춥니다.

또 확신이 없으면 태우지 않습니다. 결과 0건인데 알려진 차단 마커가 없으면 '페이지가 짧다'는 정황뿐이라, 진짜 검색결과 없음일 수 있습니다. 이럴 땐 IP 교체·재시도까지만 하고 30분 쿨다운은 걸지 않습니다.

더 깊은 내용은 아키텍처·운영 가이드를 보세요.


✨ 주요 기능

기능 설명
멀티 소스 검색 네이버 모바일 쇼핑(WTM 우회) + 쿠팡(Akamai 우회) 동시 크롤·병합. 오픈API는 2026-07-31 종료돼 둘 다 크롤
오픈마켓 폴백 크롤 네이버가 못 덮은 몰만 G마켓·옥션(Cloudflare Turnstile 우회)·11번가 크롤 → 몰별 가격. 기본 비활성([WorkerConfig].fallbacks, 배경)
AI 같은 상품 판정 "진짜 그 상품"만 선별 (액세서리·다른 규격·다른 수량 제외). 후보를 10건씩 쪼개 병렬 판정
검색어 자동 정제 0건이면 정밀/광역 검색어로 재시도
최저가 이력 그래프 조회 시점마다 네이버/쿠팡/최종 + 몰별(by_mall) 최저가를 시계열로 기록
검색 원가 계측 검색 1건의 AI 토큰·비용 + DECODO 대역폭(실측 CDP) + 시간을 집계
다중 상품 병렬 워커별 브라우저 세트로 여러 상품 동시 검색(WORKER_CONCURRENCY)
안정적 큐 처리 작업 유실 없이 순서대로, 실패 시 자동 재시도
프록시 IP 로테이션 포트 임대를 DB 장부(proxy_port)로 관리 — 프로세스가 여러 개여도 같은 IP 중복 사용 없음(LRU 배정). 예산 도달 시 차단 전 선제 교체 + 불탄 포트 쿨다운 + 전송오류 즉시 순환 + 시작 웜업. 예산 튜닝용 ip_session 관측 로그
회전 무효 차단 감지 구조적 차단(해외 IP 등)과 환경 차단 서킷브레이커(서로 다른 IP 3개가 연속 첫 요청부터 차단)를 구분해 포트를 태우지 않고 즉시 알림 — 풀 고갈 방지
임계 알림 큐·차단·DB풀·소스별 장기실패·비용·회전무효 차단 등 11룰 — 쿨다운(스팸 방지)·해소 알림, Slack 웹훅(룰 표)
API guard [WebServerConfig].api_keys 설정 시 /v1 전체 X-API-Key 검증(개발은 빈값=개방 모드)

🚀 빠른 시작

cd lps

# 1) 설정 파일 준비 (DB·API 키 등)
cp config/config.local.toml.example config/config.local.toml   # 값 채우기

# 2) API 서버 실행 (요청 접수)
./run_local_server.sh              # → http://localhost:9600/docs

# 3) 워커 실행 (실제 검색 수행) — 별도 터미널
./run_local_worker.sh              # 대화형: 동시성·프로필 선택 (또는 python worker_main.py)

# (선택) 부하 테스트 GUI — Locust 웹 UI(:8089)
./run_loadtest_gui.sh              # 브라우저에서 users/spawn 조절하며 RPS/지연 관측

간단 테스트:

curl -X POST localhost:9600/v1/lps/search -H 'Content-Type: application/json' \
  -d '{"data":[{"product_code":"T1","product_name":"맥심 커피","specification":"1박스, 160개입"}]}'

서버 실행 — Docker

# 리포 루트에서 전체 스택과 함께 (권장)
docker compose up -d                # negosium 스택 + lps-api·lps-worker·lps-admin 모두 기동

# lps 서브셋만 (대화형: 설정 검증·guard 키 안전장치 + admin 포함)
./run_docker.sh

환경 구분이 없습니다(도메인 backend·negodata·agent 와 동일) — 항상 config.local.toml. prod 서버도 그 서버의 config.local.toml 에 prod 값(시크릿·guard 키·스케일)을 채우고 그냥 docker compose up -d. DB 는 컨테이너에서 host.docker.internal(호스트 DB)로 접속하고, 관리형 DB 면 LPS_DB_HOST= 로 override 를 끄고 toml 의 호스트를 씁니다. 운영에서 API 를 외부에 열지 않으려면 export LPS_API_BIND=127.0.0.1(리버스프록시 뒤).

자세한 실행/설정은 운영 가이드 참고.


📚 문서

문서 대상 내용
아키텍처 개발자/기획자 구성요소·파이프라인·안티봇(Akamai/Turnstile)·비용계측·동시성
데이터베이스 개발자/기획자 테이블 6종 구조와 코드값(+by_mall·ip_session·proxy_port 장부)
API 사용법 연동 개발자 엔드포인트·요청/응답·metrics 예시
운영 가이드 운영자/개발자 실행·병렬·관측(readyz/ops/알림)·Docker 배포·문제 해결
크롤러 논의 팀 오픈마켓 크롤러 유지 여부(ROI) 의사결정 메모

📁 폴더 구조

lps/
├── web_main.py            # API 서버 진입점 (요청 접수)
├── worker_main.py         # 워커 진입점 (검색+웜업+유휴정리+ops모니터)
├── Dockerfile             # API 이미지(lean) · Dockerfile.worker  # 워커(Chromium+Xvfb)
├── run_local_server.sh    # 로컬 API 실행 (대화형)
├── run_local_worker.sh    # 로컬 워커 실행 (대화형: 동시성·프로필)
├── run_docker.sh          # lps 서브셋 Docker 실행 (대화형: 설정 검증·guard 안전장치 + admin 포함)
├── run_loadtest_gui.sh    # 부하 테스트 Locust 웹 UI(:8089) 실행 (대화형)
├── config/                # 설정·시크릿(config.local.toml 하나 — 미커밋. 컨테이너엔 마운트, env 는 DB 접속점만 override)
├── common/                # 공통(enums, DB 세션, 모델, 로거, alerts=임계 알림 관리자)
│   └── database/model/models.py   # DB 테이블 정의
├── loadtest.py            # 부하 테스트 (N개 상품 → 처리량·지연·비용 집계)
├── crud/                  # DB 접근 (job_crud, price_history, negative_cache, bot_detection, ip_session, port_lease=IP 임대 장부)
├── services/
│   ├── search/            # 소스 어댑터 (coupang, naver_shop=네이버 크롤, esm=G마켓·옥션, st11=11번가)
│   │   ├── browser_base.py    # patchright 공통(브라우저 수명·IP 세션·차단감지/서킷브레이커·CDP 바이트계측)
│   │   ├── proxy.py           # DECODO(포트=IP 임대·회전·쿨다운/휴식·프리플라이트)
│   │   ├── profile_slot.py    # Chrome 프로필 슬롯 배타 선점(프로세스 여러 개 대응)
│   │   └── card_parser.py     # 오픈마켓 공용 카드 파서
│   ├── pipeline/          # 필터·이상치·최저가 정렬(+몰별 분해)
│   ├── ai/                # AI 유사도 판정·검색어 생성 (OpenAI)
│   └── metrics.py         # 검색 원가 계측(AI/DECODO 비용·시간)
├── worker/                # 워커 루프·핸들러(폴백·데드라인)·알림(NOTIFY)
├── router/v1/lps/         # API 라우터
└── tests/                 # 테스트

포트

backend 9300 / negodata 9400 / agent 9500 과 겹치지 않게 LPS는 9600.