text diff compare developer-tools

텍스트 비교 도구: 텍스트 간의 차이점을 즉시 비교하고 확인하세요

온라인 텍스트 비교 도구로 두 텍스트, 문서 또는 코드 스니펫을 쉽게 비교하세요. 차이점, 추가 및 삭제된 부분을 즉시 강조 표시합니다.

소개

pull request를 열거나, 문서 수정본을 검토하거나, merge conflict를 해결할 때마다 여러분은 텍스트 diff와 상호작용하고 있습니다. diff(difference의 약자)는 두 버전의 텍스트 사이의 변경 사항을 표현한 것으로, 무엇이 추가되고, 삭제되고, 변경되지 않았는지를 보여줍니다.

텍스트 diff 도구는 현대 소프트웨어 개발의 근간입니다. 버전 관리 시스템, 코드 리뷰 플랫폼, 협업 도구, 배포 파이프라인을 구동합니다. diff가 어떻게 작동하는지 이해하는 것——단순히 읽는 방법뿐만 아니라 그 배후의 알고리즘까지——은 더 효과적인 개발자이자 더 사려 깊은 협업자가 되는 데 필수적인 기술입니다.

이 글은 1974년 Unix diff의 기원부터 오늘날 git이 사용하는 현대 알고리즘까지, diff 형식, 삼방향 병합, 시각화 전략, 그리고 실용적인 모범 사례를 상세히 설명합니다.


diff의 간략한 역사

1974년——diff의 탄생

Doug McIlroy는 1974년 벨 연구소에서 Unix용 diff 유틸리티를 작성했습니다. 이것은 혁명적이었습니다: 처음으로 개발자들이 두 텍스트 파일을 자동으로 비교하고 차이점에 대한 구조화된 설명을 생성할 수 있게 되었습니다. 이는 소프트웨어 패치 배포와 소스 코드 변경 추적에 즉시 유용했습니다.

1984년——GNU diff

자유 소프트웨어 재단이 GNU diffutils의 일부로 GNU diff를 출시하여 이식 가능하고 개선된 버전을 모두가 사용할 수 있게 했습니다. GNU diff는 context diff와 unified diff라는 추가 출력 형식을 도입했으며 이것들은 업계 표준이 되었습니다.

1986년——Myers 알고리즘

Eugene Myers가 1986년 획기적인 논문 *"An O(ND) Difference Algorithm and Its Applications"*를 발표했습니다. **최단 편집 스크립트(Shortest Edit Script, SES)**를 찾는 이 알고리즘은 git을 포함한 대부분의 현대 diff 구현의 이론적 토대가 되었습니다.

1990년대——diff/patch가 범용 패치 형식으로

diffpatch의 조합은 소프트웨어 업데이트 배포의 사실상 표준이 되었습니다. 오픈 소스 프로젝트들은 .patch 파일을 배포했고, 기여자들은 메일링 리스트에 diff를 이메일로 보냈으며, Linux 커널은 거의 전적으로 이메일 기반의 diff/patch 워크플로로 개발·유지 관리되었습니다.

2005년——Git과 Myers Diff

Linus Torvalds가 2005년 git을 만들었고, Myers 알고리즘을 기본 diff 엔진으로 채택했습니다. Git의 diff 서브시스템은 역사상 가장 널리 사용된 diff 구현 중 하나가 되어, GitHub와 GitLab 같은 플랫폼에서 매일 수십억 건의 비교를 처리합니다.

2010년대——Histogram Diff와 웹 시각화

Git은 많은 작업의 기본값으로 histogram diff 알고리즘을 도입했습니다. 동시에 웹 기반 diff 시각화가 번성했습니다——GitHub의 분할 뷰와 인라인 PR diff, GitLab의 리뷰 도구, Gerrit의 변경 추적이 모두 diff 출력을 대규모 개발자 커뮤니티에 제공했습니다.


Diff 알고리즘

LCS——최장 공통 부분 수열

diff를 계산하는 고전적인 접근 방식은 두 시퀀스의 **최장 공통 부분 수열(Longest Common Subsequence, LCS)**에 기반합니다. LCS는 두 입력 모두에서 동일한 상대적 순서로 나타나는 가장 긴 요소 시퀀스입니다(연속적일 필요는 없음).

예시:

  • 문자열 A = "ABCBDAB"
  • 문자열 B = "BDCAB"
  • LCS = "BCAB" (길이 4)

diff는 LCS에 포함되지 않은 것에서 도출됩니다: A에만 있는 요소는 삭제이고, B에만 있는 요소는 삽입입니다. LCS 계산에는 O(M×N)의 시간과 공간이 필요하며, 작은 파일에는 허용 가능하지만 큰 파일에는 느립니다.

Myers 알고리즘——최단 편집 스크립트

Eugene Myers의 1986년 알고리즘은 최단 편집 스크립트(SES)——시퀀스 A를 시퀀스 B로 변환하는 데 필요한 최소 삽입과 삭제 횟수——를 찾습니다. 이는 LCS를 찾는 것과 동등하지만, Myers의 접근 방식은 실제로 훨씬 더 효율적입니다.

주요 특성:

  • 시간 복잡도: O(ND), N = len(A) + len(B), D = 편집 거리(변경 수)
  • 공간 복잡도: O(N) (선형 공간 개선 사용 시)
  • "스네이크" 그래프 탐색 사용——편집 그래프를 통한 대각선 경로로, 각 "스네이크"는 일치하는 문자 시퀀스를 나타냄
  • git, GNU diff, 대부분의 현대 diff 도구에서 사용

Myers 알고리즘은 변경 사항이 파일 크기에 비해 작을 때(낮은 D 값) 뛰어난 성능을 보입니다——이는 버전 관리에서 일반적인 경우로, 대부분의 커밋은 파일의 작은 부분만 변경합니다.

Patience Diff——코드 구조에 최적

Patience diff는 다른 접근 방식을 취합니다: 먼저 두 파일 모두에 정확히 한 번 나타나는 고유한 줄을 찾아 앵커로 사용한 다음, 그 사이의 섹션을 재귀적으로 diff합니다.

이는 }, {, return, 또는 소스 파일 전체에 걸쳐 나타나는 빈 줄 같이 동일한 구조적 줄이 많은 코드에서 훨씬 더 나은 결과를 생성합니다. Myers는 잘못된 닫는 중괄호와 매칭될 수 있지만, patience diff는 고유하고 의미 있는 줄에 고정하여 훨씬 이해하기 쉬운 diff를 생성합니다.

Patience diff는 BazaarMercurial에서 사용되며, git에서는 git diff --diff-algorithm=patience로 사용할 수 있습니다.

Histogram Diff——Git의 현대적 기본값

Histogram diff는 patience diff의 진화버전입니다. 줄 빈도의 히스토그램을 구축하고 이 빈도 정보를 사용하여 더 스마트한 매칭 결정을 내립니다. 많이 나타나는 줄은 의미 있는 앵커가 될 가능성이 낮고, 드문 줄이 더 나은 후보입니다.

Git은 histogram diff를 도입했으며 2012년경부터 많은 시나리오에서 권장 기본값이 되었습니다. 전역으로 설정하려면:

git config --global diff.algorithm histogram

Diff 출력 형식

일반 diff 형식 (원래 Unix 형식)

diff file1.txt file2.txt가 생성하는 원래 출력 형식:

2d1
< file1에만 있는 줄
5,7c4,6
< 이전 줄 A
---
> 새 줄 A

2d1 (file1에서 2번째 줄 삭제)과 5,7c4,6 (file1의 5-7번째 줄을 file2의 4-6번째 줄로 변경) 같은 명령어들은 이 형식을 기계가 읽기에는 적합하지만 사람이 이해하기에는 어렵습니다.

Context Diff (-c 플래그)

GNU diff와 함께 도입된 context diff는 가독성을 위해 주변 줄을 추가하고, 변경된 줄을 !로 표시하며, 이전 블록과 새 블록을 *** / ---로 구분합니다.

Unified Diff 형식 (-u 플래그)——표준 형식

Unified diff 형식은 git과 모든 주요 패치 워크플로에서 사용하는 현대 표준입니다. 이전 콘텐츠와 새 콘텐츠를 단일 블록으로 결합하고, +-로 변경 사항을 표시하며, 각 변경의 위치를 식별하는 hunk 헤더를 포함합니다.

git diff 출력

Git의 출력은 추가 메타데이터가 있는 unified diff입니다——파일 모드, 인덱스 해시, 리포지토리 경로를 식별하는 diff --git 헤더가 포함됩니다.


Unified Diff 형식 이해하기

Unified diff 형식을 자세히 해독해 봅시다:

--- a/config.py
+++ b/config.py
@@ -10,7 +10,8 @@
 DATABASE_HOST = 'localhost'
 DATABASE_PORT = 5432
-DATABASE_NAME = 'myapp_dev'
+DATABASE_NAME = 'myapp_production'
+DATABASE_SSL = True
 
 # Cache settings
 CACHE_TTL = 300

파일 헤더

---원본 파일(버전 A)을 표시하고, +++ 파일(버전 B)을 표시합니다. git에서 a/b/는 관례적인 접두사입니다.

Hunk 헤더

@@ -10,7 +10,8 @@

이것이 hunk 헤더이며, 이 변경 덩어리가 파일의 어디에 있는지 정확히 알려줍니다:

  • -10,7원본 파일에서 이 hunk는 10번째 줄에서 시작하여 7줄에 걸쳐 있음
  • +10,8 파일에서 이 hunk는 10번째 줄에서 시작하여 8줄에 걸쳐 있음 (한 줄이 추가됨)

형식은 항상 @@ -시작줄,줄수 +시작줄,줄수 @@입니다.

줄 마커

hunk 본문의 각 줄은 세 가지 문자 중 하나로 시작됩니다:

  • (공백)——컨텍스트 줄: 변경 없음, 가독성을 위해 표시
  • - (마이너스)——삭제된 줄: 원본에 있고 새 파일에는 없음
  • + (플러스)——추가된 줄: 원본에 없고 새 파일에 있음

이 예시에서 DATABASE_NAME = 'myapp_dev'는 삭제되어 프로덕션 이름으로 대체되었고, DATABASE_SSL = True는 완전히 새로 추가된 줄입니다.


줄 단위 vs 단어 단위 vs 문자 단위 Diff

표준 diff는 줄 단위로 작동합니다——각 줄은 원자 단위로 취급됩니다. 이는 줄이 변경의 자연스러운 단위인 소스 코드에 이상적입니다.

단어 단위 Diff

산문, 문서, 설정 파일의 경우 단어 단위 diff가 더 유익합니다. 다음 변경을 고려해 보세요:

변경 전: The quick brown fox jumps over the lazy dog
변경 후: The quick red fox leaps over the sleeping cat

줄 단위 diff는 전체 줄이 변경된 것으로 표시합니다. 단어 단위 diff는 무엇이 변경되었는지 정확히 강조합니다:

The quick brown red fox jumps leaps over the lazy dog sleeping cat

Git은 git diff --word-diff로 단어 단위 diff를 지원합니다.

문자 단위 Diff

문자 단위 diff(Levenshtein 거리 같은 알고리즘 사용)는 개별 문자 수준에서 작동합니다. 단 하나의 문자 변경도 중요한 비밀번호, 식별자, 설정 값 같은 짧은 문자열에 가장 적합합니다.

비교 표

접근 방식 세분성 최적 용도 도구 예시
Line diff 소스 코드 git diff
Word diff 단어 산문/문서 git diff --word-diff
Char diff 문자 짧은 문자열 Levenshtein 기반
Semantic diff AST 노드 코드 리팩토링 difftastic

difftastic과 같은 Semantic diff 도구는 소스 코드를 추상 구문 트리(AST)로 파싱하고 원시 텍스트 대신 트리 구조를 diff하여, 언어 구문을 이해하고 공백 같은 표면적 변경을 무시하는 diff를 생성합니다.


삼방향 병합과 병합 충돌

삼방향 병합 모델

두 사람이 같은 파일을 독립적으로 수정하면, 단순한 양방향 diff로는 누구의 변경 사항을 채택해야 하는지 결정할 수 없습니다. Git은 **삼방향 병합(three-way merge)**을 사용합니다:

  1. Base ——공통 조상 커밋
  2. Ours ——현재 브랜치의 버전
  3. Theirs ——들어오는 브랜치의 버전

알고리즘은 ourstheirs 모두를 base와 비교합니다:

  • ours만 어떤 영역을 변경했으면 → ours 사용
  • theirs만 어떤 영역을 변경했으면 → theirs 사용
  • 둘 다 같은 영역을 다르게 변경했으면 → 충돌(conflict)

병합 충돌 마커

git이 자동으로 충돌을 해결할 수 없을 때 파일에 마커를 삽입합니다:

<<<<<<< HEAD
DATABASE_NAME = 'myapp_production'
=======
DATABASE_NAME = 'myapp_staging'
>>>>>>> feature/staging-config
  • <<<<<<<======= 사이가 여러분의 버전(HEAD)
  • =======>>>>>>> 사이가 들어오는 버전
  • 충돌을 해결하기 위해 파일을 수동으로 편집한 다음 git add를 실행해야 합니다

사용 사례

코드 리뷰

Diff는 코드 리뷰의 언어입니다. GitHub, GitLab, Bitbucket의 pull request는 모두 변경 사항을 diff로 표시하여 리뷰어가 무엇이 변경되었는지 줄별로 정확히 이해할 수 있게 합니다. 작고 집중적인 diff는 리뷰 품질과 속도를 크게 향상시킵니다.

문서 비교

법무팀은 계약서 수정본을 비교하기 위해 diff 도구를 사용합니다. 기술 작가들은 문서 변경을 검토하기 위해 사용합니다. 버전 관리된 문서가 관련된 모든 워크플로는 구조화된 diff 출력에서 이점을 얻습니다.

로그 분석

시스템 관리자는 실행 사이에 무엇이 변경되었는지 식별하기 위해 로그 파일을 비교합니다——새 오류, 누락된 항목, 설정 드리프트. diffcolordiff 같은 도구는 시스템 관리자 도구 키트의 표준 구성 요소입니다.

법적·컴플라이언스 요건

규제 제출물, 감사 추적, 컴플라이언스 문서는 종종 버전 간 변경 사항의 공식 기록을 필요로 합니다. Diff 도구는 무엇이, 언제, 어떻게 변경되었는지에 대한 객관적이고 재현 가능한 기록을 제공합니다.

보안 분석

보안 연구원들은 설정 스냅샷과 시스템 상태를 비교하여 승인되지 않은 변경을 감지합니다. 파일 무결성 모니터링 시스템은 diff 원리에 기반하여 구축됩니다.


시각화 접근 방식

나란히 보기 (분할 뷰)

두 패널이 이전 버전과 새 버전을 나란히 표시하며, 해당 행에 변경 사항을 강조합니다. 양쪽 컨텍스트가 필요한 대규모 변경에 가장 적합합니다.

인라인 (통합 뷰)

삭제와 추가가 컨텍스트 줄과 섞인 단일 스트림으로 표시됩니다. 대부분의 명령줄 도구와 GitHub PR 뷰의 기본값입니다. 작은 변경이 밀집된 경우에 가장 적합합니다.

GitHub PR 뷰

GitHub는 구문 강조, 확장 가능한 컨텍스트, 인라인 리뷰 댓글, 분할 뷰 토글, 파일별 "확인됨" 추적으로 unified diff를 강화하여 대규모 pull request를 리뷰어가 탐색하기 쉽게 만듭니다.

단어 단위 diff 강조

git diff --word-diff=color 같은 도구는 줄 내에서 변경된 단어를 강조하여 줄 단위 diff 컨텍스트에서 문자 수준의 변경을 가시화합니다. 설정 파일과 산문 문서에 특히 유용합니다.


모범 사례

  1. 커밋을 작고 집중적으로 유지하세요. 하나의 논리적 사항만 변경하는 diff는 여러 이유로 수십 개의 파일을 건드리는 diff보다 훨씬 리뷰하기 쉽습니다.

  2. 의미 있는 커밋 메시지를 작성하세요. Diff는 무엇이 변경되었는지 보여주고, 커밋 메시지는 변경되었는지 설명합니다.

  3. 올바른 diff 알고리즘을 사용하세요. 코드의 경우, histogram 또는 patience diff가 Myers보다 더 읽기 쉬운 출력을 생성하는 경우가 많습니다. 전역 설정: git config --global diff.algorithm histogram.

  4. 커밋 전에 diff를 검토하세요. git diff --staged는 커밋될 내용을 정확히 보여줍니다. git commit을 실행하기 전에 항상 확인하세요.

  5. 산문에는 word-diff를 사용하세요. 문서나 README 파일을 작성할 때, git diff --word-diff는 줄 diff보다 훨씬 읽기 쉽습니다.

  6. hunk 컨텍스트를 이해하세요. 각 hunk 주변의 세 줄 컨텍스트는 이유가 있습니다——컨텍스트 안에서 변경 사항을 이해하는 데 도움이 됩니다.

  7. 충돌을 신중하게 해결하세요. 상대방의 변경 사항을 이해하지 않고 충돌의 한 쪽을 받아들이지 마세요. 두 변경 사항이 모두 중요할 수 있습니다.

  8. 바이너리 파일에는 .gitattributes를 사용하세요. 무의미한 diff를 피하기 위해 바이너리 및 특수 파일 처리 방법을 git에 알려주세요.


자주 묻는 질문

Q: diffpatch의 차이점은 무엇인가요?
A: diff는 두 파일을 비교하여 diff 출력을 생성합니다. patch는 그 diff 출력을 받아 파일에 적용하여 변경 사항을 재현합니다. 이들은 함께 작동하도록 설계된 보완적인 도구입니다.

Q: git은 기본적으로 어떤 diff 알고리즘을 사용하나요?
A: Git은 기본적으로 Myers 알고리즘을 사용하지만, histogram diff가 권장됩니다: git config --global diff.algorithm histogram.

Q: @@ -10,7 +10,8 @@는 무엇을 의미하나요?
A: hunk가 두 파일 모두에서 10번째 줄에서 시작함을 의미합니다. 이전 파일에서는 7줄을 커버하고, 새 파일에서는 8줄(한 줄이 추가됨)을 커버합니다.

Q: 바이너리 파일을 diff할 수 있나요?
A: 표준 diff 도구는 텍스트에서 작동합니다. 바이너리 파일에는 전문 도구(bsdiff 등)가 있습니다. 대부분의 diff 도구는 단순히 "Binary files differ"라고 보고합니다.

Q: "hunk"란 무엇인가요?
A: hunk는 주변 컨텍스트 줄을 포함한 diff 내의 연속적인 변경 블록입니다. 변경 사항이 파일 전체에 분산되어 있으면 단일 diff에 여러 hunk가 포함될 수 있습니다.

Q: 왜 git이 때때로 이동된 코드에 대해 혼란스러운 diff를 생성하나요?
A: 표준 줄 기반 diff에는 "이동"의 개념이 없습니다——추가와 삭제만 볼 수 있습니다. 이동된 코드는 한 위치에서 삭제된 것으로, 다른 위치에서 추가된 것으로 나타납니다. AST 구조를 이해하는 difftastic 같은 도구는 이동을 감지할 수 있습니다.

Q: 삼방향 병합이란 무엇인가요?
A: 공통 조상(base)과 두 개의 변경된 버전을 사용하여 변경 사항을 지능적으로 결합하고, 충돌하지 않는 편집은 자동으로 해결하며 진정한 충돌에는 플래그를 달아주는 병합 전략입니다.


텍스트 diff를 이해하는 것은 단순한 기술적 호기심이 아닙니다——텍스트, 코드, 문서를 시간이 지남에 따라 다루는 모든 사람에게 필수적인 기술입니다. Unix diff 명령의 우아한 단순함부터 현대 코드 리뷰 플랫폼을 구동하는 정교한 알고리즘까지, 이 소박한 diff는 50년 이상 동안 소프트웨어가 구축, 검토, 유지 관리되는 방식을 형성해 왔습니다.