개발이군고구마

Git revert, revert의 이해 본문

Cloud Deploy

Git revert, revert의 이해

김구황 2026. 4. 3. 07:25
728x90

운영 배포 브랜치를 사용하는 흐름에서 특정 SR이 한 번 반영되었다가 revert된 뒤, 이후 같은 SR 브랜치에서 작업이 계속 이어지는 경우의 커밋 그래프 해석 정리. 다른 SR들이 정상적으로 배포된 상황까지 포함한 일반적인 사례 설명.

1. 전제 구조

브랜치 레이어를 다음과 같이 고정한 설명.

  • 맨 위: main
  • 가운데: PRD1, PRD2 같은 운영 배포 브랜치
  • 맨 아래: SR1, SR2, SR3 같은 작업 브랜치

시나리오의 핵심 조건은 다음과 같음.

  • 모든 SR 브랜치는 main에서 분기
  • SR2, SR3는 문제 없이 PRD1에 반영되고 이후 main에도 정상 반영
  • SR1은 PRD1에 반영되었으나 문제로 인해 PRD1에서 revert 수행
  • 그 revert 결과가 main에도 반영
  • 이후 main에서 PRD2 생성
  • SR1에서는 추가 수정 커밋이 계속 쌓임

2. revert의 의미

revert는 기존 커밋을 삭제하는 동작이 아니라, 기존 커밋의 변경을 반대로 적용하는 새 커밋을 만드는 동작.

기본 revert 구조
A ---- B ---- C ---- R(C)

C    : 원래 변경 커밋
R(C) : C의 반대 내용을 적용한 새 커밋
중요 포인트
커밋 C 자체가 사라지는 것 아님. Git은 여전히 C를 알고 있으며, 단지 그 이후에 C를 취소한 R(C)라는 새 이력이 추가된 상태로 이해함.

이 성질 때문에, 한 번 merge된 브랜치를 나중에 revert했다고 해서 Git이 그 브랜치를 “합쳐진 적 없는 브랜치”로 다시 인식하는 일은 없음.

3. 문제 없는 일반적인 배포 흐름

먼저 revert가 없는 일반적인 흐름의 기준선 설명.

초기 상태
main : A -------- B -------- M1
                       |\
PRD1 :                 P1
PRD2 :
                       |
SR1  :                 C1 ---- C2
SR2  :                 D1 ---- D2
SR3  :                 E1 ---- E2

M1 시점의 main에서 PRD1, SR1, SR2, SR3가 각각 갈라진 상태로 가정.

PRD1에 SR2, SR3를 정상 merge
main : A -------- B -------- M1
                       |\
PRD1 :                 P1 ---- MP2 ---- MP3
                             /       /
PRD2 :
                       |
SR1  :                 C1 ---- C2
SR2  :                 D1 ---- D2
SR3  :                 E1 ---- E2

MP2 = merge(PRD1 <- SR2)
MP3 = merge(PRD1 <- SR3)
수행 현재 브랜치 대상 브랜치 Git이 찾는 공통 부모
SR2를 PRD1에 merge PRD1 SR2 M1에서 갈라진 공통 조상 지점
SR3를 PRD1에 merge PRD1 SR3 M1에서 갈라진 공통 조상 지점
PRD1 결과를 main에 반영
main : A -------- B -------- M1 ---------------- MM1
                       |\                        /
PRD1 :                 P1 ---- MP2 ---- MP3 ----
PRD2 :
                       |
SR1  :                 C1 ---- C2
SR2  :                 D1 ---- D2
SR3  :                 E1 ---- E2

MM1은 PRD1의 정상 반영 결과. 이 상태에서는 SR2, SR3 관련 히스토리와 실제 코드 상태가 일치.

4. 문제 발생 흐름 추가

이제 같은 시점에 SR1도 PRD1에 반영되었으나, 배포 과정에서 문제를 일으켜 revert되는 상황 추가.

SR1까지 함께 PRD1에 반영된 상태
main : A -------- B -------- M1
                       |\
PRD1 :                 P1 ---- MP1 ---- MP2 ---- MP3
                             /       /       /
PRD2 :
                       |
SR1  :                 C1 ---- C2
SR2  :                 D1 ---- D2
SR3  :                 E1 ---- E2

MP1 = merge(PRD1 <- SR1)
수행 현재 브랜치 대상 브랜치 Git이 찾는 공통 부모
SR1을 PRD1에 merge PRD1 SR1 M1에서 갈라진 공통 조상 지점
SR1만 revert한 상태
main : A -------- B -------- M1
                       |\
PRD1 :                 P1 ---- MP1 ---- MP2 ---- MP3 ---- RP1
                             /       /       /
PRD2 :
                       |
SR1  :                 C1 ---- C2
SR2  :                 D1 ---- D2
SR3  :                 E1 ---- E2

RP1 = revert(MP1)
이 시점의 핵심
PRD1의 파일 상태에서는 SR1의 효과가 제거된 상태. 그러나 Git 히스토리 안에는 여전히 MP1이 존재. 즉, Git은 SR1의 초기 커밋 C1, C2가 PRD1 계열에 한 번 들어왔던 사실을 알고 있는 상태.
RP1까지 main에 반영된 상태
main : A -------- B -------- M1 ------------------------------ MM2
                       |\                                     /
PRD1 :                 P1 ---- MP1 ---- MP2 ---- MP3 ---- RP1
                             /       /       /
PRD2 :
                       |
SR1  :                 C1 ---- C2
SR2  :                 D1 ---- D2
SR3  :                 E1 ---- E2

main은 이제 SR2, SR3는 포함하고, SR1은 한 번 포함되었다가 RP1로 취소된 이력까지 함께 보유한 상태.

5. PRD2 생성과 SR1 추가 작업

RP1이 반영된 main에서 PRD2 생성, SR1은 계속 진행
main : A -------- B -------- M1 ------------------------------ MM2 -------- M2
                       |\                                     /             |
PRD1 :                 P1 ---- MP1 ---- MP2 ---- MP3 ---- RP1              |
PRD2 :                                                                        P2
                       |
SR1  :                 C1 ---- C2 ---- C3 ---- C4
SR2  :                 D1 ---- D2
SR3  :                 E1 ---- E2

이 시점에서 PRD2는 main의 자손이므로 다음 사실을 모두 포함한 상태.

  • SR2와 SR3가 정상 반영된 이력
  • SR1이 한 번 반영된 MP1 이력
  • 그 반영을 취소한 RP1 이력
중요 포인트
PRD2의 실제 코드 상태에는 SR1의 초기 내용이 없을 수 있음. 그러나 히스토리 관점에서는 SR1의 초기 커밋 ✨C1, C2가 이미 main/PRD 계열에 들어온 적 있는 이력✨으로 인식되는 상태.

6. merge로 다시 반영하는 경우

이제 SR1의 최신 상태(C1, C2, C3, C4)를 PRD2에 다시 반영하려는 상황. 일반적으로 로컬에서 SR1을 최신으로 만든 뒤, PRD2 대상으로 PR을 열거나 git merge를 수행하는 흐름.

PRD2 <- SR1 merge 시도
main : A -------- B -------- M1 ------------------------------ MM2 -------- M2
                       |\                                     /             |
PRD1 :                 P1 ---- MP1 ---- MP2 ---- MP3 ---- RP1              |
PRD2 :                                                                        P2 -------- MP4 ?
                       |                                                                  /
SR1  :                 C1 ---- C2 ---- C3 ---- C4 --------------------------------------+
SR2  :                 D1 ---- D2
SR3  :                 E1 ---- E2

6-1. 이 merge에서 Git이 계산하는 것

항목 설명
현재 브랜치 PRD2의 HEAD, 즉 P2
대상 브랜치 SR1의 HEAD, 즉 C4
공통 부모 SR1의 초기 변경 C1, C2가 이미 shared history로 취급되는 지점
Git이 해석하는 shared history
main : --------------------------------------(MP1과 RP1을 모두 알고 있음)
PRD2 : -------------------- P2
                              
SR1  : C1 ---- C2 ---- C3 ---- C4
        ^^^^^^
        이미 한 번 main/PRD 계열과 ✨공유된 이력으로 인식되는 구간

여기서 발생하는 핵심 문제는 다음과 같음.

  • Git의 역사 판단: C1, C2는 이미 공유된 이력
  • PRD2의 실제 코드 상태: RP1 때문에 C1, C2의 효과가 제거된 상태일 수 있음
  • 따라서 merge 시 ✨C3, C4 위주✨ 로만 신규 차이가 계산되거나, 예상과 다른 충돌 및 병합 결과가 발생할 수 있음
핵심 정리
merge 자체가 불가능한 상황 아님. 공통 부모가 없어서 막히는 것 아님. 오히려 공통 부모가 존재하기 때문에 Git은 C1, C2를 다시 가져올 신규 변경으로 보지 않을 수 있다는 점이 문제의 본질.

6-2. push와 merge의 관계

(헤깔렸던 개념) “push가 안 된다”라고 표현하는 상황은 대체로 다음 의미.

  • SR1 브랜치의 포인터를 PRD2 위로 단순히 fast-forward 시킬 수 없는 상태
  • PRD2와 SR1이 서로 다른 방향으로 진행되었으므로 통합 작업 필요
  • 실제 수행은 보통 PR 기반 merge, 로컬 merge 후 push, 또는 rebase 후 push의 형태
fast-forward 불가의 의미
PRD2 : -------- P2
                 \
SR1  :  C1 ---- C2 ---- C3 ---- C4

두 브랜치가 일렬로 이어진 관계가 아니므로
브랜치 포인터만 앞으로 미는 push로 해결되지 않음
통합 작업(merge / rebase / cherry-pick 등) 필요

7. rebase로 다시 반영하는 경우

이번에는 SR1이 main 최신을 기준으로 다시 정리되는 경우. 예를 들어 git rebase main 또는 유사한 방식으로 SR1의 변경을 최신 main 위에 다시 쌓는 흐름 설명.

rebase 전 상태
main : A -------- B -------- M1 ------------------------------ MM2 -------- M2
                       |\                                     /             |
PRD1 :                 P1 ---- MP1 ---- MP2 ---- MP3 ---- RP1              |
PRD2 :                                                                        P2
                       |
SR1  :                 C1 ---- C2 ---- C3 ---- C4

이 상태에서 SR1에 대해 git rebase main을 수행하면 Git은 대략 다음 방식으로 해석.

  • SR1과 main의 공통 조상 이후 SR1에만 있는 커밋을 찾아냄
  • 이미 main에 들어간 적 있다고 판단되는 커밋은 재적용 대상에서 제외될 수 있음
  • 결과적으로 C3, C4만 새 커밋처럼 다시 재생성되는 형태가 나타날 수 있음
rebase 시 Git이 재적용 대상으로 보는 쪽의 개념도
main : ------------------------------------------- MM2 -------- M2
SR1  : C1 ---- C2 ---- C3 ---- C4
        ^^^^^^
        main 쪽과 이미 공유된 history로 간주될 수 있는 구간
                    ^^^^^^^^^^
                    rebase 시 재적용 대상으로 남는 구간의 핵심
rebase 후 개념적 결과
main : A -------- B -------- M1 ------------------------------ MM2 -------- M2
                       |\                                     /             |
PRD1 :                 P1 ---- MP1 ---- MP2 ---- MP3 ---- RP1              |
PRD2 :                                                                        P2
                                                                                 
SR1  :                                                                        C3' ---- C4'

C3', C4' = main 최신(M2) 위에서 다시 생성된 새 커밋

이 경우에도 본질은 동일.

  • main은 MP1과 RP1을 모두 알고 있음
  • SR1의 초기 변경 C1, C2는 이미 shared history로 간주될 수 있음
  • 따라서 rebase 후 브랜치에는 C3, C4만 재배치된 것처럼 보일 수 있음
중요 포인트
rebase는 히스토리를 다시 쓰는 동작일 뿐, main에 들어가 있던 RP1의 의미를 무효화하지 않음. 따라서 원래 되살려야 하는 초기 변경이 있다면, 단순 rebase만으로는 충분하지 않을 수 있음.

8. revert의 revert가 필요한 이유

문제의 핵심은 공통 부모가 사라지지 않는다는 점. 더 정확히는 revert가 히스토리 연결을 지우지 않는다는 점.

revert의 revert 전후 비교
[RRP1 없이]
main : ---- MP1 ---- RP1 ---- ... ---- PRD2
SR1  :      C1 ---- C2 ---- C3 ---- C4

Git 인식 : C1, C2는 예전에 이미 공유된 이력
코드 상태 : RP1 때문에 C1, C2의 효과는 사라진 상태일 수 있음

[RRP1 적용 후]
main : ---- MP1 ---- RP1 ---- ... ---- PRD2 ---- RRP1
SR1  :      C1 ---- C2 ---- C3 ---- C4

RRP1 = revert(RP1)
효과 : RP1이 제거했던 내용을 다시 복원

RRP1 적용 후의 구조.

PRD2에 revert의 revert를 반영한 뒤 다시 통합
main : A -------- B -------- M1 ------------------------------ MM2 -------- M2
                       |\                                     /             |
PRD1 :                 P1 ---- MP1 ---- MP2 ---- MP3 ---- RP1              |
PRD2 :                                                                        P2 ---- RRP1 ---- MP5
                       |                                                                                 /
SR1  :                 C1 ---- C2 ---- C3 ---- C4 -----------------------------------------------------+
SR2  :                 D1 ---- D2
SR3  :                 E1 ---- E2
단계 의미
RRP1 생성 RP1이 취소했던 내용을 다시 복원하는 새 커밋 생성
그 다음 merge 또는 rebase 이제 C1, C2 기반이 코드 상태에도 존재하므로, C3, C4 같은 후속 변경을 자연스럽게 올릴 수 있는 상태 형성
핵심 정리
revert의 revert는 “예전에 취소했던 변경을 다시 살리는 동작”의 의미. Git이 이미 공유된 이력으로 보는 구간과 실제 코드 상태를 다시 맞춰주는 역할.

9. merge와 rebase 비교

항목 merge rebase
히스토리 형태 기존 분기 구조를 유지한 채 merge commit 추가 브랜치 커밋을 새로운 기준 위에 다시 쌓는 형태
공통 부모 활용 공통 부모 기준으로 양쪽 차이를 합침 공통 부모 이후의 브랜치 전용 커밋만 재적용
SR1 초기 커밋이 한 번 반영되었다가 revert된 상황 C1, C2를 이미 shared history로 간주할 수 있어 C3, C4 중심의 병합이 일어날 수 있음 C1, C2를 재적용 대상에서 제외하고 C3, C4만 새 커밋처럼 재배치할 수 있음
원래 기능 복원 필요 여부 복원이 필요하면 RRP1 같은 별도 조치 필요 가능성 큼 복원이 필요하면 단순 rebase만으로 충분하지 않을 수 있음

10. 핵심 요약

  • revert는 기존 커밋을 삭제하는 동작이 아니라, 반대 patch를 적용하는 새 커밋 생성 동작
  • 따라서 한 번 merge된 이력은 revert 이후에도 Git 히스토리 안에 남아 있음
  • 운영 배포 브랜치에서 특정 SR을 revert한 뒤 그 결과가 main까지 반영되면, 다음 PRD 브랜치는 그 “merge된 적 있었고, 나중에 revert된 역사”를 함께 상속받음
  • 이후 같은 SR 브랜치에서 작업을 계속하고 다시 merge 또는 rebase하면, Git은 초기 커밋을 이미 shared history로 보고 후속 커밋만 새 변경으로 처리할 수 있음
  • 원래 변경 자체를 다시 살려야 하는 경우에는 revert의 revert가 필요할 수 있음
가장 짧은 한 장 요약
1) SR1이 PRD1에 merge됨
2) SR1이 PRD1에서 revert됨
3) 그 revert가 main에 반영됨
4) main에서 PRD2 생성됨
5) SR1은 계속 작업되어 C3, C4가 쌓임
6) PRD2에 SR1을 다시 merge/rebase 시도
7) Git은 C1, C2를 이미 공유된 history로 볼 수 있음
8) 원래 기능 복원이 필요하면 revert의 revert 검토 대상