2026년 6월 2일 | 개발 깨알 상식_Tips
두 텍스트 목록의 차이를 볼 때 나는 diff보다 comm을 먼저 여는 경우가 많다. diff는 줄이 어디에서 바뀌었는지를 보여 주는 데 강하고, comm은 두 목록을 집합처럼 보고 “A에만 있는 줄”, “B에만 있는 줄”, “둘 다 있는 줄”을 나누는 데 편하다. 파일 목록, ID 목록, 실패 케이스 목록처럼 순서보다 포함 여부가 더 중요한 데이터에서는 이 차이가 꽤 크게 느껴진다.
정렬된 목록 두 개를 먼저 만들기
comm의 전제는 단순하지만 자주 까먹는다. 입력 두 개가 모두 정렬되어 있어야 한다. 원본 로그나 파일 목록을 그대로 넣으면 결과가 이상하거나, file is not in sorted order 같은 경고가 나온다. 그래서 나는 원본을 직접 건드리지 않고 정렬된 중간 파일을 먼저 만든다.
LC_ALL=C sort -u before.txt > before.sorted
LC_ALL=C sort -u after.txt > after.sorted
comm -23 before.sorted after.sorted
여기서 sort -u는 중복 줄을 한 번으로 줄인다. 같은 ID가 여러 번 나오는 로그라면, 그 반복 횟수를 분석하려는 게 아닌 이상 먼저 유일 목록으로 만드는 편이 안전하다. LC_ALL=C는 정렬 기준을 byte order 쪽으로 고정한다. 한글이나 대소문자 섞인 이름까지 완벽하게 해결해 주는 마법은 아니지만, 다른 환경에서 정렬 순서가 달라져 comm이 흔들리는 문제를 줄여 준다.
-23은 “첫 번째에만 있는 줄”
comm 기본 출력은 세 칸이다. 첫 번째 파일에만 있는 줄, 두 번째 파일에만 있는 줄, 둘 다 있는 줄이 탭으로 구분되어 나온다. 그런데 실제로는 세 칸을 한 번에 보는 일이 많지 않다. 옵션 숫자는 숨길 칸 번호로 읽으면 된다.
comm -23 before.sorted after.sorted # before에만 있는 줄
comm -13 before.sorted after.sorted # after에만 있는 줄
comm -12 before.sorted after.sorted # 둘 다 있는 줄
나는 삭제 후보를 볼 때 comm -23을 자주 쓴다. 예를 들어 이전 배포 산출물 목록이 before.txt이고, 새 빌드 산출물 목록이 after.txt라면 comm -23 결과는 “예전에는 있었는데 새 목록에는 없는 항목”이다. 이 목록을 바로 삭제 명령에 넘기기 전에 화면에서 한 번 읽으면, 실수로 사라진 파일과 의도적으로 빠진 파일을 가르는 데 도움이 된다.
파일명 목록에서는 NUL 구분자를 따로 생각하기
파일명 목록을 다룰 때는 공백과 개행이 문제다. comm 자체는 줄 단위 도구라서, 파일명에 개행이 들어가는 아주 까다로운 케이스까지 깔끔하게 처리해 주지는 않는다. 그래도 일반적인 프로젝트 파일 목록에서는 find 결과를 먼저 상대 경로로 고정하고, 공백이 있는 파일명이 섞여도 줄 자체를 깨지 않도록 조심하면 충분한 경우가 많다.
find build-old -type f -printf '%P\n' | LC_ALL=C sort -u > old-files.txt
find build-new -type f -printf '%P\n' | LC_ALL=C sort -u > new-files.txt
comm -23 old-files.txt new-files.txt
이 결과를 곧바로 xargs rm으로 넘기는 건 별로 좋아하지 않는다. 목록이 짧으면 먼저 파일로 저장하고 눈으로 본다. 목록이 길면 삭제 명령이 아니라 검증 명령부터 붙인다. 예를 들어 실제 경로가 존재하는지, 특정 확장자만 들어 있는지, 예상 밖의 상위 디렉터리가 섞였는지를 먼저 확인한다. 목록 비교와 파괴적 실행은 한 줄에 묶지 않는 편이 실수를 줄인다.
정렬 경고를 그냥 넘기지 않기
comm을 쓰다가 가장 찜찜한 순간은 정렬 경고가 떴는데도 결과가 어느 정도 그럴듯하게 보일 때다. 이때 “대충 맞겠지” 하고 넘어가면 안 된다. 정렬이 깨진 입력에서는 어느 줄이 어느 칸으로 들어갔는지 믿기 어려워진다. 나는 이 상황에서는 결과 파일을 버리고, 정렬 파일부터 다시 만든다.
LC_ALL=C sort -u before.txt > before.sorted
LC_ALL=C sort -u after.txt > after.sorted
comm --check-order -23 before.sorted after.sorted
--check-order를 붙이면 입력 순서 문제를 더 명시적으로 확인할 수 있다. GNU 환경이 아니라 옵션이 다를 수 있으면, 적어도 sort -c로 정렬 상태를 따로 검사한다. 목록 비교는 겉으로는 단순하지만, 정렬 기준이 맞지 않으면 결과 전체가 흔들린다. 그래서 나는 comm 결과가 이상할 때 본문 로직보다 먼저 정렬 기준과 locale부터 본다.
공백과 대소문자는 비교 전에 정하기
또 하나 자주 나오는 문제는 줄 끝 공백, 대소문자, 경로 prefix다. api/User.js와 ./api/User.js는 사람이 보기에는 같은 파일처럼 느껴져도 목록 비교에서는 다른 줄이다. 실패 query ID도 앞뒤 공백이 하나 붙으면 다른 ID가 된다. 그래서 비교 전에 “있는 그대로 비교할지”, “소문자로 낮출지”, “경로 prefix를 제거할지”를 먼저 정해야 한다.
sed 's/[[:space:]]*$//' raw-before.txt | LC_ALL=C sort -u > before.sorted
sed 's/[[:space:]]*$//' raw-after.txt | LC_ALL=C sort -u > after.sorted
정규화는 편하지만 위험도 있다. 대소문자가 실제로 의미 있는 파일 시스템이나 ID 체계에서는 함부로 낮추면 다른 항목을 같은 줄로 합쳐 버릴 수 있다. 그래서 나는 정규화 명령을 쓰더라도 주석처럼 한 줄 남긴다. “이번 비교에서는 줄 끝 공백만 제거했다” 정도만 남겨도, 나중에 차집합 목록을 다시 볼 때 결과의 의미가 훨씬 덜 흐려진다.
diff와 역할을 섞지 않기
comm이 diff를 대체하는 건 아니다. 설정 파일 본문이 어떻게 바뀌었는지, 줄 위치가 왜 밀렸는지, 패치로 적용할 수 있는 차이인지가 궁금하면 diff가 맞다. 반대로 “이번 실행에서 실패한 query ID 중 지난 실행에도 실패했던 것은 무엇인가”, “새로 생긴 artifact 파일은 무엇인가”, “삭제 후보 목록에서 allowlist에 없는 항목만 무엇인가”처럼 포함 여부가 질문이면 comm 쪽이 더 곧장 간다.
내가 쓰는 기준은 이렇다. 순서와 주변 문맥이 중요하면 diff, 목록의 교집합·차집합이 중요하면 comm이다. 그리고 comm을 쓸 때는 항상 정렬 파일을 따로 남긴다. 나중에 결과가 이상해 보이면 원본 목록, 정렬 목록, 차집합 목록을 각각 다시 열어 볼 수 있어야 한다. 한 번에 멋지게 파이프를 이어 붙인 명령보다, 중간 산출물이 남는 세 줄이 사고를 더 빨리 좁혀 줄 때가 많다.
'[개발 깨알 상식_Tips]' 카테고리의 다른 글
| 실행 전에 문법 오류 먼저 걸러내기 (0) | 2026.06.13 |
|---|---|
| JSON 검사를 실패 신호로 바로 세우기 (0) | 2026.06.03 |
| 복사 전에 바뀔 파일만 먼저 확인하기 (0) | 2026.05.29 |
| 실행 로그를 화면과 파일에 동시에 남기기 (0) | 2026.05.27 |
| 멈춘 검증을 제한 시간 뒤 강제로 접기 (0) | 2026.05.25 |