운영 중인 MongoDB에서 누군가 db.orders.drop()을 잘못 날리는 사고는 생각보다 흔하다. 새벽에 받아둔 풀 백업만 있으면 그 시점으로는 돌아가지만, 백업 이후 낮 동안 들어온 주문은 통째로 날아간다. 이럴 때 필요한 게 특정 시점 복구, 즉 PITR(point-in-time recovery)다.
결론부터 말하면, mongodump --oplog로 풀 백업을 뜨고 그 뒤로 oplog를 증분으로 계속 캡처해 두면, 사고가 난 순간의 바로 앞 초까지 되돌릴 수 있다. 복원은 풀 덤프를 되살린 다음 mongorestore --oplogReplay --oplogLimit로 oplog를 사고 직전까지만 재생하는 방식이다. 아래에서 풀 백업, 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의 연속 백업을 쓰는 편이 맞지만, 원리를 손으로 한 번 돌려 보면 그 도구들이 뒤에서 뭘 하는지 이해가 빨라진다.