운영 중인 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건이 있었다고 하자.
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를 그대로 넣었으니 그 연산은 재생되지 않고, 그 앞의 모든 주문 쓰기까지만 반영된다.
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의 연속 백업을 쓰는 편이 맞지만, 원리를 손으로 한 번 돌려 보면 그 도구들이 뒤에서 뭘 하는지 이해가 빨라진다.