DB 접근 가이드

시스템은 모든 상태 데이터를 MongoDB 한 곳에 저장합니다. 이 문서는 어떻게 붙고, 어디를 보고, 어떻게 안전하게 고치는지를 정리합니다.

1. 접속 방법

1.1 데이터베이스 이름

환경변수 APP_ENV 값에 따라 DB 이름이 결정됩니다 (config/__init__.py:db_name).

APP_ENVDB 이름
prdprd_heka
devdev_heka
그 외 / 미설정local_heka

운영 환경에서는 prd_heka 만 보면 됩니다.

1.2 접속 수단

수단우선순위용도접근 방법
mongo-express (웹)1순위일상적인 조회·한두 건 수정·스키마 확인https://watcher.conecloud.io/mongo/ (Basic Auth)
mongosh (CLI)2순위updateMany / deleteMany대량 작업, 집계(aggregate), 스크립트 연동, 복구 작업Primary 파드에 직접 포트포워딩 후 로컬 접속

원칙: 가능하면 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=1mongodb-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)에 커밋되어 있습니다. 값이 바뀌면 아래 경로를 보고 최신을 확인하세요.

용도시크릿/Helm 키파일 경로 (라인)네임스페이스
MongoDB root (운영자·앱이 사용)Secret/mongodb-app-user-credentials → key root-user, root-passwordkustomize/mongodb/base/kustomization.yaml:24-27mongodb
PSMDB 내부 시스템 계정 (operator 전용 — 직접 쓸 일 없음)Secret/mongodb-system-user-credentialsdatabaseAdmin / clusterAdmin / clusterMonitor / userAdmin / backupkustomize/mongodb/base/kustomization.yaml:9-20mongodb
PSMDB at-rest 암호화 키Secret/mongodb-encryption-keyencryption-keykustomize/mongodb/base/kustomization.yaml:21-23mongodb
mongo-express 웹 로그인 (Basic Auth)ID: Helm value, PW: Secret/mongo-express-credentials.basic-auth-passwordID: helm/mongo-express/overlay.yaml:7 (basicAuthUsername)
PW: kustomize/mongodb/base/kustomization.yaml:28-31
mongodb
mongo-express → MongoDB 연결 비밀번호Secret/mongo-express-credentials.mongodb-admin-passwordkustomize/mongodb/base/kustomization.yaml:28-31mongodb
앱이 MongoDB 접속 시 쓰는 값Secret/prd-cone-watcher-envMONGODB_USERNAME, MONGODB_PASSWORD (root와 동일)app/base/kustomization.yaml:27-30cone-watcher
Valkey(Redis) 비밀번호Helm value (aclUsers.default.password)helm/valkey/overlay.yaml:25valkey
앱이 Valkey 접속 시 쓰는 값Secret/prd-cone-watcher-envREDIS_PASSWORDapp/base/kustomization.yaml:31-33cone-watcher

클러스터에 배포된 실제 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 계정 · 조직

컬렉션용도주요 필드
user로그인 사용자 계정uid, email, company_id, role, organizations
companyMSP 회사 (파트너)uid, 상호/사업자 정보
organization고객사 (조직)uid, company_id, status
contract고객사 구독 계약uid, organization_id, 계약 기간/계정 목록
nhn_account연동된 NHN Cloud 리셀러 계정uid, company_id, cloud_type (NORMAL/GOV)

2.2 청구

컬렉션용도주요 필드
nhn_invoiceNHN 청구서 (월별)company_id, invoice_id, account_id, date, status
invoice_deliveries청구서 이메일 발송 기록invoice_id, 발송 상태. TTL 365일
partner_usage파트너(MSP)별 사용량 청구company_id, date, status

청구서 status 값:

의미
OriginNHN API에서 받은 원본 — 직접 수정 금지
UnissuedOrigin에서 복사된 수정 가능한 작업본 (고객 미발행)
Invoiced고객에게 발행 완료
Paid입금 확인
Overdue연체

2.3 운영 메타

컬렉션용도TTL
worker_task비동기 작업 추적현재 미사용 (레거시). 모델(common/models/worker_task_model.py)의 task_name enum(InvoiceCreation/InvoiceSummary/TrendLine/CURUpdate)이 AWS 시절 포크 유산이며, WorkerTaskController 가 어디서도 호출되지 않아 문서가 쌓이지 않음. 태스크 추적은 워커 파드 로그(kubectl logs) + error_log 에 의존
activity_log사용자 행동 감사 로그
error_log시스템 에러 로그90일
notification인앱 알림
verification이메일 인증번호
token_blacklist로그아웃된 토큰
reset_token비밀번호 재설정 토큰
temporary_data일회성 임시 결과
company_settings회사별 운영 옵션
white_label화이트라벨(브랜딩) 설정
report정기 보고서 구독

참고: 인덱스 정의와 실제 스키마는 common/database/<collection>_db.pycommon/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 쿼리로는 실행 이력을 볼 수 없습니다. 아래 두 가지를 조합해 확인합니다.

  1. 워커 파드 로그 — 태스크 시작/진행/완료·실패 스택트레이스가 여기 찍힙니다.
    # 최근 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
  2. 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. 수정 시 안전 원칙

직접 수정이 필요할 때 반드시 다음 순서를 지킵니다.

  1. 백업: 작업 직전 mongodump 으로 해당 컬렉션 또는 DB 전체를 스냅샷합니다 (백업 및 복구 참조).
  2. 조회로 대상 확정: update* 전에 동일 필터로 find 를 돌려 건수와 내용을 확인합니다.
  3. 단건 테스트: 가능하면 updateOne 으로 한 건 먼저 바꾸고, 결과를 확인한 뒤 updateMany 로 확장합니다.
  4. 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 작업 전에 훑어봅니다.

  • APP_ENV 와 접속한 DB 이름이 일치하는가? (db.getName())
  • 최근 백업이 있는가? 없다면 mongodump 먼저.
  • find 결과의 건수가 예상 범위 안인가?
  • 단건 테스트 후 확장하는가?
  • Origin 상태 청구서를 수정하려 하지 않는가?
  • 발행된(Invoiced/Paid) 청구서를 삭제한다면 이해관계자에게 공유했는가?