들어가며
SCG에서 개발했던 정보통신/소프트웨어융합대학 일반대학원 학위논문 심사 시스템(통칭 졸논 시스템)은 깃허브로 관리한다. 해당 레포를 포트폴리오 목적으로 Private에서 Public으로 바꾸기 위해 검토를 하던 중 과거 커밋 히스토리에 .env 파일이 노출된 것을 발견했다. 해당 파일은 프로젝트 초기(2023년 11월 22일)에 처음 추가됐고, 2024년 3월 6일 문제를 인식하고 환경변수가 노출된 파일을 삭제했다.
하지만 파일을 삭제했다고 해서 .env 파일이 완전히 제거되는 것이 아니다.

레포를 퍼블릭으로 전환하기 위해서는 약 1년 전에 남아있던 히스토리를 제거해야 했다.
이 글에서 다루는 내용
- git filter-repo로 민감한 파일을 히스토리에서 완전히 제거하는 방법
- 깃허브 환경의 어려움과 현실적인 대안
- 실제 작업 과정과 검증 방법
대상 독자
- .env, API 키 등 민감한 파일이 GitHub에 올라간 경험이 있는 개발자
- Git 기본 명령어(commit, push, branch)에 익숙한 사람
- GitHub 기본 기능(PR, 커밋 히스토리 조회 등)에 익숙한 사람
해결이 까다로운 이유
문제 자체는 단순하다. .env가 포함된 커밋을 히스토리에서 제거하면 된다.
만약 .env가 최근 커밋에만 있었다면 git commit --amend나 git rebase -i로 간단히 해결할 수 있다. 하지만 이번 상황은 달랐다.
- .env 최초 추가: 2023년 11월 22일
- .env 삭제: 2024년 3월 6일
- 그 위에 쌓인 커밋: 약 1년치
Git 커밋은 부모 커밋의 해시를 포함한다. .env가 추가된 커밋을 수정하면 해당 커밋의 해시가 변경되고, 그 커밋을 부모로 참조하는 모든 후속 커밋의 해시도 연쇄적으로 변경된다.
즉, 1년 전 커밋 하나를 고치려면 그 이후 모든 커밋을 재작성해야 한다.
단순히 git rebase -i로 처리할 수 있는 범위가 아니었다. 1년치의 커밋을 일괄 재작성할 수 있는 도구가 필요했다.
왜 커밋의 해시가 연속적으로 변하게 되는지 조금 더 자세한 내막이 궁금하다면 아래 영상을 참고하자!ㅎㅎ
https://youtu.be/K45jv8DbB88?si=s36Xo-A2fipV6UTW
해결 방법: 히스토리 재작성
해결책은 .env가 포함된 모든 커밋을 재작성(rewrite)하는 것이다. 마치 처음부터 .env가 없었던 것처럼 히스토리를 다시 쓴다.
도구 선택
| 도구 | 특징 | 권장 |
| git filter-branch | Git 내장, 느림, deprecated | ❌ |
| BFG Repo-Cleaner | 빠름, 간단함 | ⭕ |
| git filter-repo | 빠름, 유연함, Git 공식 권장 | ✅ |
git filter-repo를 사용했다. Git 공식 문서에서 filter-branch 대신 권장하는 도구다.
현실적인 선택: 새 레포 vs 기존 레포
git filter-repo로 히스토리를 재작성한 후 두 가지 선택지가 있다.
방법 1: 기존 레포에 force push
git push --force --all변경된 히스토리를 기존 레포지터리에 강제로 push하는 방법이다. 가장 먼저 떠오르는 방법이지만, 몇가지 문제가 있다.
GitHub 캐시 문제
커밋 히스토리가 바뀌어도 GitHub에는 여전히 이전 커밋이 남아있다. 관련 PR이나 기존 커밋 해시를 URL에 직접 입력하면 .env 파일을 그대로 볼 수 있다. 로컬에서 히스토리를 지워도 GitHub 시스템에서 완전히 삭제되지 않는다.
GitHub 캐시는 개발자가 직접 삭제할 수 없다. GitHub Support에 캐시 삭제를 요청해야 하고, GitHub 측에서 민감한 데이터인지 검토한 후 삭제하는 과정을 거쳐야 한다.
PR 깨짐 문제
GitHub 캐시를 삭제하면 PR의 "Files changed" 탭이 깨질 수 있다. 캐시가 없으면 GitHub는 실제 Git 히스토리에서 diff를 계산해야 하는데, 커밋 해시가 전부 바뀌었기 때문에 해당 PR의 커밋을 찾을 수 없다.
2023년 11월 22일 이후 생성된 모든 PR의 diff가 조회 불가능해질 수 있다. 개발 과정에서 과거 PR을 자주 참고하기 때문에 무시할 수 없는 문제다.
팀원 전원 fresh clone 필요
팀원들은 이미 기존 히스토리를 가진 로컬 저장소를 갖고 있다. force push 이후 로컬과 리모트의 히스토리가 달라지면서, 기존 로컬에서 push를 시도할 경우 거부당한다. 모든 팀원이 저장소를 새로 clone해야 한다.
더 위험한 시나리오도 있다. 팀원이 상황을 인지하지 못하고 기존 로컬에서 git push --force를 실행하면, .env가 포함된 히스토리가 다시 복구된다.
방법 2: 새 레포에 push (채택)
기존 레포에 force push하는 방식은 문제가 많다. 특히 PR 히스토리가 깨지는 것이 치명적이라고 판단했다.
최종 선택한 방식은 다음과 같다:
- 기존 레포는 private 상태로 유지
- .env를 제거한 히스토리를 새 레포에 push
- 기존 레포는 archive 처리하여 필요할 때 참고
이 방식의 장점:
- 새 레포에는 GitHub 캐시가 없으므로 삭제 요청 불필요
- 기존 레포의 PR 히스토리가 그대로 보존됨
- 기존 레포는 더 이상 사용하지 않으므로 팀원이 실수로 push해도 문제없음
단점은 레포가 2개가 되어 초기에 혼동이 있을 수 있다는 점이다. 하지만 장점이 훨씬 크기 때문에 이 방식을 선택했다.
실제 작업 절차
Step 0: 사전 준비
# git filter-repo 설치
brew install git-filter-repo # macOS
pip install git-filter-repo # pip아래 명령어를 치면 현재 존재하는 히스토리 목록을 확인할 수 있다.
git log --all --full-history -- .envStep 1: mirror clone
git clone --mirror <https://github.com/[owner]/[repo].git>
cd [repo].git
- --mirror 옵션은 모든 브랜치와 태그를 포함해서 복제한다. 일반 clone과 달리 .git 디렉토리 자체를 복제하는 것과 비슷하다.
Step 2: 히스토리에서 .env 제거
git filter-repo --path .env --invert-paths
- --path .env : .env 파일을 대상으로 지정
- --invert-paths : 지정된 경로를 제외하고 나머지 보존
실행하면 다음과 같은 출력이 나온다:
Parsed 993 commits
New history written in 0.17 seconds; now repacking/cleaning...
Completely finished after 0.32 seconds.
993개 커밋을 0.32초 만에 처리했다. 참고로 filter-repo는 실수 방지를 위해 자동으로 origin remote를 제거한다.
Step 3: 검증
git log --all --full-history -- .env출력이 없으면 성공이다. .env가 포함된 커밋이 히스토리에서 완전히 사라졌다.
Step 4: 새 레포에 push
git remote add origin <https://github.com/[owner]/[new-repo].git>
git push --mirror origin
결과

왼쪽이 기존 레포, 오른쪽이 신규 레포이다. 기존 커밋 내역은 그대로 복사되었고 해시 값만 바뀌었다.

.env 파일의 마지막 기록이 있던 2024.03.06의 히스토리도 바뀌었다. 관련 커밋이 삭제된 것을 볼 수 있다.
마치며
환경변수와 시크릿 관리의 중요성을 제대로 느낄 수 있었다.
깃 히스토리를 바꾸는 것은 간단하지만, 원격 저장소의 기록을 바꾸는 것은 복잡하다.
그리고 테코톡할 때 공부했던 Git Objects 개념이 여기서 등장해서 반가웠다.
해당 개념을 알아둔 덕분에 이 현상을 좀 더 제대로 이해할 수 있었다.
'개발 > DevOps' 카테고리의 다른 글
| 롤링 배포를 사용한 모아온의 무중단 배포 전략 (0) | 2025.10.27 |
|---|