망했다, API 키를 깃허브에 올렸다 - Git 실수 수습하기 (Reset, Revert, Stash)
1. 새벽 3시의 비명
실수로 API Key를 커밋하면 위험하다는 건 많이 알려진 이야기다.
새벽 3시, 졸린 눈을 비비며 마무리 작업을 하는 상황을 상상해보자. "이것만 푸시하고 자야지"라는 안일한 생각이 화근이 된다.
git add .
git commit -m "배포 설정 완료"
git push origin main
터미널에 To https://github.com/... 메시지가 뜨는 걸 보고, 뿌듯한 마음으로 노트북을 덮고 침대에 누웠는데.
그런데 실제로 공개 레포에 키가 올라가면 봇이 즉시 스캔한다는 사례가 있다. 몇 분도 걸리지 않는다고 한다.
AWS Billing Alert와 Security Alert 메일이 동시에 쏟아지는 상황.
[AWS] Action Required: Your AWS account may be compromised...
.env 파일에 들어있던 AWS Secret Access Key를 그대로 커밋해서 깃허브 공개 저장소(Public Repo)에 올린 것이었다.
.gitignore에 .env를 추가하는 걸 깜빡한 거다.
패닉에 빠진 채 급하게 로컬에서 .env 파일을 삭제하고 다시 커밋/푸시를 한다.
rm .env
git add .
git commit -m "키 삭제"
git push origin main
하지만 바보 같은 짓이다. Git은 모든 히스토리를 기억하니까.
저장소의 Commits 탭을 눌러 이전 커밋을 열어보면, 비밀키가 붉은색 글씨(삭제된 내역)로 아주 선명하게 남아있다.
이 상황을 피하려면 Git의 "시간 되돌리기" 기능들을 알아야 한다. AWS 키는 즉시 폐기하고 새 키를 발급받아야 하지만, Git 이력을 어떻게 다루는지 정리해본다.
2. Git의 3가지 공간 이해하기
Git 명령어가 헷갈리는 이유는 "내 코드가 어디에 있는지" 모르기 때문입니다.
Git은 파일을 3가지 물리적/논리적 공간에서 관리합니다.
- Working Directory (작업대): 내가 지금 에디터(VS Code 등)로 보고 있는 실제 파일들.
- Staging Area (포장대):
git add를 하면 올라가는 곳. 택배 보내기 전 상자에 담는 단계(Index).
- Repository (배송 완료):
git commit을 하면 저장되는 곳. 영구적인 기록(HEAD).
graph LR
WD[Working Directory] -- git add --> SA[Staging Area]
SA -- git commit --> Repo[Repository]
Repo -- git reset --> SA
Repo -- git reset --hard --> WD
커밋을 취소한다는 건, Repository에 들어간 짐을 다시 포장대(Staging)나 작업대(Working)로 꺼내오는 것입니다.
3. 타임머신: git reset
아직 git push를 하기 전이라면(로컬에서만 실수했다면), git reset은 완벽한 타임머신입니다.
이 명령어는 시간을 과거로 되돌립니다. 뒤에 붙는 옵션에 따라 "어디까지 되돌릴지"가 결정됩니다. 게임의 세이브 포인트 불러오기라고 생각하면 이해가 빠릅니다.
3.1 git reset --soft (가장 안전, 추천)
"커밋만 취소하고, 작성한 코드는 살려줘."
# HEAD~1: 현재(HEAD)에서 1칸 뒤로 간다.
git reset --soft HEAD~1
- 효과: 방금 한 커밋은 히스토리에서 사라지지만, 파일들은 Staging Area에 그대로 남아있습니다. (
git add 된 초록색 상태)
- 활용: "아 맞다, 파일 하나 빼먹었다!" 혹은 "커밋 메시지에 오타 났다!" 할 때 씁니다. 파일을 더 추가하거나 수정한 뒤 다시
git commit 하면 됩니다. 가장 자주 쓰는 옵션입니다.
3.2 git reset --mixed (기본값)
"커밋 취소하고, git add도 취소해 줘."
git reset HEAD~1
# 또는 옵션 생략 가능
git reset HEAD~1
- 효과: 커밋도 사라지고, 파일들도 Staging Area에서 내려옵니다. (
Unstaged 상태). 하지만 파일 내용은 그대로입니다. 작업 내용은 날아가지 않습니다.
- 활용: 실수로 이상한 파일까지 몽땅
git add . 해버렸을 때 유용합니다.
3.3 git reset --hard (파괴신)
"그냥 다 없던 일로 해. 싹 다 지워."
git reset --hard HEAD~1
- 효과: 커밋도 사라지고, 작성하던 코드도 다 사라집니다. 완전히 그 시점으로 돌아갑니다.
- 주의: 복구하기 매우 힘듭니다. 정말 확실할 때만 쓰세요. 저도 홧김에 "에라이 모르겠다" 하고 이거 썼다가, 3시간 동안 짠 코드를 날리고 울 뻔했습니다.
4. 잠깐 딴짓해야 할 때: git stash
실수 수습은 아니지만, 개발하다 보면 이런 상황이 자주 옵니다.
"한창 기능 A를 개발 중인데, 갑자기 긴급 버그 B를 고쳐달라고 함."
지금 코드는 에러 투성이라 커밋할 수도 없고, 그렇다고 버그 수정을 위해 브랜치를 바꾸면 작업 중인 파일들이 충돌(Conflict)을 일으킵니다.
이때 쓰는 것이 git stash (임시 저장소)입니다.
# 1. 작업 중인 코드 임시 저장 (서랍에 넣어두기)
git stash save "기능 A 개발 중, API 연결 전"
# 2. 이제 코드가 깨끗해졌으니 버그 수정 브랜치로 이동
git checkout hotfix/bug-b
# ... 버그 수정 및 푸시 ...
# 3. 다시 원래 브랜치로 복귀
git checkout feature-a
# 4. 아까 넣어둔 코드 다시 꺼내기
git stash pop
Stash는 정말 유용합니다. 저는 퇴근할 때 커밋하기 애매한 코드들은 다 stash에 넣어두고, 다음 날 출근해서 pop으로 꺼냅니다.
5. 이미 배송(Push)해버렸다면? : git revert
문제는 이미 git push를 해서 원격 저장소(Remote)에 올라갔을 때입니다. (제 AWS 키 사건처럼요)
이때 내 로컬에서 git reset으로 과거를 지워버리고 다시 푸시하려고 하면, Git은 거부합니다.
Error: failed to push some refs to '...'. Updates were rejected because the tip of your current branch is behind its remote counterpart.
(너의 로컬 역사가 원격보다 뒤처져 있어!)
물론 git push --force로 강제 덮어쓰기를 할 수 있습니다. 하지만 혼자 쓰는 브랜치가 아니라면?
동료들이 git pull을 받는 순간 헬게이트가 열립니다. 팀 프로젝트에서 --force는 선전포고와 같습니다. 다른 개발자의 커밋을 덮어써서 날려버릴 수 있기 때문입니다.
이때는 git revert를 써야 합니다.
git revert HEAD
이건 "시간을 되돌리는 것"이 아니라, "실수를 상쇄하는 전표를 끊는 것"입니다.
A라는 커밋에서 코드를 추가했다면, A'라는 커밋을 새로 만들어서 그 코드를 삭제합니다. (+1을 -1로 상쇄)
결과적으로 코드는 사라지지만, "내가 실수했고, 그걸 취소했다"는 기록은 남습니다.
쪽팔리지만, 협업을 위해서는 가장 안전하고 정중한 방법입니다. 히스토리의 선형성(Linearity)을 해치지 않으니까요.
6. 역사를 예쁘게 다듬기: git rebase -i
가끔은 실수를 감추고 싶을 때가 있습니다.
예를 들어, 오타 수정만 5번 해서 커밋 히스토리가 지저분할 때.
Commit 5: Fix typo again
Commit 4: Fix typo
Commit 3: Fix bug
Commit 2: Oops missed one file
Commit 1: Add new feature
이걸 남들에게 보여주기 부끄럽다면, Interactive Rebase를 사용해서 커밋을 하나로 합칠(Squash) 수 있습니다.
# 최근 5개의 커밋을 다듬겠다
git rebase -i HEAD~5
그러면 에디터가 열리는데, 여기서 pick 이라고 되어 있는 부분을 squash (또는 s)로 바꾸면, 해당 커밋이 이전 커밋과 합쳐집니다.
pick a1b2c Add new feature
squash d3e4f Oops missed one file
squash g5h6i Fix bug
squash j7k8l Fix typo
squash m9n0o Fix typo again
이렇게 저장하면 5개의 커밋이 깔끔하게 "Add new feature (완벽함)" 커밋 하나로 병합됩니다. 단, 이 작업도 push 하기 전에만 해야 합니다!
7. 흑역사 지우개: git reflog
만약 실수로 git reset --hard를 써서 코드를 다 날려먹었다면? 인생이 끝난 걸까요?
아니요. Git에는 reflog라는 블랙박스가 있습니다.
git reflog
이걸 치면 내가 수행한 모든 명령(커밋, 리셋, 체크아웃 등)이 순서대로 나옵니다. 심지어 삭제한 커밋의 ID도 나옵니다.
a1b2c3d HEAD@{0}: reset: moving to HEAD~1
d4e5f6g HEAD@{1}: commit: 배포 설정 완료 (AWS 키 포함됨)
여기서 d4e5f6g가 방금 지워버린(reset으로 되돌린) 커밋입니다.
Git에서 커밋은 reset을 해도 즉시 삭제되지 않고, '고아(Dangling) 객체'로 한동안 남아있습니다.
# 타임머신 타고 다시 그 시점으로 이동!
git reset --hard d4e5f6g
이렇게 하면 마법처럼 날아간 코드가 돌아옵니다. Git은 생각보다 훨씬 안전하고 끈질긴 도구였습니다. 단지 제가 사용법을 몰랐을 뿐이죠.
8. 체리픽(Cherry-pick) - 원하는 것만 쏙 빼오기
"다른 브랜치에서 작업한 커밋 딱 하나만 가져오고 싶어!"
전체 병합(Merge)은 부담스러울 때, 체리픽을 씁니다.
# 1. 깃 로그에서 원하는 커밋 ID 확인 (예: a1b2c3d)
# 2. 내 브랜치로 가져오기
git cherry-pick a1b2c3d
마치 체리 케이크에서 맛있는 체리만 쏙 골라 먹듯이, 특정 변경사항만 내 브랜치에 적용할 수 있습니다.
9. 버그 찾기의 신: git bisect
"어제까진 잘 됐는데 오늘 갑자기 안 되네? 도대체 언제부터 망가진 거야?"
커밋이 수십 개 쌓여있다면, 범인을 찾기 위해 하나하나 체크아웃해보는 건 고문입니다.
이때 이진 탐색(Binary Search) 알고리즘으로 범인 커밋을 찾아주는 도구가 bisect입니다.
git bisect start
git bisect bad # 지금은 망가졌음(Bad)
git bisect good HEAD~20 # 20개 전 커밋은 확실히 정상이었음(Good)
그러면 Git이 중간 지점으로 체크아웃해줍니다. 테스트해보고 git bisect good 또는 git bisect bad라고만 입력하면, 범위를 좁혀가며 순식간에 "범인은 바로 이 커밋입니다!"라고 알려줍니다.
10. 그래서 내 API 키는? (히스토리 영구 삭제)
제 경우처럼 API 키가 히스토리에 남았다면 reset이나 revert로는 부족합니다. 해커들은 히스토리 전체를 스캔하니까요.
히스토리 자체를 조작해서 특정 파일의 흔적을 지구상에서 영구적으로 없애야 합니다.
git filter-branch 같은 명령어가 있지만 너무 어렵고 느립니다. BFG Repo-Cleaner라는 툴을 쓰는 게 가장 쉽고 빠릅니다.
# 1. BFG 툴로 .env 파일이 포함된 모든 역사 삭제 (로컬에서)
bfg --delete-files .env
# 2. 찌꺼기 정리 (Garbage Collection)
git reflog expire --expire=now --all
git gc --prune=now --aggressive
# 3. 강제로 덮어쓰기 (이때는 어쩔 수 없이 force 써야 함, 팀원들에게 공지 필수!)
git push --force
하지만, 가장 현실적이고 안전한 해결책은 따로 있습니다.
-
AWS 콘솔에 들어가서 유출된 키를 비활성화(Deactivate) 및 삭제한다.
- 새 키를 발급받는다.
- Git 수습하느라 시간 쓰지 마라. 그 사이에 과금된다.
키가 유출되면 그 키는 이미 "공공재"입니다. Git 히스토리를 지워도 누군가는 이미 가져갔을 겁니다. 키를 바꾸는 게 유일한 정답입니다.
9. 한 줄 요약
로컬에서 실수하면 git reset --soft로 조용히 수습하고, 잠깐 딴짓할 땐 git stash를 쓰고, 이미 푸시했다면 git revert로 정중하게 사과하고, 비밀번호를 올렸다면 기도하지 말고 AWS 키부터 삭제해라.