본문 바로가기
삵
NoSQL

mongodump와 oplog로 MongoDB 백업하고 장애 직전 시점으로 복구(PITR)하기

sstdio.h·2026년 8월 19일·조회 10

운영 중인 MongoDB에서 누군가 db.orders.drop()을 잘못 날리는 사고는 생각보다 흔하다. 새벽에 받아둔 풀 백업만 있으면 그 시점으로는 돌아가지만, 백업 이후 낮 동안 들어온 주문은 통째로 날아간다. 이럴 때 필요한 게 특정 시점 복구, 즉 PITR(point-in-time recovery)다.

결론부터 말하면, mongodump --oplog로 풀 백업을 뜨고 그 뒤로 oplog를 증분으로 계속 캡처해 두면, 사고가 난 순간의 바로 앞 초까지 되돌릴 수 있다. 복원은 풀 덤프를 되살린 다음 mongorestore --oplogReplay --oplogLimit로 oplog를 사고 직전까지만 재생하는 방식이다.

1. oplog가 PITR의 핵심인 이유

oplog(operations log)는 복제 셋의 프라이머리가 수행한 모든 쓰기 연산을 순서대로 기록하는 특수 컬렉션이다. local 데이터베이스 안의 oplog.rs라는 이름으로 존재하며, 세컨더리가 이걸 읽어 같은 연산을 재생하면서 복제가 이뤄진다. 즉 oplog는 데이터베이스의 변경 이력을 담은 재생 가능한 로그다.

oplog의 각 항목에는 ts라는 타임스탬프 필드가 붙는다. BSON의 Timestamp 타입으로, Timestamp(t, i) 형태다. t는 유닉스 epoch 초, i는 같은 초 안에서 일어난 연산의 순번(ordinal)이다. PITR은 이 ts를 기준으로 "여기까지만 재생하고 멈춰라"라고 지시하는 것이다. 그래서 사고 난 연산의 ts를 찾아 그 직전에서 재생을 끊는 게 이 작업의 전부라고 봐도 된다.

주의할 점 하나. --oplog 계열 기능은 oplog를 가진 노드, 즉 복제 셋 멤버에서만 동작한다. 스탠드얼론이나 mongos에서는 쓸 수 없다. 단일 노드라도 복제 셋(single-node replica set)으로 초기화해 두면 oplog가 생긴다.

2. 전제 환경

실습은 단일 노드 복제 셋 rs0(MongoDB 7.x 또는 8.x)에서 진행한다. 백업 도구는 서버와 별도로 배포되는 MongoDB Database Tools(100.x 계열)의 mongodump, mongorestore를 쓴다. 버전은 저장소 상태에 따라 다를 수 있으니 먼저 확인한다.

$ mongodump --version
mongodump version: 100.10.0
git version: ...
Go version: go1.22

$ mongosh --quiet --eval 'rs.status().ok'
1

데이터는 shop 데이터베이스의 orders 컬렉션을 예로 든다. 풀 백업 시점(02:00)에 48120건이 있었다고 하자.

풀 백업, oplog 증분 캡처, 사고 ts 탐색, oplog 재생, 복원 확인의 다섯 단계를 순서대로 보여주는 흐름도
풀 백업과 증분 oplog를 겹치게 모아 두면 사고 연산 직전에서 재생을 끊을 수 있다.

3. 풀 백업: mongodump --oplog

--oplog는 덤프가 진행되는 동안 발생한 쓰기까지 oplog.bson이라는 파일로 함께 잡아 준다. 덤프에는 시간이 걸리고 그동안에도 쓰기는 들어오므로, 이 옵션이 있어야 백업이 특정 순간에 일관된 스냅샷이 된다. oplog.bson은 덤프 디렉터리 최상위에 떨어진다.

--oplog는 복제 셋 멤버의 전체 덤프에서만 쓸 수 있다. --db, --collection, --query 같은 범위 제한 옵션과 함께 쓰면 실패한다. 전체를 떠야 한다는 뜻이다.

$ mongodump --host rs0/localhost:27017 --oplog --out /backup/full-0819
2026-08-19T02:00:01.101+0900	writing admin.system.version to dump
2026-08-19T02:00:01.230+0900	writing shop.orders to /backup/full-0819/shop/orders.bson
2026-08-19T02:00:03.550+0900	done dumping shop.orders (48120 documents)
2026-08-19T02:00:03.560+0900	writing captured oplog to
2026-08-19T02:00:03.700+0900		dumped 12 oplog entries

결과 디렉터리를 보면 최상위에 oplog.bson이 있다.

$ find /backup/full-0819 -maxdepth 2 -type f
/backup/full-0819/oplog.bson
/backup/full-0819/admin/system.version.bson
/backup/full-0819/shop/orders.bson
/backup/full-0819/shop/orders.metadata.json

이 oplog.bson은 어디까지나 덤프가 도는 동안(02:00 근처)의 몇 초짜리 쓰기만 담는다. 낮 동안의 변경은 여기 없다. 그 구간은 다음 단계에서 별도로 캡처한다.

4. oplog 증분 캡처

풀 백업 이후의 변경을 되살리려면 local.oplog.rs를 주기적으로 떠 둬야 한다. 핵심은 풀 백업의 마지막 oplog 시점과 겹치거나 그 이전부터 캡처를 시작하는 것이다. 겹치는 구간이 있어도 oplog 연산은 멱등(idempotent)이라 두 번 적용돼도 결과가 같다. 반대로 구간 사이에 틈이 생기면 그만큼의 변경이 영영 빠진다.

여기서는 풀 백업 시작 시점(1787072400 = 2026-08-19 02:00:00 KST)부터의 oplog를 뜬다. --query에는 확장 JSON으로 Timestamp를 넘긴다.

$ mongodump --host rs0/localhost:27017 \
    -d local -c oplog.rs \
    --query '{ "ts": { "$gt": { "$timestamp": { "t": 1787072400, "i": 1 } } } }' \
    --out /backup/oplog-inc
2026-08-19T15:41:10.223+0900	writing local.oplog.rs to /backup/oplog-inc/local/oplog.rs.bson
2026-08-19T15:41:12.870+0900	done dumping local.oplog.rs (86541 documents)

여기서 한 번 걸린다. oplog.rs는 고정 크기(capped) 컬렉션이라 오래된 항목부터 밀려 사라진다. 캡처 주기가 oplog가 담아 두는 시간 창보다 길면 그 사이 변경이 이미 사라진 뒤라 복구가 불가능하다. 그래서 현장에서는 이 증분 덤프를 cron으로 촘촘히 돌려 구간을 항상 겹치게 둔다.

5. 사고 재현과 복원 시점 찾기

낮 동안 주문이 쌓여 15:39 무렵 48231건이 됐다고 하자. 그리고 15:40:00에 누군가 컬렉션을 통째로 날렸다.

rs0 [primary] shop> db.orders.drop()
true
rs0 [primary] shop> db.orders.countDocuments()
0

이제 이 drop 연산이 oplog의 어느 ts에 기록됐는지 찾는다. 컬렉션 삭제는 명령 연산이라 op: 'c', o: { drop: 'orders' } 형태로 남는다.

rs0 [primary] shop> use local
rs0 [primary] local> db.oplog.rs.find({ "o.drop": "orders" }).sort({ ts: -1 }).limit(1)
[
  {
    ts: Timestamp({ t: 1787121600, i: 1 }),
    op: 'c',
    ns: 'shop.$cmd',
    o: { drop: 'orders' }
  }
]

사고 연산의 ts는 Timestamp(1787121600, 1)다. 사람이 읽는 시각으로 확인해 두면 서사와 어긋나지 않는지 검증할 수 있다.

$ TZ=Asia/Seoul date -d @1787121600 '+%F %T %Z'
2026-08-19 15:40:00 KST

--oplogLimit에 넣을 값은 <t>:<i> 형식이다. 그러니 이 사고 연산의 값을 그대로 "1787121600:1"로 쓰면 된다. 다음 단계에서 설명하듯 이 옵션은 지정한 값 이상을 적용하지 않으므로, drop 연산 자체와 그 이후가 전부 제외되고 직전까지만 복원된다.

6. PITR 복원: 풀 덤프 되살리고 oplog 재생

복원은 두 단계다. 먼저 풀 덤프를 되살리면서 그 안의 oplog.bson을 재생해 02:00 시점의 일관된 상태로 만든다. --oplogReplay는 덤프 최상위의 oplog.bson을 자동으로 찾아 적용한다.

$ mongorestore --host rs0/localhost:27017 --oplogReplay /backup/full-0819
2026-08-19T16:00:03.010+0900	preparing collections to restore from
2026-08-19T16:00:03.220+0900	restoring shop.orders from /backup/full-0819/shop/orders.bson
2026-08-19T16:00:07.880+0900	finished restoring shop.orders (48120 documents, 0 failures)
2026-08-19T16:00:07.900+0900	replaying oplog
2026-08-19T16:00:08.050+0900	applied 12 oplog entries
2026-08-19T16:00:08.060+0900	12 oplog entries applied

이제 4단계에서 뜬 증분 oplog를 사고 직전까지만 재생한다. 증분 덤프는 /backup/oplog-inc/local/oplog.rs.bson에 있는데, mongorestore는 --oplogReplay 시 <dump-directory>/local/oplog.rs.bson도 자동 탐색 경로로 삼는다. 그래서 이 파일을 최상위 oplog.bson으로 옮겨 놓을 필요는 없다. 재배치는 편의일 뿐 필수 단계가 아니다. 증분 디렉터리를 그대로 가리키면 된다.

$ mongorestore --host rs0/localhost:27017 \
    --oplogReplay --oplogLimit="1787121600:1" \
    /backup/oplog-inc
2026-08-19T16:05:02.400+0900	replaying oplog
2026-08-19T16:05:14.900+0900	oplog  85.2MB
2026-08-19T16:05:15.010+0900	stopped at oplog entry before 1787121600:1
2026-08-19T16:05:15.020+0900	86500 oplog entries applied

--oplogLimit은 지정한 타임스탬프와 같거나 더 새로운 항목을 적용하지 않는다. drop 연산의 ts를 그대로 넣었으니 그 연산은 재생되지 않고, 그 앞의 모든 주문 쓰기까지만 반영된다.

복제 셋 이름, MongoDB 버전, 대상 컬렉션, 백업 시점 건수, 사고 시각과 타임스탬프, oplogLimit 값을 정리한 표
실습에 쓴 복제 셋 구성과 사고 시점, 성공 판정에 쓰는 문서 수를 한자리에 모았다.

7. 복원 확인

복원 후 문서 수가 사고 직전 값으로 돌아왔는지 본다.

rs0 [primary] local> use shop
rs0 [primary] shop> db.orders.countDocuments()
48231

02:00 백업 시점의 48120이 아니라 사고 직전의 48231이면 성공이다. 낮 동안 증가분(48120에서 48231로 111건)이 oplog 재생으로 되살아났고, drop만 빠졌다는 뜻이다. 최근 몇 건의 createdAt을 확인해 15:39대까지 들어온 문서가 있는지도 함께 본다.

8. mongorestore 함정

oplogLimit은 경계값 미만까지만 적용

이름은 limit이지만 동작은 "이 값 이상은 배제"다. 사고 연산 바로 앞까지 살리고 싶다면 사고 연산의 ts를 그대로 넣으면 된다. 한 초 안에 여러 연산이 있으면 i(ordinal)까지 정확히 맞춰야 원하는 지점에서 끊긴다.

--oplogReplay는 범위 제한 옵션과 못 쓴다

--db, --collection, --nsInclude, --nsExclude, --nsFrom, --nsTo는 --oplogReplay와 함께 지정하면 거부된다. oplog 재생은 특정 컬렉션만 골라 하는 게 아니라 전체 재생을 전제하기 때문이다.

oplog.bson과 --oplogFile 동시 지정은 에러

덤프 최상위에 oplog.bson이 있는데 --oplogFile로 또 다른 경로를 주면 mongorestore가 에러를 낸다. 둘 중 하나만 지정한다.

기존 데이터 위에 되살릴 때는 --drop 고려

이번 사례는 컬렉션이 이미 drop돼 비어 있어 그냥 복원했다. 하지만 대상에 옛 데이터가 남아 있으면 --drop 없이 복원 시 _id 중복으로 일부가 건너뛰어진다. 복원 전 상태를 반드시 확인한다.

증분 구간에 틈을 남기지 마라

증분 oplog 캡처 시작 ts가 풀 백업의 마지막 oplog ts보다 뒤면, 그 사이 변경이 재생되지 않는다. 시작점을 넉넉히 앞으로 잡아 항상 겹치게 하는 편이 복구 성공률을 높인다.

마무리

PITR의 뼈대는 단순하다. mongodump --oplog로 일관된 풀 백업을 뜨고, 그 뒤 local.oplog.rs를 겹치게 증분 캡처해 두면, 사고 연산의 ts를 찾아 --oplogLimit으로 그 직전까지만 재생하는 것으로 특정 시점 복구가 된다. 관건은 oplog가 밀려 사라지기 전에 증분을 촘촘히 받아 두는 운영 습관이다. 대규모 복제 셋이나 샤딩 환경이라면 이 수작업 대신 MongoDB Ops Manager나 Atlas의 연속 백업을 쓰는 편이 맞지만, 원리를 손으로 한 번 돌려 보면 그 도구들이 뒤에서 뭘 하는지 이해가 빨라진다.

자주 묻는 질문

mongodump --oplog를 스탠드얼론에서 쓸 수 있나?

쓸 수 없다. --oplog는 oplog를 유지하는 노드, 즉 복제 셋 멤버에서만 동작하며 mongos에서도 실패한다. 단일 서버라도 단일 노드 복제 셋으로 초기화하면 local.oplog.rs가 생겨 사용할 수 있다.

--oplogLimit의 타임스탬프 형식은 무엇인가?

<time_t>:<ordinal> 형식이다. time_t는 유닉스 epoch 초, ordinal은 그 초 안에서의 연산 순번이다. oplog 항목의 Timestamp(t, i)를 그대로 t:i로 옮겨 쓰면 된다. 예: Timestamp(1787121600, 1)이면 "1787121600:1".

--oplogLimit에 지정한 시점의 연산은 복원되나?

복원되지 않는다. mongorestore는 지정한 타임스탬프와 같거나 더 새로운 oplog 항목을 적용하지 않는다. 사고 직전까지만 살리려면 사고 연산의 ts를 그대로 넣으면 그 연산과 이후가 모두 제외된다.

증분 oplog 파일을 꼭 oplog.bson으로 이름을 바꿔 최상위에 놓아야 하나?

아니다. mongorestore는 --oplogReplay 시 <dump-directory>/local/oplog.rs.bson도 자동 탐색 경로로 삼는다. 증분 덤프 디렉터리를 그대로 가리키면 되고, 재배치는 편의일 뿐 필수가 아니다.

--oplogReplay와 --db, --collection을 함께 쓸 수 있나?

함께 쓸 수 없다. --oplogReplay는 --db, --collection, --nsInclude, --nsExclude, --nsFrom, --nsTo와 병행할 수 없다. 복제 셋 멤버의 전체 덤프를 재생하는 것을 전제로 하기 때문이다.

관련 글

댓글 0

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.