# 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](backend/README.md) · [negodata/backend](negodata/backend/README.md) ### DB 는 compose 밖 (config 로 연결) DB 는 docker-compose 에서 관리하지 않는다. 각 backend 는 `config..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 | ## 빠른 시작 ```bash # 1) DB 준비 (최초 1회) — 사용할 PostgreSQL 에 스키마 + 시드 적용 psql -h 127.0.0.1 -p 5432 -U postgres -f postgres-init/00-init.sql # 스키마 전체 (negosium_db + 도메인·learning·anchoring schema) psql -h 127.0.0.1 -p 5432 -U postgres -f postgres-init/temp-data.sql # 임시 데이터 시드 (admin / admin1234) # 2) 백엔드 기동 docker compose up -d # 두 backend (DB 는 config 대로 외부 연결) docker compose logs -f docker compose down ``` ## 테스트 ```bash # 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** 가 나왔다. **두 가지 최적화** 1. **bcrypt 를 `asyncio.to_thread` 로 오프로드** — 이벤트 루프 비차단. bcrypt 는 해싱 중 GIL 을 해제하므로 스레드들이 여러 코어에서 실제 병렬 실행된다. 2. **워커 수 증가** (`process_count` 1 → 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 은 초기 계정생성 버스트 후 바닥으로 떨어진다. ![Locust Benchmark](backend/tests/Benchmark.png) > login 은 여전히 가장 느리다(bcrypt 의 의도된 비용). 핵심은 그게 **서버 전체를 막지 않는다**는 점. > 더 높은 처리량은 워커/인스턴스 수평 확장이 정석이다(bcrypt cost 낮추기는 보안 트레이드오프). > ⚠️ to_thread 가 이미 단일 워커에서 멀티코어 병렬화를 하므로, 워커를 코어 수만큼 늘리면서 > to_thread 까지 쓰면 `워커 x 스레드` 가 코어를 넘어 오버서브스크립션이 된다(워커는 코어의 절반 안팎). 부하 재현: ```bash 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