모듈·backend·문서 3개 관점의 적대 리뷰에서 확인된 결함 일괄 수정: [모듈] - 배치에 company_ids 스코프 옵션 추가 — 통합 테스트가 공유 dev DB 의 실세션을 소비/마킹하던 문제 해소(테스트는 시드 회사로 한정), 표적 수동 실행 옵션 겸용 - Redis 방어: compose 포트를 127.0.0.1 바인딩(무인증 공개 차단), get_rate 에 범위([10,200]) 검증 — 오염 캐시값은 미스 취급 후 자가 교정, 미스 백필은 SET NX (배치가 방금 쓴 새 값을 구값으로 덮는 write-after-read 경합 방지) - 배치: Redis ping 후 re-SET(다운 시 셀×timeout 지연 없이 즉시 스킵), 스캔 조인 ON 절에 quotations/items deleted 필터(철회 거래를 학습에서 배제), 제외 마킹을 청크별 커밋(레거시 대량 첫 실행의 장시간 단일 트랜잭션 방지) - main: SIGTERM/SIGINT 핸들러(docker stop 시 정리 로직 보장), --once 부분 실패 시 종료코드 1(런북/cron 감지 가능) [backend] - finalize_session·update_last_offered_price 에 status=IN_PROGRESS 가드 — negodata 일괄마감/중복 전송 경합이 종료된 세션을 되살리거나 가격 흔적을 사후 변경하는 것 차단(파생 판정 결정성 보호) - 신규 DB 부트스트랩: sessions 3컬럼을 postgres-init/01-schema·04-alter 에도 반영(backend 가 모듈 DDL 없이 기동) — anchoring 스키마 자체는 모듈 소유 유지 - 낡은 주석 정리(agent_client·quotation_settings 의 구 앵커 산출 서술) [테스트·문서] - 신규 테스트: 격주 게이트 골든(ISO 주차), supplier_type NULL, 가격 제시율 0% WARN — 모듈 18개·backend 57개 통과 - 문서 정합 감사 20건 반영: 잔존 33,334/노출 문구 제거, §10 SQL 을 실제 코드 (LEFT JOIN+deleted)와 일치, §11 자동/수동 검증 구분, FastAPI 오기 제거, 인수인계 reader 시그니처(db 인자), TODO 백로그 5건 기록 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|---|---|---|
| agent | ||
| backend | ||
| frontend | ||
| negodata | ||
| postgres-init | ||
| schedules/anchoring | ||
| .gitignore | ||
| docker-compose.yml | ||
| README.md | ||
O2O Negosium
동일 구조의 두 서비스(negosium, negodata)가 하나의 PostgreSQL 인스턴스를 공유한다.
구성
o2o-negosium/
├── docker-compose.yml # 두 backend (DB 는 외부)
├── postgres-init/ # DB·테이블 셋업 SQL (대상 DB 에 1회 적용)
├── backend/ # negosium 백엔드 (포트 9300)
├── negodata/backend/ # negodata 백엔드 (포트 9400)
├── agent/ front/ # (예정)
└── negodata/front/ # (예정)
두 백엔드는 같은 코드 골격(MVC · 람다 DB · Depends 주입 · JWT 로그인)을 쓴다. 아키텍처/패턴 상세는 각 서버 README 참고: backend · negodata/backend
DB 는 compose 밖 (config 로 연결)
DB 는 docker-compose 에서 관리하지 않는다. 각 backend 는 config.<APP_ENV>.toml 의 접속 정보대로
외부 PostgreSQL(호스트 로컬 postgres, 또는 따로 떠 있는 docker postgres)에 연결한다.
한 PostgreSQL 안에 서비스별 database 를 둔다.
PostgreSQL (외부, 5432)
├── negosium_db ← negosium-backend
└── negodata_db ← negodata-backend
- 컨테이너(docker env)에서 호스트 DB 접근:
host.docker.internal:5432(config.docker.toml) - 로컬 실행/테스트(local·test env):
127.0.0.1:5432(config.local/test.toml) - 계정/database 명은 config 에 맞춘다 (기본
postgres/password).
| 서비스 | 서버 | docs | database |
|---|---|---|---|
| negosium-backend | http://localhost:9300 | /docs | negosium_db |
| negodata-backend | http://localhost:9400 | /docs | negodata_db |
빠른 시작
# 1) DB 준비 (최초 1회) — 사용할 PostgreSQL 에 스키마 + 시드 적용
psql -h 127.0.0.1 -p 5432 -U postgres -f postgres-init/01-schema.sql # negosium_db + 도메인 schema
psql -h 127.0.0.1 -p 5432 -U postgres -f postgres-init/02-learning-schema.sql # agent learning 스키마
psql -h 127.0.0.1 -p 5432 -U postgres -f postgres-init/03-seed-negodata.sql # negodata 전용 시드
# 2) 백엔드 기동
docker compose up -d # 두 backend (DB 는 config 대로 외부 연결)
docker compose logs -f
docker compose down
테스트
# config.test.toml 의 PostgreSQL(기본 127.0.0.1:5432) 이 떠 있어야 한다
cd backend # 또는 negodata/backend
pip install pytest pytest-asyncio httpx
python -m pytest
- httpx
ASGITransport로 네트워크 없이 앱을 직접 호출하는 e2e (각 5개). DB_SESSION_MNG싱글톤의 커넥션 풀이 첫 이벤트 루프에 묶이므로, 모든 테스트가 단일 session 루프를 공유한다(pytest.ini).
성능 / 벤치마크
/login 은 bcrypt(CPU 바운드) 가 비용의 대부분이다. 초기에는 bcrypt 가 asyncio
이벤트 루프를 막아 아무 일도 안 하는 /healthz 조차 p99 3.3s 가 나왔다.
두 가지 최적화
- bcrypt 를
asyncio.to_thread로 오프로드 — 이벤트 루프 비차단. bcrypt 는 해싱 중 GIL 을 해제하므로 스레드들이 여러 코어에서 실제 병렬 실행된다. - 워커 수 증가 (
process_count1 → 4) — login 처리량을 코어만큼 확장.
Before / After (동일 부하: 100 users)
| 지표 | Before | After | 변화 |
|---|---|---|---|
/healthz median |
1700ms | 2ms | 850배 개선 |
/healthz p99 |
3300ms | 14ms | 235배 개선 |
/me p99 |
3100ms | 12ms | 258배 개선 |
/login median |
9900ms | 220ms | 45배 개선 |
/login p99 |
15000ms | 1400ms | 11배 개선 |
/login RPS |
5.5 | 19.3 | 3.5배 개선 |
| 전체 RPS | 17.6 | 75.2 | 4.3배 개선 |
최적화 후 Locust 차트 (100 users)
RPS 가 ~71 로 안정, p95 ~250ms(bcrypt), 실패 0%. median 은 초기 계정생성 버스트 후 바닥으로 떨어진다.
login 은 여전히 가장 느리다(bcrypt 의 의도된 비용). 핵심은 그게 서버 전체를 막지 않는다는 점. 더 높은 처리량은 워커/인스턴스 수평 확장이 정석이다(bcrypt cost 낮추기는 보안 트레이드오프). ⚠️ to_thread 가 이미 단일 워커에서 멀티코어 병렬화를 하므로, 워커를 코어 수만큼 늘리면서 to_thread 까지 쓰면
워커 x 스레드가 코어를 넘어 오버서브스크립션이 된다(워커는 코어의 절반 안팎).
부하 재현:
docker compose up -d
cd backend && pip install locust
python -m locust -f loadtest/locustfile.py --host http://localhost:9300 --headless -u 100 -r 10 -t 2m
# 부하 중 커넥션 모니터링: psql -h 127.0.0.1 -U postgres -c "SELECT count(*) FROM pg_stat_activity;"
기술 스택
FastAPI · SQLAlchemy(async) · asyncpg · PostgreSQL 16 · python-jose(JWT) · bcrypt · uvicorn · Docker Compose
