← 블로그

노트북 강제 종료 후 Git bad signature 오류 복구하기

노트북 강제 종료 후 Git bad signature 오류 복구하기

노트북이 갑자기 꺼진 뒤 IntelliJ를 다시 실행했더니 처음 보는 오류가 나타났습니다.

bad signature 0x00000000
index file corrupt

Git index 손상 오류

처음에는 IntelliJ의 캐시나 인덱스가 깨진 문제라고 생각했습니다.

IntelliJ에서 문제가 생기면 보통 Invalidate Caches를 먼저 떠올리게 됩니다. 하지만 이번 오류에서 말하는 index는 IntelliJ의 인덱스가 아니라 Git의 .git/index 파일이었습니다.

노트북이 꺼지는 순간 Git 관련 파일을 쓰고 있었고, 그 과정에서 일부 파일이 손상된 것으로 보였습니다.

Git index 파일 복구 시도

프로젝트 폴더에서 PowerShell을 열고 기존 index 파일의 이름을 변경했습니다.

Rename-Item .git\index index.corrupt
git reset
git status

일반적인 Git index 손상이라면 이 과정에서 인덱스가 다시 만들어지고, 기존 수정 파일이 정상적으로 표시되어야 합니다.

그런데 예상과 다른 결과가 나왔습니다.

No commits yet

Untracked files:
  .gitignore
  README.md
  build.gradle
  src/
  ...

기존에 Git으로 관리되던 모든 파일이 갑자기 Untracked 상태로 표시됐습니다.

처음에는 커밋 기록이 모두 사라진 것처럼 보여서 당황했습니다. 하지만 git reset만으로 기존 커밋 자체가 삭제되지는 않습니다.

단순히 .git/index만 손상된 것이 아니라 현재 브랜치를 가리키는 Git 참조 정보도 함께 손상됐을 가능성이 있었습니다.

Git 저장소 상태 확인

현재 Git이 어떤 저장소와 브랜치를 보고 있는지 확인했습니다.

git rev-parse --show-toplevel
git rev-parse --git-dir
git remote -v
git branch -a
git log --all --oneline --decorate -n 20
Get-Content .git\HEAD

프로젝트 위치와 .git 폴더는 정상적으로 인식하고 있었습니다.

원격 저장소 정보도 남아 있었습니다.

하지만 브랜치와 로그를 확인하는 과정에서 다음 오류가 발생했습니다.

fatal: Failed to resolve HEAD as a valid ref.
fatal: bad object refs/heads/<branch-name>

.git\HEAD 파일에는 현재 브랜치가 다음과 같이 기록되어 있었습니다.

ref: refs/heads/<branch-name>

HEAD는 기존 브랜치를 가리키고 있었지만, 실제 브랜치 참조 파일 안에 저장된 커밋 해시가 손상된 상태였습니다.

즉, Git 저장소 전체가 사라진 것은 아니었습니다. 현재 브랜치가 어떤 커밋을 가리켜야 하는지 알 수 없는 상태에 가까웠습니다.

손상된 브랜치 참조 파일 이동

손상된 브랜치 참조 파일을 백업하기 위해 파일 이름을 변경했습니다.

다만 이 과정에서 주의할 점이 있었습니다.

손상된 파일을 다음과 같이 .git\refs\heads 폴더 안에 남겨두면 안 됩니다.

.git\refs\heads\<branch-name>.corrupt

확장자가 .corrupt여도 Git은 refs/heads 아래에 있는 파일을 브랜치 참조로 인식합니다.

이 상태에서 git fetch를 실행하면 다음과 같은 오류가 발생할 수 있습니다.

fatal: bad object refs/heads/<branch-name>.corrupt
error: remote repository did not send all necessary objects

Git 입장에서는 이름만 바뀐 또 하나의 손상된 브랜치가 존재하는 것처럼 보이기 때문입니다.

따라서 손상된 참조 파일은 .git\refs 폴더 바깥으로 옮겨야 합니다.

New-Item -ItemType Directory -Force .git\recovery | Out-Null

Move-Item `
  .git\refs\heads\<branch-name>.corrupt `
  .git\recovery\<branch-name>.corrupt

아직 파일 이름을 변경하지 않은 상태라면 바로 옮길 수도 있습니다.

New-Item -ItemType Directory -Force .git\recovery | Out-Null

Move-Item `
  .git\refs\heads\<branch-name> `
  .git\recovery\<branch-name>.corrupt

원격 브랜치 기준으로 로컬 브랜치 복구

손상된 참조 파일을 제거한 뒤 원격 저장소의 브랜치 정보를 다시 가져왔습니다.

git fetch origin --prune
git branch -r

목록에서 기존 원격 브랜치가 남아 있는지 확인했습니다.

origin/<branch-name>

원격 브랜치가 정상적으로 존재했기 때문에 로컬 브랜치 참조를 원격 브랜치가 가리키는 커밋으로 다시 생성했습니다.

git update-ref `
  refs/heads/<branch-name> `
  refs/remotes/origin/<branch-name>

git symbolic-ref HEAD refs/heads/<branch-name>
git reset

여기서 실행한 명령은 git reset --hard가 아닙니다.

현재 작업 파일을 삭제하지 않고 정상 커밋을 기준으로 Git index를 다시 구성하기 위한 명령입니다.

복구 후 상태를 확인했습니다.

git branch --show-current
git log --oneline -5
git status

현재 브랜치가 정상적으로 표시됐고, 커밋 로그도 다시 확인할 수 있었습니다.

로컬 브랜치와 원격 브랜치도 같은 최신 커밋을 가리키고 있었습니다.

커밋하지 않았던 수정 파일도 남아 있었다

가장 걱정했던 부분은 노트북이 꺼지기 직전에 작업하던 파일이었습니다.

복구 후 git status를 확인하자 강제 종료 전에 수정했던 파일들이 다시 변경 상태로 표시됐습니다.

Changes not staged for commit:
  modified: path/to/modified-file
  modified: path/to/config-file

실제로 수정은 했지만 아직 커밋하지 않았던 파일들이었습니다.

Git index와 브랜치 참조는 손상됐지만 실제 작업 파일은 그대로 남아 있었습니다.

처음 git status에서 모든 파일이 Untracked로 나타났을 때는 작업 내용과 커밋 기록이 전부 사라진 것처럼 보였습니다.

하지만 브랜치 참조를 정상 커밋에 다시 연결하고 index를 재생성하자 기존 커밋에 포함된 파일과 로컬에서 수정한 파일이 다시 정확하게 구분됐습니다.

전체 복구 순서 정리

같은 오류가 발생한다면 우선 Git index 파일을 백업하고 다시 생성합니다.

Rename-Item .git\index index.corrupt
git reset
git status

정상적으로 수정 파일만 표시된다면 여기서 끝입니다.

하지만 다음과 같이 표시된다면 브랜치 참조 상태도 확인해야 합니다.

No commits yet
Untracked files:
  src/
  build.gradle
  README.md
  ...

현재 HEAD와 브랜치 상태를 확인합니다.

Get-Content .git\HEAD
git branch -a
git log --all --oneline -n 20

다음과 같은 오류가 나온다면 브랜치 참조 파일이 손상됐을 가능성이 있습니다.

fatal: bad object refs/heads/<branch-name>

손상된 참조 파일을 .git\refs 바깥으로 옮깁니다.

New-Item -ItemType Directory -Force .git\recovery | Out-Null

Move-Item `
  .git\refs\heads\<branch-name> `
  .git\recovery\<branch-name>.corrupt

그다음 원격 저장소에서 브랜치 정보를 다시 가져옵니다.

git fetch origin --prune
git branch -r

원격 브랜치가 존재한다면 로컬 브랜치를 다시 연결합니다.

git update-ref `
  refs/heads/<branch-name> `
  refs/remotes/origin/<branch-name>

git symbolic-ref HEAD refs/heads/<branch-name>
git reset

마지막으로 복구 결과를 확인합니다.

git branch --show-current
git log --oneline -5
git status

복구 과정에서 피한 명령어

복구 과정에서는 다음 명령어를 실행하지 않았습니다.

git reset --hard
git clean -fd
git restore .

이 명령어들은 상황에 따라 커밋하지 않은 로컬 수정 내용을 지울 수 있습니다.

특히 노트북 강제 종료 직전에 작업하던 파일이 있다면 Git 저장소 복구보다 현재 작업 파일을 보호하는 것이 먼저입니다.

불안하다면 기존 프로젝트 폴더를 바로 삭제하지 말고, 폴더 이름을 변경하거나 별도 위치에 복사한 뒤 작업하는 것이 안전합니다.

Rename-Item project project_backup

꼭 복구해야 하는 상황인지 먼저 판단하기

이번에는 커밋하지 않은 변경 내용이 남아 있었기 때문에 Git 메타데이터를 직접 복구했습니다.

하지만 로컬에서 수정한 내용이 없거나, 다시 작업해도 될 정도로 사소한 변경이라면 굳이 여기까지 복구할 필요는 없을 것 같습니다.

Git 내부 파일이 어디까지 손상됐는지 확인하고 브랜치 참조를 다시 연결하는 과정이 생각보다 길었기 때문입니다.

솔직히 이번처럼 커밋하지 않은 변경 내용이 남아 있지 않았다면 프로젝트를 원격 저장소에서 새로 받는 편이 더 빨랐을 것 같습니다.

다만 기존 프로젝트 폴더를 바로 삭제하는 것은 권하지 않습니다.

기존 폴더의 이름을 변경해 보관한 뒤 새로 clone한 프로젝트가 정상적으로 실행되는지 확인하고, 로컬 설정 파일이나 필요한 변경 내용이 남아 있지 않은지 비교한 다음 삭제하는 편이 안전합니다.

정리하면 다음과 같습니다.

  • 중요한 로컬 수정사항이 남아 있다면 Git 메타데이터 복구를 시도
  • 로컬 수정사항이 없거나 중요하지 않다면 새로 clone
  • 어느 쪽이든 기존 프로젝트 폴더는 바로 삭제하지 않고 먼저 백업

마무리

처음에는 Git index 파일 하나만 깨진 단순한 문제라고 생각했습니다.

하지만 index를 재생성한 뒤 No commits yet가 나오면서 상황이 생각보다 단순하지 않다는 것을 알게 됐습니다.

노트북이 꺼지는 과정에서 .git/index뿐 아니라 현재 브랜치가 가리키는 커밋 정보를 저장하는 참조 파일도 함께 손상된 것으로 보입니다.

다행히 원격 저장소에는 기존 브랜치와 커밋이 남아 있었습니다.

원격 브랜치를 기준으로 로컬 브랜치 참조를 다시 연결하자 커밋 기록이 복구됐고, 커밋하지 않았던 수정 파일도 그대로 남아 있었습니다.

이번 일을 겪고 나서 느낀 점은 Git 오류가 발생했을 때 바로 프로젝트를 삭제하거나 reset --hard를 실행하면 안 된다는 것입니다.

화면에 모든 파일이 Untracked로 보여도 실제 소스 파일과 Git 객체는 남아 있을 수 있습니다.

먼저 현재 프로젝트 폴더를 보존하고, HEAD와 브랜치 참조가 어디를 가리키고 있는지 확인하는 것이 중요했습니다.

반대로 복구해야 할 로컬 작업이 없다면 너무 오래 붙잡고 있을 필요도 없습니다.

상황에 따라서는 기존 폴더를 백업해 두고 프로젝트를 새로 받는 것이 가장 빠른 해결 방법일 수 있습니다.