o2o-negosium-original/lps/config/config_models.py
민헌 495e16b951 feat(lps): IP 를 태우지 말고 한계 직전까지 쓰고 쉬게 — 네이버 예산 상향 + 휴식 개념
기존엔 네이버가 쿠팡 기준 예산(3회)을 그대로 썼다. 실측하니 체급이 다르다:
같은 KR IP 로 **12회 연속 검색까지 무차단**(IP 4개 전부 한계 미도달). 3회로 돌리면
불필요하게 4배 자주 회전해 KR 풀만 빨리 소모하고 회전마다 브라우저 재기동(~20s)이 붙는다.
→ [DecodoConfig].naver_ip_request_budget = 10 (실측 12 에 여유). 쿠팡은 3 유지.

그리고 선제 회전에 빠져 있던 조각을 채웠다 — **휴식(rest)**:
예산 도달로 놓은 포트를 곧바로 다른 워커가 집으면 그 IP 의 요청률이 도로 올라가
예산의 의미가 사라진다. release(rest_sec=...) 로 sticky 수명만큼 쉬게 한다.
차단으로 태우는 burn(30분)과는 별개 상태다:
  휴식  탄 게 아님 · 짧음 · 소진 시 가장 먼저 회수
  쿨다운 차단당함 · 김 · 휴식보다 나중에 회수
회전 종류(kind)를 browser_base → DecodoProxy.rotate(kind) 로 전달해 budget 일 때만 휴식을 건다.

라이브 검증(예산 3으로 낮춰 관찰): 6회 검색 = IP 2개만 사용, 3회마다 선제 회전,
놓은 포트는 휴식 1 · 쿨다운 0 · 차단 0. 즉 IP 를 태우지 않고 로테이션만으로 돌아간다.

테스트 6건 추가(휴식 재사용 금지·만료 복귀·burn 우선·회수 우선순위·budget vs block),
전체 202 passed. _MockProxy.rotate 가 kind 를 받도록 갱신.
2026-08-05 11:16:37 +09:00

150 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from pydantic import BaseModel
from config.config_loader import ConfigModel
class WebServerConfig(ConfigModel):
server_name: str = ""
port: int = 0
process_count: int = 1
is_ssl: bool = False
is_test: bool = False
# CORS 허용 오리진(프론트). 비우면 CORS 미적용. 예: ["http://localhost:5173"]
cors_origins: list[str] = []
# API guard 키. 비우면 개방 모드(개발). 채우면 /v1 전체에 X-API-Key 검증(prod).
# 여러 개 등록 가능 — 무중단 키 교체(새 키 추가 → 호출자 전환 → 옛 키 제거).
api_keys: list[str] = []
class LogConfig(ConfigModel):
print_console: bool = True
log_level: str = "debug"
# DB Read/Write 분리 설정.
# 하나의 논리 DB 에 대해 write(주) / read(복제) 접속 정보를 각각 가진다.
class MainDBConfig(ConfigModel):
db_type: str = "postgresql"
name: str = ""
write_host: str = ""
write_port: int = 5432
write_id: str = ""
write_pw: str = ""
read_host: str = ""
read_port: int = 5432
read_id: str = ""
read_pw: str = ""
show_log: bool = False
# 커넥션 풀 사이징. 실제 동시 커넥션 = (pool_size + max_overflow) x 엔진수(R/W=2) x process_count.
pool_size: int = 10
max_overflow: int = 20
# 커넥션 예산(자동 산정). >0 이면 process_count 에 맞춰 pool_size/max_overflow 를 자동 계산한다:
# (pool+overflow)x2xprocess_count ≤ connection_budget 가 되도록. (이때 위 pool_size/max_overflow 는 무시)
# PG max_connections·공유 DB 동거 서비스(worker·negosium 등)를 고려한 'lps API 가 쓸 총 커넥션 상한'.
# 0 이면 자동 산정 끔(위 pool_size/max_overflow 그대로 사용). env DB_CONNECTION_BUDGET 로 override.
connection_budget: int = 40
# SSL/TLS 모드: ""/"disable"=미사용(로컬), "require"/"verify-ca"/"verify-full"=관리형 DB(RDS/Aurora/Azure).
sslmode: str = ""
# ── 시크릿(API 키 등)도 TOML 로 통합 관리. config.local.toml 은 미커밋(*.toml). ──
# 배포는 이 파일을 마운트하거나(권장), 환경별로 바뀌는 값만 env override 한다(DB_HOST 등).
class NaverKey(BaseModel):
id: str = ""
secret: str = ""
class NaverConfig(ConfigModel):
"""네이버 쇼핑 오픈API 키. 여러 개면 429/403 로테이션에 자동 포함.
⚠️ 2026-07-31 네이버가 검색 오픈API 중 쇼핑·책·전문자료를 종료했다(유예·대체 없음).
shop.json 은 정상 키로도 404 SE05 를 반환한다 → services/search/naver 는 사실상 사문화.
후속인 NCP NAVER API HUB 에도 쇼핑 '검색'은 없다(→ NaverApiHubConfig 는 인사이트/트렌드용).
"""
keys: list[NaverKey] = []
class NaverApiHubConfig(ConfigModel):
"""NCP NAVER API HUB(검색·검색어 트렌드·쇼핑 인사이트). 개발자센터 오픈API 의 후속.
옛 오픈API 와 인증 방식이 다르다 — X-Naver-Client-Id/Secret 이 아니라
X-NCP-APIGW-API-KEY-ID / X-NCP-APIGW-API-KEY 헤더를 쓴다(발급처도 NCP 콘솔).
client_id/secret 이 다 차야 활성(enabled).
"""
base_url: str = "https://naverapihub.apigw.ntruss.com"
client_id: str = ""
client_secret: str = ""
timeout_sec: float = 10.0
@property
def enabled(self) -> bool:
return bool(self.base_url and self.client_id and self.client_secret)
class OpenAIConfig(ConfigModel):
"""AI 유사도 판정/검색어 생성용 OpenAI."""
api_key: str = ""
model: str = "gpt-4o-mini"
class DecodoConfig(ConfigModel):
"""DECODO residential 프록시(쿠팡 전용, 포트기반 sticky). 값이 다 차야 활성."""
host: str = ""
# 한국 residential 타깃 게이트웨이(kr.decodo.com). 네이버는 해외 IP 를 즉시 하드차단하므로
# 국내 사이트 전용으로 쓴다(실측 2026-08-04: 해외 IP=2.6KB 차단페이지, KR IP=정상).
# 비우면 host 를 그대로 쓴다(=국가 무지정 월드와이드).
kr_host: str = ""
username: str = ""
password: str = ""
port_start: int = 0
port_end: int = 0
session_minutes: int = 10
cost_per_gb: float = 0.0 # DECODO residential 요금($/GB) — 검색 원가의 대역폭 비용 산정용(플랜에 맞게 설정)
# IP(포트 세션)당 요청 예산 — 도달 시 차단당하기 전에 선제 회전(평판 보존). 0=비활성.
# 쿠팡(Akamai) 실측상 5회 부근 차단 이력 → 보수적 3. 튜닝은 ip_session 분석(docs/database.md).
ip_request_budget: int = 3
# 네이버는 같은 KR IP 로 **12회 연속 검색까지 무차단**(2026-08-05 실측, IP 4개 전부 한계 미도달).
# 쿠팡 기준 3을 그대로 쓰면 불필요하게 4배 자주 회전해 KR 풀만 빨리 소모하고 지연도 늘어난다
# (회전 1회 = 브라우저 재기동 ~20s). 실측 12에 여유를 둔 10. 0=비활성(시간창 회전만).
naver_ip_request_budget: int = 10
# 차단 감지된 포트 격리 시간(초). 0=자동(max(sticky, 30분)) — sticky 만료 후 복귀라 사실상 새 IP.
port_cooldown_sec: int = 0
# 선제 회전(예산 도달)으로 놓은 포트의 휴식 시간(초). 0=자동(sticky 수명).
# 차단이 아니라 '쉬게 하는' 것이라 쿨다운보다 짧다 — 그래도 곧바로 재사용되면 안 된다.
port_rest_sec: int = 0
class WorkerConfig(ConfigModel):
"""워커 런타임 설정. (동시성만 실행 시 WORKER_CONCURRENCY env 로 임시 override 가능 — 대화형 스크립트용)"""
concurrency: int = 1 # 상품 동시 검색 수(워커별 브라우저 세트, Chrome 최대 4×N). 로컬 권장 2~3
fallbacks: list[str] = [] # 오픈마켓 폴백(기본 비활성). 예: ["gmarket", "auction", "st11"] — 켜기 전 라이브 스모크
profile_dir: str = "/tmp" # Chrome 프로필 베이스 경로. 영속 볼륨이면 재시작에도 cf_clearance 유지(재웜업 회피)
job_deadline_sec: float = 300 # 잡 1건 처리 상한(크롤 행 방어). 0=무제한(테스트용)
shutdown_grace_sec: float = 60 # graceful 종료 유예 — docker stop_grace_period 를 이보다 길게
chrome_channel: str = "chrome" # 로컬: 실제 Chrome 채널
chrome_executable: str = "" # 컨테이너: 시스템 chromium 경로(설정 시 channel 무시, --no-sandbox 적용)
heartbeat_file: str = "/tmp/lps_worker_heartbeat" # 하트비트 파일(Docker HEALTHCHECK 신선도 확인)
class AlertConfig(ConfigModel):
"""임계 알림(AlertManager) 설정 — 룰 의미는 docs/operations.md 표 참고."""
webhook: str = "" # Slack 호환 웹훅 URL. 비우면 로그로만 알림
cooldown_min: int = 30 # 같은 룰 재발송 억제 시간(분). 해소 알림은 즉시
dead_1h: int = 20 # 최근 1h DEAD 잡 수 임계
blocks_1h: int = 80 # 최근 1h 봇 감지 수 임계
queue_lag_sec: int = 300 # 가장 오래된 PENDING 대기 초 임계
pool_pct: int = 90 # DB 커넥션 풀 포화율(%) 임계
source_fail_30m: int = 5 # 소스별 30분 내 시도 N회 이상 & 성공 0건
deadline_1h: int = 5 # 최근 1h 잡 데드라인 강제종료 수 임계
cost_1h_usd: float = 1.0 # 최근 1h 검색원가 합($) 임계
ports_low_pct: int = 30 # 가용 프록시 포트 비율(%) 임계
block_sessions_6h: int = 1 # 최근 6h '예산 회전에도 차단된' IP 세션 수 임계