DB 접근 가이드
시스템은 모든 상태 데이터를 MongoDB 한 곳에 저장합니다. 이 문서는 어떻게 붙고, 어디를 보고, 어떻게 안전하게 고치는지를 정리합니다.
1. 접속 방법
1.1 데이터베이스 이름
환경변수 APP_ENV 값에 따라 DB 이름이 결정됩니다 (config/__init__.py:db_name).
운영 환경에서는 prd_heka 만 보면 됩니다.
1.2 접속 수단
원칙: 가능하면 mongo-express 로 먼저 접근합니다. GUI가 조회·단건 수정에서 사고 확률이 가장 낮고, 접속 이력이 브라우저 Basic Auth 로그에 일관되게 남습니다. mongosh 는 (1) 여러 문서를 한 번에 바꿔야 하거나, (2) aggregate/$lookup 같은 복잡한 쿼리가 필요하거나, (3) 백업·복구 스크립트와 연동해야 할 때만 사용합니다. 어떤 경로든 쓰기 작업 전 §4 안전 원칙은 동일하게 지킵니다.
배포 레이아웃 참고: MongoDB는 Percona Server for MongoDB(PSMDB) operator 로 배포되며 다음 구조입니다.
- 네임스페이스:
mongodb (앱의 cone-watcher 와 분리됨)
- Cluster CR 이름:
mongodb, replset 이름: rs, 크기: 3 (prd) / 1 (dev)
- 파드:
mongodb-rs-0, mongodb-rs-1, mongodb-rs-2
- Replica priority (
kustomize/mongodb/overlays/prd/patches/server.yaml): rs-0=2, rs-1=1, rs-2=1 → mongodb-rs-0 이 선호 primary
- Headless Service:
mongodb-rs.mongodb.svc.cluster.local (PSMDB operator 기본 컨벤션)
- root 계정 시크릿:
mongodb-app-user-credentials (key root-password) — mongodb 네임스페이스
mongosh 접속 예시
# 1. MongoDB primary 파드에 직접 포트포워딩 (네임스페이스 mongodb)
# Service(svc/mongodb-rs)는 secondary로 라우팅될 수 있어 write 작업이 실패할 수 있습니다.
# replica set의 primary 파드(기본적으로 mongodb-rs-0)를 타겟하세요.
kubectl -n mongodb port-forward pod/mongodb-rs-0 27017:27017
# 2. mongosh 접속 (root 비밀번호는 secret/mongodb-app-user-credentials 의 root-password)
mongosh "mongodb://root:<PASSWORD>@localhost:27017/prd_heka?authSource=admin&directConnection=true"
# 3. 접속 후 primary 여부 확인 — write 작업 전 반드시 체크
# db.hello().isWritablePrimary // true 여야 함
# 아니라면 rs.status() 로 primary 파드 이름 확인 후 해당 파드로 다시 포트포워딩
주의 1: directConnection=true 는 로컬에서 포트포워딩으로 접속할 때만 켜는 옵션입니다. mongosh 는 replica set 멤버가 광고한 내부 DNS 이름(mongodb-rs-0.<...>.svc.cluster.local)으로 재접속을 시도하는데, 로컬에서는 그 이름이 풀리지 않아 연결이 끊어집니다. directConnection=true 가 이 재접속을 막아 localhost:27017 한 노드에만 붙게 해 줍니다.
클러스터 내부의 애플리케이션은 해당 DNS가 정상적으로 풀리므로 이 옵션이 필요 없습니다. 애플리케이션 기본값은 MONGODB_DIRECT_CONNECTION=false 이며(config/__init__.py), .env.development.example 만 로컬 실행 편의상 true 로 되어 있습니다.
주의 2: Service가 아닌 Pod를 직접 포트포워딩 하는 것을 권장합니다. Service는 현재 primary가 아닌 복제본으로 연결을 보낼 수 있고, 이 경우 updateOne/deleteMany 같은 write 명령이 not master 에러로 실패합니다. Read-only 쿼리만 한다면 svc도 무방하지만, 운영 편의상 항상 primary 파드로 붙는 것을 기본 규칙으로 삼습니다.
1.3 자격증명 위치 (cone-watcher-gitops 리포)
DB·mongo-express 접속 자격증명은 모두 gitops 리포(grumatic/cone-watcher-gitops, branch develop)에 커밋되어 있습니다. 값이 바뀌면 아래 경로를 보고 최신을 확인하세요.
클러스터에 배포된 실제 Secret 값을 바로 꺼내려면:
# 운영자용 MongoDB root 비밀번호
kubectl -n mongodb get secret mongodb-app-user-credentials -o jsonpath='{.data.root-password}' | base64 -d; echo
# mongo-express Basic Auth 비밀번호
kubectl -n mongodb get secret mongo-express-credentials -o jsonpath='{.data.basic-auth-password}' | base64 -d; echo
# mongo-express Basic Auth ID (Helm value — Secret에는 없고 Deployment의 env 에 박힘)
kubectl -n mongodb get deploy mongo-express -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="ME_CONFIG_BASICAUTH_USERNAME")].value}'; echo
보안 주의: 현재 gitops 리포는 위 자격증명이 평문으로 커밋되어 있습니다. 리포 접근 권한 = DB/웹 로그인 접근 권한입니다. 운영 중에는 SOPS, Sealed Secrets, External Secrets Operator 등으로 전환하는 것을 권장합니다.
2. 컬렉션 맵
컬렉션은 크게 계정/조직, 청구, 운영 메타 세 그룹으로 나뉩니다.
2.1 계정 · 조직
2.2 청구
청구서 status 값:
2.3 운영 메타
참고: 인덱스 정의와 실제 스키마는 common/database/<collection>_db.py 와 common/models/<collection>_model.py 에 있습니다.
3. 자주 쓰는 조회 쿼리
모든 예시는 prd_heka DB에서 mongosh 로 실행한다고 가정합니다.
3.1 회사 / 조직 / 계정 확인
// 특정 회사의 기본 정보
db.company.findOne({ uid: '<COMPANY_UID>' });
// 해당 회사의 고객사(조직) 목록
db.organization.find(
{ company_id: '<COMPANY_UID>' },
{ uid: 1, name: 1, status: 1, created_at: 1 }
).sort({ created_at: -1 });
// 연동된 NHN 계정 목록
db.nhn_account.find(
{ company_id: '<COMPANY_UID>' },
{ uid: 1, alias: 1, cloud_type: 1, email: 1 }
);
3.2 사용자 확인
// 이메일로 사용자 찾기
db.user.findOne({ email: '<USER_EMAIL>' });
// 특정 회사의 관리자 역할만
db.user.find(
{ company_id: '<COMPANY_UID>', role: { $in: ['ROOT', 'SYSTEM_ADMIN', 'ADMIN'] } },
{ email: 1, role: 1, deactivate: 1 }
);
// 비활성 사용자
db.user.find(
{ company_id: '<COMPANY_UID>', deactivate: true },
{ email: 1, role: 1 }
);
3.3 청구서 확인
// 특정 월의 모든 청구서 (status별 개수)
db.nhn_invoice.aggregate([
{ $match: { company_id: '<COMPANY_UID>', date: '2026-04' } },
{ $group: { _id: '$status', count: { $sum: 1 } } }
]);
// 특정 조직의 최근 청구서 3개
db.nhn_invoice.find(
{ company_id: '<COMPANY_UID>', organization_id: '<ORG_UID>' },
{ invoice_id: 1, date: 1, status: 1, total_amount: 1 }
).sort({ date: -1 }).limit(3);
// 발행되지 않고 남아있는 작업본(Unissued) 목록
db.nhn_invoice.find(
{ company_id: '<COMPANY_UID>', status: 'Unissued' },
{ invoice_id: 1, organization_id: 1, date: 1 }
);
3.4 배치/워커 상태 확인
worker_task 컬렉션은 현재 미사용(레거시) 이라 DB 쿼리로는 실행 이력을 볼 수 없습니다. 아래 두 가지를 조합해 확인합니다.
- 워커 파드 로그 — 태스크 시작/진행/완료·실패 스택트레이스가 여기 찍힙니다.
# 최근 500줄 중 에러/태스크 키워드만
kubectl logs -n cone-watcher -l app.kubernetes.io/component=worker --tail=500 \
| grep -iE 'error|traceback|task|invoice'
# 특정 파드 전체 로그
kubectl logs -n cone-watcher <POD_NAME> --since=6h
error_log 컬렉션 — 애플리케이션이 의도적으로 기록한 시스템 에러(주로 NHN API 실패). §3.5 참조.
청구서 배치가 돌았는지 결과물 기준으로 역추적하려면 nhn_invoice 로 직접 확인이 가장 확실합니다:
// 해당 월에 실제로 생성된 청구서 수 (Origin 단계까지 왔는지)
db.nhn_invoice.countDocuments({ date: '2026-04', status: 'Origin' });
// Unissued 로 복사까지 성공한 수
db.nhn_invoice.countDocuments({ date: '2026-04', status: 'Unissued' });
3.5 시스템 에러 로그
// 최근 1시간 NHN API 관련 에러
db.error_log.find({
source: 'nhn_api',
created_at: { $gte: new Date(Date.now() - 60 * 60 * 1000) }
}).sort({ created_at: -1 });
// 특정 계정에서 발생한 에러
db.error_log.find({
'context.account_uid': '<ACCOUNT_UID>'
}).sort({ created_at: -1 }).limit(50);
4. 수정 시 안전 원칙
직접 수정이 필요할 때 반드시 다음 순서를 지킵니다.
- 백업: 작업 직전
mongodump 으로 해당 컬렉션 또는 DB 전체를 스냅샷합니다 (백업 및 복구 참조).
- 조회로 대상 확정:
update* 전에 동일 필터로 find 를 돌려 건수와 내용을 확인합니다.
- 단건 테스트: 가능하면
updateOne 으로 한 건 먼저 바꾸고, 결과를 확인한 뒤 updateMany 로 확장합니다.
activity_log 또는 작업 메모 남기기: 누가, 언제, 왜 수정했는지 별도 기록합니다. 운영 DB에 직접 쓴 모든 변경은 감사 대상입니다.
4.1 자주 쓰는 수정 예시
청구서 상태 되돌리기 (Invoiced → Unissued)
발행 후 수정이 필요한 경우(청구 항목 오류 등):
// 1. 대상 확인
db.nhn_invoice.find({
company_id: '<COMPANY_UID>',
organization_id: '<ORG_UID>',
date: '2026-04',
status: 'Invoiced'
});
// 2. 상태 되돌리기
db.nhn_invoice.updateOne(
{ company_id: '<COMPANY_UID>', invoice_id: '<INVOICE_ID>' },
{ $set: { status: 'Unissued', updated_at: new Date() } }
);
주의: Origin 상태는 NHN API 원본이므로 절대 수정하지 않습니다. 수정할 대상은 항상 Unissued 이후의 복사본입니다.
사용자 비활성화/활성화
db.user.updateOne(
{ email: '<USER_EMAIL>' },
{ $set: { deactivate: true, updated_at: new Date() } }
);
비밀번호 초기화 플래그 설정
초기 비밀번호 재설정을 강제하고 싶을 때. 해시 값을 직접 바꾸는 대신 플래그만 세웁니다.
db.user.updateOne(
{ email: '<USER_EMAIL>' },
{ $set: { need_change_password: true } }
);
// 이후 사용자에게 비밀번호 재설정 메일을 API로 발송 (04번 문서 참조)
조직 상태 변경 (일시정지)
db.organization.updateOne(
{ uid: '<ORG_UID>' },
{ $set: { status: 'suspended', updated_at: new Date() } }
);
status 값: in-use / canceled / suspended / poc.
5. 복구 시나리오
5.1 특정 월 청구서 재생성
NHN API 수집이 잘못되어 해당 월 청구서를 처음부터 다시 만들고 싶을 때.
// 1. 대상 청구서 먼저 조회해서 수량 확인
db.nhn_invoice.countDocuments({
company_id: '<COMPANY_UID>',
date: '2026-04'
});
// 2. 해당 월 청구서 전체 삭제 (Origin 포함)
db.nhn_invoice.deleteMany({
company_id: '<COMPANY_UID>',
date: '2026-04'
});
// 3. Worker에 재생성 작업 enqueue — 관리 API 또는 Python 쉘에서:
// nhn_invoice_task.send(company_id='<COMPANY_UID>', month='2026-04')
주의: 이미 고객사에 발행된(Invoiced/Paid) 청구서를 삭제하면 고객 화면에서도 사라집니다. 반드시 사전에 공유하고, 발행 전 상태에서만 수행하는 것을 권장합니다.
5.2 특정 조직만 재생성
// 해당 조직의 해당 월 청구서만 삭제 후 재생성
db.nhn_invoice.deleteMany({
company_id: '<COMPANY_UID>',
organization_id: '<ORG_UID>',
date: '2026-04'
});
// → nhn_invoice_task.send(
// company_id='<COMPANY_UID>',
// organization_id='<ORG_UID>',
// month='2026-04'
// )
5.3 프로젝트 동기화 강제 실행
NHN 쪽에서 프로젝트가 추가/삭제됐는데 화면에 반영되지 않을 때:
# Worker Python 쉘 또는 관리 API:
update_nhn_project_list_task.send(
company_id='<COMPANY_UID>',
organization_id='<ORG_UID>',
account_id='<NHN_ACCOUNT_UID>'
)
파라미터 없이 호출하면 전체 회사·조직·계정을 동기화합니다.
5.4 감사 로그 / 에러 로그 정리
error_log 는 TTL 90일로 자동 삭제됩니다 — 수동 정리 불필요.
invoice_deliveries 는 TTL 365일.
activity_log 는 TTL이 없으므로 장기간 저장됩니다. 성장 속도가 빨라지면 아카이브/삭제 정책을 별도 수립하세요.
6. 체크리스트
DB 작업 전에 훑어봅니다.