Skip to content
Published on

Git 되돌리기 완전 가이드: 복구되는 것과 영원히 사라지는 것

공유하기
Authors

들어가며

Git에서 무언가를 잘못했을 때 가장 먼저 알아야 할 것은 명령이 아닙니다. 지금 잃어버린 것이 아직 어딘가에 남아 있는지입니다. 남아 있다면 침착하게 꺼내면 되고, 남아 있지 않다면 다른 사람의 사본을 찾아야 합니다. 이 판단을 못 하면 복구 가능한 상황에서 시간을 낭비하고, 복구 불가능한 상황에서 헛된 시도를 반복합니다.

이 블로그에는 git 되돌리기 — 상황별로 고르는 restore, reset, revert, reflog가 이미 있습니다. 그 글은 혼자 작업하는 로컬 흐름을 상황별로 안내하는 실전 치트시트입니다. 이 글은 그 옆에 놓을 레퍼런스를 목표로 합니다. 복구 가능성의 경계를 만료 정책과 함께 수치로 정리하고, 강제 푸시 사고나 워크트리·서브모듈처럼 팀 환경에서만 생기는 상황까지 다룹니다. 치트시트가 "지금 뭘 칠까"에 답한다면, 이 글은 "이건 되살릴 수 있는가"에 답합니다.

기준 버전은 Git 2.40 이상입니다. git restoregit switch는 2.23에서 도입되어 오랫동안 실험적 표시가 붙어 있었으므로, 오래된 환경에서는 git checkout을 써야 할 수 있습니다.


1. 복구 가능성 지도 — 이 글의 핵심 한 장

먼저 결론입니다. 아래 표가 이 글에서 가장 중요합니다.

잃어버린 것복구 가능성수단
커밋된 내용매우 높음reflog, fsck
스테이징(git add)했으나 미커밋가능git fsck --lost-found의 blob
수정만 하고 add도 커밋도 안 한 변경불가능에디터 로컬 히스토리, 백업만
stash 했다가 drop한 것가능(만료 전)reflog, fsck
삭제한 브랜치매우 높음reflog, fsck
rebase로 사라진 커밋매우 높음ORIG_HEAD, reflog
강제 푸시로 원격에서 사라진 커밋조건부누군가의 로컬 사본, 서버 reflog
gc --prune=now 이후의 도달 불가 객체불가능없음
추적되지 않는 파일을 git clean불가능없음

굵게 표시한 세 줄이 경계선입니다. Git이 지켜 주는 범위는 "한 번이라도 객체 데이터베이스에 기록된 것"입니다. 기록된 적 없는 변경은 Git의 책임 밖입니다.

그래서 실무 규칙 하나가 나옵니다. 위험한 작업을 하기 전에 일단 커밋하거나 stash 하세요. 임시 커밋 하나가 복구 가능성을 "불가능"에서 "매우 높음"으로 바꿉니다.


2. 세 개의 영역과 그것을 옮기는 명령

Git은 세 곳에 상태를 둡니다. 작업 트리, 인덱스(스테이징 영역), 그리고 HEAD가 가리키는 커밋입니다. 되돌리기 명령은 결국 "어느 영역을 어디에 맞출 것인가"입니다.

git status
git status --short --branch
git diff
git diff --staged
  • git diff는 작업 트리와 인덱스의 차이입니다.
  • git diff --staged는 인덱스와 HEAD의 차이입니다.

이 두 명령의 차이를 명확히 알면 git status의 안내문이 정확히 무슨 뜻인지 이해됩니다.

작업 트리와 인덱스를 되돌리는 현대적 명령은 git restore입니다.

git restore path/to/file
git restore --staged path/to/file
git restore --staged --worktree path/to/file
git restore --source=HEAD~2 path/to/file
  • 인자 없이 git restore <파일>작업 트리의 수정을 버립니다. 커밋도 add도 하지 않은 변경이라면 이 시점에 영원히 사라집니다.
  • --staged는 인덱스만 되돌립니다. 작업 트리 수정은 남습니다.
  • --source는 특정 커밋의 내용으로 가져옵니다.

파괴적 명령 경고: git restore <파일>은 확인을 묻지 않고 되돌립니다. 되살릴 방법이 없으므로, 버리려는 내용이 정말 필요 없는지 git diff로 먼저 확인하세요.


3. reset — 세 가지 모드의 실제 차이

git reset은 HEAD를 옮깁니다. 옵션은 인덱스와 작업 트리를 어디까지 함께 옮길지를 정합니다.

git reset --soft HEAD~1
git reset --mixed HEAD~1
git reset --hard HEAD~1
모드HEAD인덱스작업 트리언제 쓰나
--soft이동유지유지커밋만 취소하고 다시 커밋할 때
--mixed이동이동유지기본값. add까지 취소할 때
--hard이동이동이동전부 버릴 때

파괴적 명령 경고: git reset --hard는 작업 트리의 미커밋 변경을 삭제하며, 그 변경은 복구할 수 없습니다. 커밋 자체는 reflog로 되살릴 수 있지만, 커밋된 적 없는 수정은 아닙니다. 이 구분이 매우 중요합니다.

안전한 습관은 실행 전에 반드시 확인하는 것입니다.

git status
git stash push -m 'before hard reset 2026-08-15'
git reset --hard HEAD~1

git reset --hard 직후 "아, 그 커밋이 필요했다"면 reflog가 답입니다.

git reflog
git reset --hard HEAD@{1}

ORIG_HEAD도 유용합니다. reset, merge, rebase 같은 명령은 실행 직전의 HEAD를 여기에 저장합니다.

git reset --hard ORIG_HEAD

4. revert — 이미 공유된 역사를 다루는 유일한 방법

푸시된 커밋을 없애려고 reset 후 강제 푸시를 하면, 그 브랜치를 이미 받아 간 모든 사람의 저장소가 어긋납니다. 공유된 역사에는 revert를 씁니다. 역사를 지우는 대신 반대 내용을 담은 새 커밋을 추가하는 방식입니다.

git revert 3f2a1b9
git revert --no-commit 3f2a1b9
git revert HEAD~3..HEAD
git revert --abort

머지 커밋을 되돌릴 때는 어느 쪽을 남길지 지정해야 합니다.

git revert -m 1 8c4d2e0

-m 1은 첫 번째 부모(대개 머지를 받은 쪽, 즉 main)를 기준으로 삼겠다는 뜻입니다. 부모 번호는 다음으로 확인합니다.

git log --merges -1 --format='%H %P'

머지 revert에는 후속 함정이 있습니다. 머지를 되돌린 뒤 같은 브랜치를 다시 머지하면, Git은 이미 머지된 것으로 판단해 되돌린 변경을 다시 가져오지 않습니다. 이 경우 "revert의 revert"를 하거나 브랜치를 다시 만들어야 합니다. 팀 작업에서 흔히 겪는 혼란이므로, 머지 revert를 할 때는 그 브랜치를 나중에 어떻게 다시 넣을지 계획을 함께 세우세요.


5. reflog — 어디까지, 언제까지 남는가

reflog는 참조가 움직인 기록입니다. 커밋 해시를 몰라도 "두 단계 전 HEAD"를 지칭할 수 있게 해 줍니다.

git reflog
git reflog show main
git reflog --date=iso | head -20
git log -g --oneline

여기서 반드시 알아야 할 세 가지 성질이 있습니다.

첫째, reflog는 로컬입니다. 문서가 명시하듯 reflog는 저장소별로 존재하며 푸시되지 않습니다. 다른 사람의 저장소에는 여러분의 reflog가 없고, 여러분이 새로 clone하면 reflog는 비어 있습니다. clone 직후에는 안전망이 없다는 뜻입니다.

둘째, 만료 기한이 있습니다. 기본값은 명확합니다.

설정기본값대상
gc.reflogExpire90일현재 팁에서 도달 가능한 항목
gc.reflogExpireUnreachable30일도달 불가능한 항목

즉 rebase나 reset으로 버려진 커밋의 reflog 항목은 30일이 지나면 정리 대상이 됩니다. 중요한 저장소라면 늘릴 수 있습니다.

git config gc.reflogExpire '365 days'
git config gc.reflogExpireUnreachable '180 days'
git config --get gc.reflogExpire

셋째, reflog 항목이 남아 있어도 객체가 지워졌으면 소용없습니다. 다음 절에서 이어집니다.

브랜치를 실수로 지웠을 때의 복구는 이렇습니다.

git reflog --all | grep -i 'feature/payment'
git branch feature/payment 9a1c3f7

git reflog expire는 직접 쓸 일이 드물지만, 무엇을 지울지 미리 볼 수 있습니다.

git reflog expire --dry-run --expire-unreachable=30.days --all

--dry-run(-n)은 실제로 지우지 않고 대상만 보여 줍니다.


6. gc — 객체가 실제로 삭제되는 시점

Git은 도달 불가능한 객체를 즉시 지우지 않습니다. git gc가 정리하되 유예 기간을 둡니다.

설정기본값의미
gc.pruneExpire2.weeks.ago이보다 오래된 느슨한 객체를 정리
gc.auto6700느슨한 객체가 이 정도를 넘으면 자동 실행

문서에 따르면 git gc는 기본적으로 2주보다 오래된 도달 불가 객체를 제거합니다. 유예 기간은 다른 프로세스가 동시에 쓰는 중일 때 손상을 막기 위한 장치이기도 합니다.

절대 하지 말아야 할 것이 있습니다.

# 위험: 복구 여지를 즉시 없앤다
git gc --prune=now
git reflog expire --expire-unreachable=now --all

파괴적 명령 경고: 이 두 명령을 함께 실행하면 도달 불가능한 객체가 즉시 삭제되어 어떤 방법으로도 복구할 수 없습니다. 저장소 용량을 줄이려고 검색해서 나온 명령을 그대로 붙여 넣는 사고가 흔합니다. 사고 직후 복구를 시도하는 중이라면 gc를 절대 실행하지 마세요. 문서 역시 --prune=now가 동시 쓰기 상황에서 손상 위험을 높인다고 경고합니다.

반대로, 복구 여지를 늘리려면 다음처럼 설정합니다.

git config gc.pruneExpire '90 days'
git config gc.auto 0

gc.auto 0은 자동 gc를 끕니다. 큰 저장소에서 예기치 않은 시점에 gc가 도는 것을 막고 싶을 때 씁니다. 대신 주기적으로 수동 실행이 필요합니다.


7. fsck — reflog에도 없을 때 마지막 그물

reflog 항목이 만료되었지만 객체는 아직 남아 있는 상태가 있습니다. 이때 git fsck로 고아 객체를 찾습니다.

git fsck --unreachable
git fsck --dangling
git fsck --lost-found
git fsck --no-reflogs --unreachable --name-objects
  • --unreachable은 어떤 참조에서도 도달할 수 없는 객체를 나열합니다.
  • --dangling은 직접 참조되지 않는 객체를 보여 줍니다(기본 동작).
  • --lost-found찾은 객체를 파일로 꺼내 줍니다. 문서에 따르면 커밋은 .git/lost-found/commit/, 그 외는 .git/lost-found/other/에 기록되며, blob은 이름 대신 내용이 파일로 저장됩니다.
  • --no-reflogs는 reflog로만 참조되는 커밋을 도달 가능으로 치지 않습니다. "예전에 브랜치였지만 지금은 아닌" 커밋을 찾을 때 씁니다.
  • --name-objects는 그 객체에 어떻게 도달할 수 있는지 경로를 함께 보여 줍니다.

찾은 커밋의 내용을 확인하고 살립니다.

git fsck --lost-found
ls .git/lost-found/commit/
git show --stat 4b7e91c
git branch recovered-4b7e91c 4b7e91c

커밋하지 않고 git add만 한 상태에서 reset --hard를 했다면 blob은 남아 있습니다. 파일 이름은 잃어버렸지만 내용은 살릴 수 있습니다.

git fsck --lost-found
grep -rl 'function calculateTax' .git/lost-found/other/ | head

내용의 특징적인 문자열로 찾는 것이 현실적인 방법입니다.

stash도 같은 원리로 되살립니다. git stash drop이나 git stash clear는 스택 항목만 지울 뿐 객체는 남기므로, 만료 전이라면 복구할 수 있습니다.

git fsck --unreachable | grep commit
git show --stat 7d3e5a2
git stash apply 7d3e5a2

stash는 내부적으로 커밋 객체로 저장되기 때문에 git stash apply에 해시를 직접 넘길 수 있습니다. 다만 여기서도 시간 제한은 같습니다. gc가 이미 정리했다면 어떤 명령으로도 되살릴 수 없습니다.

정리하면 fsck는 "객체는 남아 있는데 그것을 가리키는 이름이 사라진 상황" 을 위한 도구입니다. 객체 자체가 삭제된 뒤에는 아무 역할도 하지 못합니다. 그래서 사고를 인지한 직후에 해야 할 첫 번째 행동은 복구 명령을 찾는 것이 아니라, 그 저장소에서 gc가 돌지 않도록 막는 것입니다.


8. 강제 푸시 사고와 팀 상황

원격 브랜치가 강제 푸시로 덮였을 때 복구 경로는 세 갈래입니다.

경로 1 — 덮어쓴 사람의 로컬 reflog. 가장 빠릅니다.

git reflog show origin/main
git log --oneline origin/main@{1}

경로 2 — 다른 사람의 로컬 사본. 아직 fetch하지 않은 사람이 있다면 그 사람의 원격 추적 브랜치에 이전 상태가 남아 있습니다. 사고를 인지한 즉시 팀 채널에 "지금 fetch/pull 하지 마세요"라고 알리는 것이 실질적으로 가장 중요한 조치입니다.

경로 3 — 서버 쪽 기록. GitHub, GitLab 등 대부분의 호스팅은 이벤트 기록에 이전 커밋 해시를 남깁니다. 해시만 알면 되살릴 수 있습니다.

git fetch origin 9a1c3f7
git branch rescue 9a1c3f7

애초에 사고를 줄이는 설정도 있습니다.

git push --force-with-lease origin main
git push --force-with-lease=main:9a1c3f7 origin main

--force-with-lease는 원격이 내가 마지막으로 본 상태 그대로일 때만 덮어씁니다. 그 사이에 다른 사람이 푸시했다면 거부됩니다. 강제 푸시가 필요한 상황이라면 항상 이쪽을 쓰세요. 다만 fetch를 먼저 해 버리면 lease 기준이 갱신되어 보호 효과가 약해진다는 점은 알아 두어야 합니다.

워크트리와 서브모듈은 별도로 신경 써야 합니다.

git worktree list
git worktree remove ../feature-wt
git submodule status
git submodule update --init --recursive

워크트리를 강제 제거하면 그 안의 미커밋 변경은 사라집니다. 서브모듈은 부모 저장소가 특정 커밋을 가리킬 뿐이므로, 서브모듈 저장소에서 그 커밋이 사라지면 부모의 참조는 살아 있어도 내용을 가져올 수 없습니다. 서브모듈 쪽 브랜치를 지우기 전에 부모가 참조하는 커밋인지 확인하세요.

마지막으로, Git이 절대 지켜 주지 않는 것을 다시 강조합니다.

git clean -n -d
git clean -f -d

파괴적 명령 경고: git clean -f -d는 추적되지 않는 파일과 디렉터리를 삭제하며 복구 수단이 전혀 없습니다. 반드시 -n(dry run)으로 목록을 먼저 확인하세요. 설정 파일이나 로컬 데이터가 목록에 있으면 실행하지 말고 제외 패턴을 지정해야 합니다.


퀴즈: 실력을 확인해 보세요

퀴즈 1: git reset --hard로 되돌렸습니다. 어떤 것이 복구되고 어떤 것이 복구되지 않나요?

정답: 커밋된 내용은 reflog로 복구되지만, 커밋된 적 없는 작업 트리 수정은 복구되지 않습니다

설명: reset은 HEAD를 옮길 뿐 객체를 지우지 않으므로, 버려진 커밋은 여전히 객체 데이터베이스에 있습니다.

git reflog
git reset --hard HEAD@{1}

반면 편집만 하고 add도 커밋도 하지 않은 변경은 Git이 한 번도 본 적이 없습니다. 이 경우 남은 희망은 에디터의 로컬 히스토리나 파일시스템 스냅샷뿐입니다. 그래서 위험한 작업 전에는 임시 커밋이나 stash를 먼저 하는 것이 규칙입니다.

퀴즈 2: 3주 전 rebase에서 사라진 커밋을 찾으려 합니다. reflog에 없다면 다음 수단은?

정답: git fsck로 도달 불가 객체를 찾습니다. 단 gc가 이미 정리했다면 불가능합니다

설명: 도달 불가 reflog 항목의 기본 만료는 30일이므로 3주면 아직 남아 있을 가능성이 큽니다. 없다면 객체 자체를 찾습니다.

git fsck --lost-found --no-reflogs --unreachable
ls .git/lost-found/commit/
git show --stat <해시>

gc.pruneExpire의 기본값은 2주이므로, 그 사이에 gc가 실행되었다면 이미 삭제되었을 수 있습니다. 이것이 중요한 저장소에서 만료 설정을 늘려 두는 이유입니다.

퀴즈 3: 동료가 main에 강제 푸시를 했습니다. 가장 먼저 할 행동은?

정답: 팀에 fetch와 pull을 중단하라고 알리는 것입니다

설명: 기술적 복구보다 이 커뮤니케이션이 먼저입니다. 아직 fetch하지 않은 사람의 저장소에는 이전 상태의 origin/main이 남아 있으며, 그것이 가장 확실한 사본입니다. 모두가 fetch해 버리면 그 사본이 사라집니다.

이후 복구는 다음 순서입니다.

git reflog show origin/main
git log --oneline origin/main@{1}
git branch rescue origin/main@{1}

재발 방지는 브랜치 보호 규칙과 --force-with-lease 사용 규칙입니다.

퀴즈 4: 저장소가 크다는 이유로 검색 결과에서 본 gc 명령을 실행하려 합니다. 어떤 조합이 위험한가요?

정답: reflog를 즉시 만료시키고 gc --prune=now를 함께 실행하는 조합입니다

설명: 다음 조합은 도달 불가능한 모든 객체를 즉시 삭제하며 복구 수단을 남기지 않습니다.

git reflog expire --expire-unreachable=now --all
git gc --prune=now

특히 복구 작업 중에는 절대 실행하면 안 됩니다. 용량이 목적이라면 무엇이 용량을 차지하는지 먼저 확인하는 편이 낫습니다.

git count-objects -vH

대용량 파일이 히스토리에 들어간 것이 원인이라면 gc가 아니라 히스토리 재작성이 필요하며, 그것은 팀 전체가 합의해야 하는 작업입니다.

퀴즈 5: 머지 커밋을 revert한 뒤 같은 기능 브랜치를 다시 머지했는데 변경이 들어오지 않습니다. 왜일까요?

정답: Git은 그 브랜치를 이미 머지된 것으로 보기 때문입니다

설명: revert는 역사를 지우지 않고 반대 변경을 추가합니다. 머지 커밋 자체는 히스토리에 남아 있으므로, 다시 머지해도 "이미 합쳐진 커밋"으로 판단해 아무것도 가져오지 않습니다. 해결책은 두 가지입니다.

git revert <revert 커밋 해시>

되돌리기를 다시 되돌리거나, 기능 브랜치를 새 브랜치로 다시 만들어 커밋을 옮겨 담습니다.

git switch -c feature/payment-v2 origin/main
git cherry-pick <커밋 범위>
퀴즈 6: 방금 clone한 저장소에서 reset --hard를 실행했습니다. 안전망 측면에서 무엇이 다를까요?

정답: clone 직후에는 reflog에 이전 상태가 거의 없어 되돌릴 지점이 부족합니다

설명: reflog는 저장소별 로컬 기록이며 clone으로 전달되지 않습니다. 갓 clone한 저장소의 reflog에는 clone 항목 하나뿐입니다. 다만 원격 추적 브랜치가 그대로 있으므로 원격 상태로는 언제든 돌아갈 수 있습니다.

git reflog
git reset --hard origin/main

위험한 것은 clone 직후 로컬에서만 만든 작업입니다. 그 작업은 원격에도 없고 reflog 이력도 짧으므로, 중간중간 커밋해 두는 습관이 유일한 방어입니다.


마치며

Git 되돌리기에서 외워야 할 것은 명령 목록이 아니라 세 개의 시간입니다. 도달 불가 reflog 항목은 기본 30일, 도달 가능 항목은 90일, 느슨한 객체는 gc 기준 2주입니다. 이 숫자를 알면 "3주 전 것을 찾을 수 있을까"라는 질문에 근거를 가지고 답할 수 있습니다.

그리고 경계선 하나를 기억하세요. Git은 자신이 본 적 있는 것만 지켜 줍니다. 커밋되지 않은 수정, 추적되지 않는 파일, clean으로 지운 것은 Git의 보호 밖입니다.

실무 습관으로 바꾸면 이렇게 됩니다. 위험한 작업 전에는 임시 커밋을 만들고, 강제 푸시는 반드시 --force-with-lease로 하고, 중요한 저장소에서는 만료 설정을 늘려 둡니다. 이 세 가지면 대부분의 사고가 "복구 가능" 쪽으로 넘어옵니다.


참고 자료


이어서 읽기