o2o-negosium-original/schedules/anchoring/docs/운영및유지보수.md
민헌 26f518ed6e refactor(db): postgres-init 2파일 체계로 통합 — 00-init.sql(스키마 전체) + temp-data.sql(시드)
- 00-init.sql: 구 01(도메인)+02(learning)+05(anchoring) 통합, 구 04(누적 ALTER)는 01에 기반영되어 폐기
- temp-data.sql: 구 03 시드 + 협상 카드 시드(일반 11장·와일드 5장, 멱등 가드, available=TRUE)
- card 테이블: script TEXT 전환 + tone·strategy_type 컬럼 추가, 카드 변수 9종 체계 문서화
- 참조 갱신: 루트 README·docker-compose 주석, schedules/anchoring conftest(00-init 적용)·README·문서
- anchoring 테스트 20건 통과, 신규 DB 초기화·멱등성 검증 완료

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 10:20:44 +09:00

260 lines
15 KiB
Markdown
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.

# 앵커링 서비스 — 운영 및 유지보수 가이드
> **대상 독자**: 이 프로젝트를 처음 보는 운영/개발 담당자. 이 문서 하나로 설치 → 실행 → 로그 확인 → 문제 해결까지 따라할 수 있게 쓰였습니다.
> **함께 볼 문서**: 무엇을 하는 시스템인지 → `기획용.md` / 흐름 그림 → `워크플로우.md` / 구현 규범 → `개발용.md` / 타 팀 적용 → `인수인계.md`
---
## 목차
1. [이 서비스는 무엇인가](#1-이-서비스는-무엇인가)
2. [구성 요소 한눈에](#2-구성-요소-한눈에)
3. [처음 설치하고 실행하기](#3-처음-설치하고-실행하기)
4. [정상 동작 확인 체크리스트](#4-정상-동작-확인-체크리스트)
5. [로그 읽는 법](#5-로그-읽는-법)
6. [자주 하는 운영 작업](#6-자주-하는-운영-작업)
7. [문제 해결 (트러블슈팅)](#7-문제-해결-트러블슈팅)
8. [DB로 이력 추적하기](#8-db로-이력-추적하기)
9. [절대 하면 안 되는 것](#9-절대-하면-안-되는-것)
10. [정기 점검 체크리스트](#10-정기-점검-체크리스트)
---
## 1. 이 서비스는 무엇인가
협상 시스템의 **앵커링 값**(협상 합의 기준선을 목표가에서 몇 % 아래에 둘지)을 **격주 토요일 00:00(KST)** 에 협상 성공률을 보고 자동 조정하는 배치 서비스입니다.
- backend/negodata/agent 와 **완전히 독립**된 컨테이너로 돕니다. 이 서비스가 꺼져 있어도 협상·견적은 정상 동작합니다(값 조정만 멈춤).
- 켜두기만 하면 스케줄이 자동으로 돕니다. 사람이 정기적으로 할 일은 없고, 격주 배치 다음 날 로그 한 번 확인이 전부입니다(§10).
## 2. 구성 요소 한눈에
```
[anchoring 컨테이너] ──── 격주 배치 실행 (APScheduler 내장)
│ 읽기: negotiation.sessions / quotation.quotations / partner.items
│ 쓰기: anchoring.adjustments (조정 이력) + sessions.used_by_adjustment_id (채점 마킹)
▼
[PostgreSQL (외부, negosium_db)] [anchoring-redis 컨테이너]
진실 원천 — 영구 이력 조회 캐시(사본) — 없어져도 복구됨
```
| 구성 요소 | 역할 | 죽으면? |
|---|---|---|
| anchoring 컨테이너 | 격주 조정 배치 + 캐시 갱신 | 조정만 멈춤. 재기동 후 `--once`로 캐치업 |
| anchoring-redis | rate 조회 캐시 (negodata가 참조) | **무해** — 자동으로 DB 폴백, 복구 시 자가 회복 |
| PostgreSQL | 모든 데이터의 원본 | 서비스 전체 의존 (기존 DB 운영 정책에 따름) |
## 3. 처음 설치하고 실행하기
### 사전 준비
- PostgreSQL(negosium_db) 접속 정보 (기존 `postgres-init/01~04` 스키마가 적용된 DB)
- Docker (운영) 또는 Python 3.12+ (로컬 개발)
### STEP 1 — DB 스키마 적용 (최초 1회)
```bash
psql -h <DB호스트> -U <계정> -f postgres-init/00-init.sql # 레포 루트에서 (스키마 전체 통합 파일)
```
- anchoring 스키마: 테이블 1개(`anchoring.adjustments`)·조회용 뷰 2개(`value_history`, `current_values`)·인덱스를 추가합니다.
(`negotiation.sessions` 앵커링 컬럼도 같은 `00-init.sql` 의 negotiation 섹션에 포함)
- `IF NOT EXISTS` 라 **여러 번 실행해도 안전**합니다.
### STEP 2 — 설정 채우기
```bash
# 실행 환경(APP_ENV, 기본 local)에 맞는 파일을 만든다 — local/dev/prod
cp config.toml.example config.local.toml
# config.local.toml 열어서 [db] 호스트/계정/비밀번호 채우기 (dev/prod 는 config.dev.toml/config.prod.toml)
```
- 환경 선택은 `APP_ENV` 환경변수(기본 `local` → `config.local.toml`). **dev/prod 는 파일이 없으면 기동이 즉시 중단**됩니다(오타·미배치 상태로 로컬 기본값에 붙는 사고 방지). local 은 파일 없이도 코드 기본값으로 뜹니다.
환경변수로 덮어쓸 수도 있습니다(우선순위: env > config.{APP_ENV}.toml > 기본값):
`DB_HOST` `DB_PORT` `DB_USER` `DB_PASSWORD` `DB_NAME` / `REDIS_HOST` `REDIS_PORT` `REDIS_DB` `REDIS_PASSWORD` / `LOG_LEVEL`
> config.{env}.toml 은 **이미지에 들어가지 않습니다**(시크릿이 이미지 레이어에 남는 것을 방지 — .dockerignore 로도 차단).
> 도커 실행 시 compose 가 읽기 전용 마운트하므로, **`docker compose up` 전에 해당 환경 파일이 반드시 존재해야 합니다**
> (없이 up 하면 docker 가 같은 이름의 디렉터리를 만들어 기동에 실패합니다). 루트 compose 는 APP_ENV=local + config.local.toml 마운트.
### STEP 3-A — 도커로 실행 (운영 권장)
```bash
# 레포 루트에서 (anchoring 서비스는 루트 docker-compose.yml 에 통합됨)
docker compose up -d --build anchoring anchoring-redis
docker logs -f anchoring # 기동 로그 확인 (아래 §4)
```
redis 가 함께 뜨고, 로그 로테이션(10MB×5)·재시작 정책까지 자동 설정됩니다.
### STEP 3-B — 로컬 파이썬으로 실행 (개발용)
```bash
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
PYTHONPATH=src .venv/bin/python -m anchoring.main # 스케줄러 상주
# 또는
PYTHONPATH=src .venv/bin/python -m anchoring.main --once # 배치 즉시 1회 실행 후 종료
```
## 4. 정상 동작 확인 체크리스트
기동 직후 로그에 아래 3줄이 순서대로 보이면 정상입니다:
```
[main] 정적 기본 테이블 로드·검증 완료 (46칸 사다리)
[scheduler] 등록 — 매주 토 00:00 Asia/Seoul (격주 게이트는 잡 내부)
[main] 스케줄러 상주 시작 — 다음 실행 예정: 2026-07-04 00:00:00+09:00
```
배치가 실제로 도는지 즉시 확인하고 싶으면:
```bash
PYTHONPATH=src .venv/bin/python -m anchoring.main --once
# 도커: docker exec anchoring python -m anchoring.main --once
```
끝부분에 `종료 {'run_id': ..., 'status': 'done', ...}` 와 `[main] 결과: {...}` 가 나오면 성공입니다(부분 실패면 종료코드 1).
(협상 데이터가 없으면 `scanned: 0` — 이것도 정상)
**첫 운영 실행 전에는 예행 연습을 먼저** 하세요 — 쌓여 있는 협상 전량이 첫 실행에서 한 번에 채점되므로, 무엇이 얼마나 바뀔지 미리 보는 게 안전합니다:
```bash
docker exec anchoring python -m anchoring.main --once --dry-run
# DB/Redis 를 전혀 바꾸지 않고 "조정예정 …" 라인과 제외 예정 건수만 로그로 보여줍니다 (status: dry_run)
```
테스트 스위트로 확인하려면:
```bash
PYTHONPATH=src .venv/bin/python -m pytest tests/ -q # 전부 passed 기대(현재 20개)
```
## 5. 로그 읽는 법
### 로그 한 줄의 구조
```
2026-07-02 16:45:12+0900 INFO anchoring [batch 20260702-164512] 조정 company=f23c… type=1 price_range=10 n=13 성공=8 10‰→30‰ adj_id=32
└──── 시각(항상 KST) ──┘ └레벨┘ └── 회차 태그 ──────┘ └──────────────── 내용 (key=value 형식) ────────────────┘
```
- **시각은 항상 한국시간(+0900)** — 서버 시간대와 무관하게 고정돼 있습니다.
- `[batch 20260702-164512]` = **회차 태그**(run_id, 배치 시작 시각). 한 회차의 모든 로그가 같은 태그를 답니다.
- `‰`(천분율) 표기: `10‰ = 1%`. `10‰→30‰` 는 "1%에서 3%로 올렸다"는 뜻.
### 회차 하나의 로그 흐름 (위에서 아래로)
| 라인 | 의미 |
|---|---|
| `시작 — ISO 주차 27, force=False` | 배치 깨어남. force=True 는 수동 실행(`--once`) |
| `캐시 re-SET n칸` | 조정 이력 있는 칸 전체를 Redis 에 다시 적재(매주, 캐시 자가 회복) |
| `격주 게이트 미충족 — 평가 스킵` | 이번 주는 쉬는 주(격주). **정상 동작** |
| `제외 확정 마킹 n건` | 가격을 안 써낸 협상들을 채점 대상에서 영구 제외 처리 |
| `조정 company=… n=13 성공=8 10‰→30‰ adj_id=32` | **칸 하나의 값이 조정됨** — adj_id 로 DB 행과 대조 가능 |
| `회사요약 company=… 평가=1 상승=1 …` | 회사(테넌트)별 이번 회차 집계 |
| `종료 {…}` | 회차 전체 요약(스캔 건수, 평가 칸 수, 이월 등) |
### 자주 쓰는 검색 명령
```bash
docker logs anchoring | grep "batch 20260705" # 특정 회차 전체 보기
docker logs anchoring | grep "company=<uuid>" # 특정 회사만 (조정 + 회사요약)
docker logs anchoring | grep -E "WARNING|ERROR" # 이상 신호만
docker logs anchoring | grep "조정 " # 값이 바뀐 칸만
docker logs anchoring | tail -20 # 최근 상태
```
### 레벨별 대응 기준
| 레벨 | 의미 | 대응 |
|---|---|---|
| INFO | 정상 동작 기록 | 조치 불필요 |
| WARNING | 동작은 하지만 점검 필요 | §7 트러블슈팅에서 해당 메시지 찾기 |
| ERROR | 칸 단위 실패(다른 칸엔 영향 없음) | 스택 확인. 실패 칸은 다음 회차 자동 재시도 |
**핵심 규칙: WARNING 이상이 하나라도 있으면 들여다본다. INFO 뿐이면 건강하다.**
## 6. 자주 하는 운영 작업
| 작업 | 명령 |
|---|---|
| 수동 배치 1회 (격주 게이트 무시) | `docker exec anchoring python -m anchoring.main --once` |
| **예행 연습** (DB/Redis 무변경, 예상 결과만 로그) | `docker exec anchoring python -m anchoring.main --once --dry-run` — 첫 운영 실행 전 필수 권장 |
| 재기동 | `docker compose restart anchoring` |
| 서비스 중지/시작 | `docker compose stop anchoring anchoring-redis` / `docker compose up -d anchoring anchoring-redis` (레포 루트에서) |
| 설정 변경 반영 | config.{env}.toml 수정 → `docker compose restart anchoring` (파일은 마운트라 리빌드 불필요) |
| 다음 실행 예정 시각 확인 | `docker logs anchoring \| grep "다음 실행 예정"` |
| 로그 레벨 올리기(디버깅) | env `LOG_LEVEL=debug` 로 재기동 |
## 7. 문제 해결 (트러블슈팅)
| 증상 (로그 메시지) | 원인 | 조치 |
|---|---|---|
| 기동 실패 + `BaseTableError: 정적 테이블 …` | `resources/anchoring_base.json` 손상/수정됨 | **의도된 안전장치** — git 으로 파일 원복 후 재기동. 이 파일은 절대 수정 금지 |
| 기동 실패 + DB 연결 예외 | config.{env}.toml/env 의 DB 접속 정보 오류(dev/prod 의 CHANGE_ME 미기입 포함) | 접속 정보 확인, `psql` 로 직접 접속 테스트 |
| 기동 실패 + `설정 파일이 없습니다 (APP_ENV=…)` | dev/prod 인데 config.{env}.toml 미배치 | `cp config.toml.example config.{env}.toml` 채우고 재기동 |
| 기동 실패 + `config.local.toml` 이 **디렉터리**로 생겨 있음 | 파일 없이 `docker compose up` — 마운트 대상이 없어 docker 가 디렉터리를 만듦 | `docker compose down` → 디렉터리 삭제 → `cp config.toml.example config.local.toml` 채우고 재기동 |
| `[redis] GET/SET 실패 … DB 폴백` WARN | Redis 다운/네트워크 | **서비스는 계속 정상 동작**(DB 폴백). `docker compose up -d anchoring-redis` 로 복구하면 다음 실행 때 캐시 자동 재적재 |
| `redis 실패 누계 get=… set=…` WARN | 위와 동일(회차 요약) | 위와 동일 |
| `[redis] 범위 밖 캐시 값 무시(오염 의심)` WARN | 누군가/다른 프로세스가 Redis 에 비정상 값을 씀 | 동작엔 문제 없음(자동 무시 + DB 폴백 + 재적재로 자가 교정). 반복되면 Redis 접근 경로 점검 — 포트가 외부에 열려 있지 않은지(`127.0.0.1` 바인딩) 확인 |
| `가격 제시 흔적 0%` WARN | backend 의 가격 기록 배선이 끊김(배포 사고 등) — 학습이 조용히 멈추는 신호 | backend 팀에 `chat_service` 의 `last_offer_price` 갱신 경로 점검 요청 |
| `칸 평가 실패 company=…` ERROR | 해당 칸 DB 오류/마킹 경합 | 스택 확인. 실패 칸은 마킹되지 않아 **다음 회차 자동 재시도** — 같은 칸이 연속 실패하면 개발 팀 문의 |
| `박제 정합 불일치 n건` WARN | negodata 의 앵커 산출 이식 오류 의심(정수식 ≠ 박제 anchor) | negodata 팀에 `docs/인수인계.md` §1.3 정수식 적용 여부 점검 요청 |
| 종료 요약이 WARNING (`failed_cells > 0`) | 일부 칸 실패 | 바로 위 ERROR 라인들 확인 |
| 토요일 00:00 에 서비스가 꺼져 있었음 | 배치 회차 누락 | 데이터 유실 없음(자동 이월). 재기동 후 `--once` 로 즉시 캐치업 |
| 로그가 아무것도 안 나옴 | 컨테이너 죽음 | `docker ps -a` 로 상태 확인 → `docker logs anchoring` 마지막 로그 → 재기동 |
## 8. DB로 이력 추적하기
로그는 로테이션되지만 **DB 이력은 영구**입니다. "왜 이 값이 됐는가"는 항상 DB로 답할 수 있습니다.
```sql
-- ① 어떤 회사의 값 변천사 (시간순) — 이전 값→새 값·변화폭·성공률까지 한 줄에
SELECT * FROM anchoring.value_history
WHERE company_id = '<uuid>'
ORDER BY adjustment_id;
-- ①-b 어떤 회사의 칸별 "현재값" 한눈에 (여기 없는 칸 = 시작값 1%)
SELECT * FROM anchoring.current_values
WHERE company_id = '<uuid>';
-- ② 특정 조정(adj_id)의 근거가 된 협상들
SELECT s.session_id, s.status, s.anchoring_price, s.last_offer_price, s.bid_price
FROM negotiation.sessions s
WHERE s.session_id IN (
SELECT jsonb_array_elements_text(used_session_ids)::uuid
FROM anchoring.adjustments WHERE id = <adj_id>
);
-- ③ 특정 협상이 어느 조정에 채점됐나
SELECT used_by_adjustment_id FROM negotiation.sessions WHERE session_id = '<uuid>';
-- NULL = 아직 채점 전(다음 회차로 이월) / 0 = 채점 제외 확정 / 숫자 = 해당 조정 id → ② 로
```
로그의 `adj_id=32` ↔ DB 의 `adjustments.id=32` 가 같은 것을 가리킵니다.
## 9. 절대 하면 안 되는 것
이 시스템의 신뢰성은 "기록이 불변"이라는 전제 위에 서 있습니다 (상세 근거: `개발용.md` §12).
- ❌ `anchoring.adjustments` 행을 **UPDATE/DELETE** — 조정 이력은 유일한 진실 원천
- ❌ `sessions` 의 `anchoring_price` / `anchoring_value` 수동 수정 — 채점 근거가 오염됨
- ❌ `resources/anchoring_base.json`(기준표) 수정 — 검증 실패로 기동이 막히며, 값 변경은 정책 재확정 사안
- ❌ 상수(조정폭 δ, 경계 60/30, 상·하한, 10건 임계, 배치 주기) 임의 변경 — 전부 정책 고정값
- ❌ anchoring 컨테이너를 **2개 이상 동시 실행** — 중복 조정 방지 장치(롤백)가 막아주긴 하지만 설계상 단일 인스턴스가 원칙
## 10. 정기 점검 체크리스트
**격주 배치 다음 날(일요일) 5분 점검:**
```bash
docker logs anchoring | grep -E "WARNING|ERROR" | tail # ① 이상 신호 없나
docker logs anchoring | grep "종료" | tail -1 # ② status: done 인가
docker logs anchoring | grep "다음 실행 예정" # ③ (재기동했다면) 다음 스케줄 정상인가
```
- ① 이 비어 있고 ② 가 `'status': 'done'` 이면 끝.
- `carryover_cells`(이월)가 계속 크기만 하고 `evaluated_cells` 가 0인 상태가 몇 달 지속되면 거래량 자체가 적은 것 — 장애가 아니라 정책 검토(희소 칸 과제, `기획용.md` FAQ) 대상입니다.
- 분기에 한 번쯤: 조정 이력 백업이 DB 백업 정책에 포함돼 있는지 확인 (`adjustments` 는 영구 보존 대상).