본문 바로가기
Tools2026년 9월 19일9분 읽기

coreutils 교체 검증하기 — uutils 전환 전에 돌려 볼 것들

YS
김영삼
조회 57
coreutils 교체 검증하기 — uutils 전환 전에 돌려 볼 것들

배포판들이 기본 coreutils를 Rust로 작성된 uutils 구현으로 바꾸는 흐름이 이어지고 있다. 목표는 드롭인 호환이고, 대부분의 일상적인 사용에서는 차이를 느끼기 어렵다.

문제가 생기는 곳은 정해져 있다. 출력 문자열을 파싱하는 스크립트, 로케일에 의존하는 처리, 그리고 문서화되지 않은 동작에 기대는 코드다. 이 세 가지만 점검하면 전환 리스크의 대부분이 사라진다.

나는 배포 스크립트에서 ls -l 출력을 awk로 잘라 쓰는 코드를 발견하고 한숨을 쉰 적이 있다. 10년 전에 누가 짰는지 모르는 코드였고, 잘 돌아갔다. 그런 코드는 구현이 바뀔 때 조용히 틀린 값을 만든다. 에러도 안 난다. 그게 최악이다.

어디가 깨지나

영역위험도이유
출력 문자열 파싱높음메시지 문구·정렬·구분자가 구현마다 다를 수 있다
에러 메시지 매칭높음문구가 다르면 조건문이 통째로 무력화
로케일 의존 출력중간날짜·숫자 형식이 환경에 따라 달라진다
드물게 쓰는 플래그중간구현 범위 차이가 남아 있을 수 있다
종료 코드낮음대체로 호환되지만 엣지 케이스 확인 필요
성능 특성낮음대량 파일 처리에서 차이가 날 수 있다
가장 먼저 할 일 스크립트에서 coreutils 출력을 파싱하는 곳을 찾아내는 것. 이건 전환 여부와 무관하게 해야 하는 일이기도 하다. 문자열 파싱은 원래부터 취약한 관행이었고, 구현 교체는 그 부채를 드러낼 뿐이다.

색출 — 파싱 코드 찾기

# coreutils 출력을 파싱하는 패턴 찾기
grep -rnE '\b(ls|df|du|stat|date|wc|find)\b[^|]*\|[[:space:]]*(awk|cut|sed|grep)' \
  --include='*.sh' --include='*.bash' --include='Makefile' .

# 에러 메시지에 의존하는 코드
grep -rnE '(No such file|Permission denied|cannot access)' --include='*.sh' .

# 파일 크기·권한을 텍스트로 파싱하는 곳
grep -rn "ls -l" --include='*.sh' .

# 더 나은 대안이 있는 경우가 대부분이다:
#   파일 크기      → stat -c %s file      (또는 언어 API)
#   수정 시각      → stat -c %Y file
#   파일 개수      → find ... -printf '.' | wc -c
#   권한 비트      → stat -c %a file

stat의 포맷 지정자를 쓰면 출력이 기계 친화적이라 구현 간 차이에 훨씬 강하다. ls 출력을 자르는 코드는 거의 언제나 stat이나 find -printf로 대체할 수 있다.

검증 절차

1
격리된 환경 준비
새 구현이 기본인 배포판 이미지를 컨테이너로 하나 띄운다. 기존 환경은 건드리지 않는다.
2
기존 파이프라인 실행
CI 스크립트, 배포 스크립트, 백업 스크립트를 그대로 돌려 본다.
3
출력 비교
같은 입력에 대해 두 구현의 출력을 나란히 놓고 diff를 본다. 로케일은 고정한다.
4
에러 경로 확인
존재하지 않는 파일, 권한 없는 경로 등 실패 상황의 메시지와 종료 코드를 비교한다.
5
대량 처리 테스트
파일 수만 개 규모의 복사·삭제를 수행해 성능과 동작을 확인한다.
6
위험 스크립트 우선 수정
백업·배포처럼 실패 시 복구가 어려운 경로부터 파싱을 제거한다.
# 두 구현의 출력을 비교하는 간단한 하네스
compare() {
  local desc="$1"; shift
  local a b
  a=$(LC_ALL=C "$@" 2>&1; echo "exit=$?")
  b=$(LC_ALL=C docker run --rm -v "$PWD:/w" -w /w <new-image> "$@" 2>&1; echo "exit=$?")
  if [ "$a" != "$b" ]; then
    echo "=== 차이: $desc"
    diff <(echo "$a") <(echo "$b") | head -20
  fi
}

compare "ls 상세" ls -l testdir
compare "df 헤더" df -h /
compare "없는 파일" cat /nonexistent
compare "권한 없음" cat /root/.bashrc
compare "stat 포맷" stat -c '%n %s %a' testdir/file

로케일을 고정하라

로케일에 따라 날짜 형식, 숫자 구분자, 정렬 순서, 에러 메시지 언어가 달라진다. 스크립트에서는 예외 없이 고정하는 것이 정석이다. 이건 구현 교체와 무관하게 이미 해야 할 일이다.

#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C          # 모든 출력·정렬을 예측 가능하게
export TZ=UTC            # 시각 처리도 고정

# 정렬 순서가 로케일에 따라 달라지는 대표 사례
# LC_ALL=en_US.UTF-8 에서는 대소문자 혼합 정렬이 다르게 나온다
ls | sort                # LC_ALL=C 고정 시 바이트 순서로 안정

안전한 대체 패턴 모음

하려는 일취약한 방법안정적인 방법
파일 크기ls -l | awk '{print $5}'stat -c %s file
수정 시각ls -l 파싱stat -c %Y file
파일 존재 확인ls file 2>/dev/null[ -e file ]
파일 개수ls | wc -lfind . -maxdepth 1 -type f -printf . | wc -c
디스크 여유df -h 파싱df -P --output=avail /path | tail -1
빈 디렉터리 확인ls -A dir | wc -lfind dir -mindepth 1 -maxdepth 1 -print -quit

오른쪽 방식들은 구현이 바뀌어도, 파일 이름에 공백이나 개행이 들어 있어도 잘 동작한다. 후자는 구현 교체보다 훨씬 자주 마주치는 버그 원인이다.

전환 판단

전환을 서두를 이유
  • 메모리 안전성 관련 취약점 클래스 감소
  • 배포판 기본값이 그쪽으로 이동 중
  • 새 환경에서는 어차피 기본이 된다
서두르지 않을 이유
  • 기능적 이득이 사용자에게 보이지 않는다
  • 검증 비용이 조직 규모에 비례
  • LTS 환경이라면 시간이 있다
현실적인 접근
지금 당장 프로덕션 서버를 바꿀 필요는 없다
CI 러너나 테스트 컨테이너를 새 구현 기반으로 하나 돌려 본다
발견된 파싱 코드는 구현 교체와 무관하게 수정한다 (부채 상환)
새로 만드는 이미지·스크립트는 처음부터 안정적인 패턴으로 작성
다음 LTS 전환 시점을 목표로 계획을 세운다

요약하면 이렇다. 지금 해야 할 일은 전환이 아니라 검증이다. 그리고 그 검증 과정에서 나오는 수정 사항은 대부분 원래부터 고쳤어야 할 것들이다.

자주 묻는 질문

GNU coreutils에서 uutils로 바꾸면 무엇이 달라지나요?

일상적인 사용에서는 거의 차이가 없습니다. 드롭인 호환을 목표로 하며 차이가 발견되면 버그로 취급됩니다. 변화의 목적은 기능이 아니라 메모리 안전성 관련 취약점 클래스를 줄이는 것입니다.

어떤 스크립트가 깨질 가능성이 높나요?

명령 출력 문자열을 awk나 cut으로 파싱하는 스크립트, 에러 메시지 문구로 분기하는 코드, 로케일 의존 출력에 기대는 처리입니다. 종료 코드나 기본 플래그에 의존하는 코드는 대체로 안전합니다.

어떻게 검증하면 되나요?

새 구현이 기본인 배포판 컨테이너를 하나 띄우고 기존 CI·배포·백업 스크립트를 그대로 돌려 보세요. 로케일을 LC_ALL=C로 고정한 상태에서 두 구현의 출력과 종료 코드를 비교하면 차이를 빠르게 찾을 수 있습니다.

ls 출력을 파싱하는 코드는 어떻게 고치나요?

stat의 포맷 지정자나 find의 printf를 사용하세요. 파일 크기는 stat -c %s, 수정 시각은 stat -c %Y로 얻을 수 있으며, 출력이 기계 친화적이라 구현 차이와 파일 이름의 특수문자 양쪽에 강합니다.

지금 서버를 바꿔야 하나요?

급하지 않습니다. 단기 지원 릴리스에서 먼저 기본값이 바뀌는 경우가 많으므로, CI 러너나 테스트 환경을 새 구현으로 돌려 보며 다음 LTS 전환에 대비하는 편이 비용 대비 효과가 큽니다.

로케일 고정이 왜 중요한가요?

날짜·숫자 형식, 정렬 순서, 에러 메시지 언어가 로케일에 따라 달라지기 때문입니다. 스크립트에서 LC_ALL=C와 TZ를 고정하면 환경 차이로 인한 오작동이 크게 줄고, 구현 교체 시에도 비교가 쉬워집니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록