o2o-negosium-original/schedules/anchoring/docs/운영및유지보수.md
민헌 a132dac57a feat(anchoring): 조회용 뷰 2종 신설 — records 테이블은 검토 후 기각 (TODO 2)
요구(회사별 앵커링 값 업데이트 리스트업 + 이전 값 판별)는 rate_adjustments
한 행에 anchor_rate_before→after 가 박제되어 이미 충족 — 신규 테이블은 동일
정보의 사본만 만들므로 기각하고, 조회를 제품화하는 파생 뷰로 해결:

- anchoring.rate_history: 값 변경 이력 리스트업(이전→새 값, delta_permille,
  success_rate, created_at)
- anchoring.current_rates: 칸별 현재값(최신 조정 행 — 없는 칸 = 시작값 10‰)

뷰는 상태가 없어 오염·재구축 이슈 자체가 없고 append-only 보호 대상 아님.
통합 테스트에 뷰 검증 추가(이력 before/after·성공률, 현재값). 운영 문서 §8
쿼리를 뷰 기반으로 단순화, TODO 과제 2 종결(대시보드 페이징 요구 시 스냅샷
테이블 승격 재검토 명시).

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

12 KiB
Raw Blame History

앵커링 서비스 — 운영 및 유지보수 가이드

대상 독자: 이 프로젝트를 처음 보는 운영/개발 담당자. 이 문서 하나로 설치 → 실행 → 로그 확인 → 문제 해결까지 따라할 수 있게 쓰였습니다. 함께 볼 문서: 무엇을 하는 시스템인지 → 기획용.md / 흐름 그림 → 워크플로우.md / 구현 규범 → 개발용.md / 타 팀 적용 → 인수인계.md


목차

  1. 이 서비스는 무엇인가
  2. 구성 요소 한눈에
  3. 처음 설치하고 실행하기
  4. 정상 동작 확인 체크리스트
  5. 로그 읽는 법
  6. 자주 하는 운영 작업
  7. 문제 해결 (트러블슈팅)
  8. DB로 이력 추적하기
  9. 절대 하면 안 되는 것
  10. 정기 점검 체크리스트

1. 이 서비스는 무엇인가

협상 시스템의 앵커링 값(협상 합의 기준선을 목표가에서 몇 % 아래에 둘지)을 격주 토요일 00:00(KST) 에 협상 성공률을 보고 자동 조정하는 배치 서비스입니다.

  • backend/negodata/agent 와 완전히 독립된 컨테이너로 돕니다. 이 서비스가 꺼져 있어도 협상·견적은 정상 동작합니다(값 조정만 멈춤).
  • 켜두기만 하면 스케줄이 자동으로 돕니다. 사람이 정기적으로 할 일은 없고, 격주 배치 다음 날 로그 한 번 확인이 전부입니다(§10).

2. 구성 요소 한눈에

[anchoring 컨테이너] ──── 격주 배치 실행 (APScheduler 내장)
        │ 읽기: negotiation.sessions / quotation.quotations / partner.items
        │ 쓰기: anchoring.rate_adjustments (조정 이력) + sessions.anchoring_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회)

cd schedules/anchoring
psql -h <DB호스트> -U <계정> -d negosium_db -f schema.sql
  • 테이블 1개(anchoring.rate_adjustments)와 negotiation.sessions 컬럼 3개를 추가합니다.
  • IF NOT EXISTS 라 여러 번 실행해도 안전합니다.

STEP 2 — 설정 채우기

cp config.toml.example config.toml
# config.toml 열어서 [db] 호스트/계정/비밀번호 채우기

환경변수로 덮어쓸 수도 있습니다(우선순위: env > config.toml > 기본값): DB_HOST DB_PORT DB_USER DB_PASSWORD DB_NAME / REDIS_HOST REDIS_PORT REDIS_PASSWORD / LOG_LEVEL

STEP 3-A — 도커로 실행 (운영 권장)

docker compose up -d --build
docker logs -f anchoring        # 기동 로그 확인 (아래 §4)

redis 가 함께 뜨고, 로그 로테이션(10MB×5)·재시작 정책까지 자동 설정됩니다.

STEP 3-B — 로컬 파이썬으로 실행 (개발용)

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

배치가 실제로 도는지 즉시 확인하고 싶으면:

PYTHONPATH=src .venv/bin/python -m anchoring.main --once
# 도커: docker exec anchoring python -m anchoring.main --once

마지막 줄에 종료 {'run_id': ..., 'status': 'done', ...} 가 나오면 성공입니다. (협상 데이터가 없으면 scanned: 0 — 이것도 정상)

테스트 스위트로 확인하려면:

PYTHONPATH=src .venv/bin/python -m pytest tests/ -q     # 15 passed 기대

5. 로그 읽는 법

로그 한 줄의 구조

2026-07-02 16:45:12+0900 INFO anchoring [batch 20260702-164512] 조정 company=f23c… type=1 bracket=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 … 회사(테넌트)별 이번 회차 집계
종료 {…} 회차 전체 요약(스캔 건수, 평가 칸 수, 이월 등)

자주 쓰는 검색 명령

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
재기동 docker compose restart anchoring
서비스 중지/시작 docker compose stop / docker compose up -d
설정 변경 반영 config.toml 수정 → docker compose up -d --build
다음 실행 예정 시각 확인 docker logs anchoring | grep "다음 실행 예정"
로그 레벨 올리기(디버깅) env LOG_LEVEL=debug 로 재기동

7. 문제 해결 (트러블슈팅)

증상 (로그 메시지) 원인 조치
기동 실패 + BaseTableError: 정적 테이블 … resources/anchoring_base.json 손상/수정됨 의도된 안전장치 — git 으로 파일 원복 후 재기동. 이 파일은 절대 수정 금지
기동 실패 + DB 연결 예외 config.toml/env 의 DB 접속 정보 오류 접속 정보 확인, psql 로 직접 접속 테스트
[redis] GET/SET 실패 … DB 폴백 WARN Redis 다운/네트워크 서비스는 계속 정상 동작(DB 폴백). docker compose up -d anchoring-redis 로 복구하면 다음 실행 때 캐시 자동 재적재
redis 실패 누계 get=… set=… WARN 위와 동일(회차 요약) 위와 동일
가격 제시 흔적 0% WARN backend 의 가격 기록 배선이 끊김(배포 사고 등) — 학습이 조용히 멈추는 신호 backend 팀에 chat_service 의 last_offered_price 갱신 경로 점검 요청
칸 평가 실패 company=… ERROR 해당 칸 DB 오류/마킹 경합 스택 확인. 실패 칸은 마킹되지 않아 다음 회차 자동 재시도 — 같은 칸이 연속 실패하면 개발 팀 문의
종료 요약이 WARNING (failed_cells > 0) 일부 칸 실패 바로 위 ERROR 라인들 확인
토요일 00:00 에 서비스가 꺼져 있었음 배치 회차 누락 데이터 유실 없음(자동 이월). 재기동 후 --once 로 즉시 캐치업
로그가 아무것도 안 나옴 컨테이너 죽음 docker ps -a 로 상태 확인 → docker logs anchoring 마지막 로그 → 재기동

8. DB로 이력 추적하기

로그는 로테이션되지만 DB 이력은 영구입니다. "왜 이 값이 됐는가"는 항상 DB로 답할 수 있습니다.

-- ① 어떤 회사의 값 변천사 (시간순) — 이전 값→새 값·변화폭·성공률까지 한 줄에
SELECT * FROM anchoring.rate_history
WHERE company_id = '<uuid>'
ORDER BY adjustment_id;

-- ①-b 어떤 회사의 칸별 "현재값" 한눈에 (여기 없는 칸 = 시작값 1%)
SELECT * FROM anchoring.current_rates
WHERE company_id = '<uuid>';

-- ② 특정 조정(adj_id)의 근거가 된 협상들
SELECT s.session_id, s.status, s.target_anchoring_price, s.last_offered_price, s.bid_price
FROM negotiation.sessions s
WHERE s.session_id IN (
  SELECT jsonb_array_elements_text(consumed_session_ids)::uuid
  FROM anchoring.rate_adjustments WHERE id = <adj_id>
);

-- ③ 특정 협상이 어느 조정에 채점됐나
SELECT anchoring_adjustment_id FROM negotiation.sessions WHERE session_id = '<uuid>';
-- NULL = 아직 채점 전(다음 회차로 이월) / 0 = 채점 제외 확정 / 숫자 = 해당 조정 id → ② 로

로그의 adj_id=32 ↔ DB 의 rate_adjustments.id=32 가 같은 것을 가리킵니다.

9. 절대 하면 안 되는 것

이 시스템의 신뢰성은 "기록이 불변"이라는 전제 위에 서 있습니다 (상세 근거: 개발용.md §12).

  • ❌ anchoring.rate_adjustments 행을 UPDATE/DELETE — 조정 이력은 유일한 진실 원천
  • ❌ sessions 의 target_anchoring_price / anchor_rate_permille 수동 수정 — 채점 근거가 오염됨
  • ❌ resources/anchoring_base.json(기준표) 수정 — 검증 실패로 기동이 막히며, 값 변경은 정책 재확정 사안
  • ❌ 상수(조정폭 δ, 경계 60/30, 상·하한, 10건 임계, 배치 주기) 임의 변경 — 전부 정책 고정값
  • ❌ anchoring 컨테이너를 2개 이상 동시 실행 — 중복 조정 방지 장치(롤백)가 막아주긴 하지만 설계상 단일 인스턴스가 원칙

10. 정기 점검 체크리스트

격주 배치 다음 날(일요일) 5분 점검:

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 백업 정책에 포함돼 있는지 확인 (rate_adjustments 는 영구 보존 대상).