본문 바로가기
빌드노트 검색

빌드노트

같은 파일을 sort했는데 서버마다 순서가 다르다

같은 GNU sort도 로케일에 따라 줄 순서가 달라진다. 같은 33바이트를 C와 작업 폴더에 만든 en_US.UTF-8로 정렬하고 sort -c의 판정 차이를 확인했다. 실행 환경의 우선순위를 읽고 바이트 비교용 정렬과 검사에만 LC_ALL=C를 적용한다.

같은 텍스트 파일에 같은 GNU sort 명령을 실행했는데 Linux 서버마다 줄 순서가 다르다면, 정렬 명령이 실행되는 환경의 로케일을 확인한다. 기본 문자열 정렬은 LC_COLLATE의 비교 규칙을 따르므로, 늘 바이트 순서가 되는 것은 아니다. 파일 비교용으로 재현 가능한 바이트 정렬이 필요하다면 해당 명령에만 LC_ALL=C를 붙인다. 사람이 읽는 이름 목록에 언어권의 정렬 규칙이 필요한지는 별도로 판단한다.

이 동작은 GNU coreutils 9.4 공식 매뉴얼의 sort 절 (새 탭에서 열림)에 설명되어 있다. 아래는 실제 서버 장애 기록이 아니라, 같은 입력 바이트에 로케일만 바꿔 실행한 격리 실험이다.

LANG만 같다고 정렬 조건이 같은 것은 아니다

순서가 달라진 서버나 컨테이너에서 다음 명령으로 sort 구현, 현재 로케일, 설치 목록을 확인한다. 배치 작업에서만 문제가 난다면 그 작업이 실행되는 환경에서 조회해야 한다. 여기서는 Linux의 GNU sort와 Bash 또는 POSIX 계열 셸을 전제로 한다.

sort --version
locale
locale -a

locale 출력 중 LC_ALL, LC_COLLATE, LANG을 읽는다. 비어 있지 않은 LC_ALL이 우선하고, 없으면 해당 범주의 LC_COLLATE, 그것도 없으면 LANG을 따른다. 두 서버의 LANG이 같더라도 한쪽에 LC_ALL이 설정되어 있으면 비교 규칙은 달라질 수 있다. 이 우선순위는 glibc의 로케일 범주 설명 (새 탭에서 열림)에 나와 있다.

따라서 상위 환경에 LC_ALL이 있는 상태에서 LC_COLLATE=C만 붙이면 원하는 기준으로 바뀌지 않는다. 이번 실험에서도 LC_ALL=en_US.UTF-8과 LC_COLLATE=C를 함께 전달한 결과는 en_US 정렬과 같았다. 반면 그 환경에서 시작한 셸의 명령 앞에 LC_ALL=C를 붙이면 C 정렬로 바뀌었다.

locale -a는 시스템이 지원하는 로케일 목록을 보여 준다. glibc의 Locale Names 설명 (새 탭에서 열림)처럼 C와 POSIX 이외의 이름은 시스템에 따라 다를 수 있다. 사용할 이름을 목록과 대조하고, 지원하지 않는 로케일이라는 경고가 나오면 그 실행을 의도한 로케일의 결과로 해석하지 않는다.

같은 33바이트에서 달라진 순서

실행 환경은 WSL2의 Ubuntu 24.04.4 LTS, GNU coreutils 9.4, glibc 2.39였다. 기본 환경의 관련 값은 LANG=C.UTF-8, LC_COLLATE="C.UTF-8", 빈 LC_ALL이었다. 설치 목록의 전체 출력은 다음과 같았다.

C
C.utf8
POSIX

이 세 이름만 비교했을 때는 아래 입력의 정렬 결과가 모두 같았다. POSIX는 C의 별칭 (새 탭에서 열림)이므로 세 이름을 서로 다른 세 언어권의 정렬 규칙으로 세면 안 된다.

다른 비교 규칙도 실제로 실행하기 위해, 설치되어 있던 en_US 정의와 UTF-8 문자맵을 localedef로 실험 전용 폴더에 컴파일했다. 각 명령에만 LOCPATH와 LC_ALL=en_US.UTF-8을 전달했다. localedef 매뉴얼의 디렉터리 출력 방식 (새 탭에서 열림)과 LOCPATH의 검색 규칙 (새 탭에서 열림)을 따른 준비다. 시스템 로케일을 설치하거나 설정 파일을 바꾸지 않았으며, 실험 뒤에도 설치 목록과 기본 환경은 같았다.

다음 입력으로 대소문자·숫자·비ASCII 문자의 위치를 비교할 수 있다. UTF-8 터미널의 빈 작업 폴더에서 실행하면 names.txt가 새로 생긴다. 두 번째 정렬은 en_US UTF-8 로케일을 사용할 수 있을 때 실행하고, 설치 목록의 실제 이름에 맞춘다. 해당 로케일이 없다면 C 결과만 확인할 수 있으며, 이름만 지정해 비교가 되었다고 판단하지 않는다.

printf '%s\n' 'b' 'A' '10' 'a' '2' 'B' 'z' 'Z' 'ä' 'é' '가' 'alpha' > ./names.txt
LC_ALL=C sort ./names.txt
LC_ALL=en_US.UTF-8 sort ./names.txt

입력은 UTF-8·LF로 저장한 12줄, 33바이트였다. 같은 파일을 읽은 C 정렬의 전체 출력이다.

10
2
A
B
Z
a
alpha
b
z
ä
é
가

같은 입력을 en_US.UTF-8로 정렬한 전체 출력은 다음과 같았다.

가
10
2
a
A
ä
alpha
b
B
é
z
Z

C에서는 Z가 a보다 앞이고, ä는 ASCII 문자들 뒤에 있다. 이번 en_US 결과에서는 a, A, ä가 이어지고 가가 맨 앞으로 옮겨졌다. 이 위치가 모든 시스템의 en_US에서 같다고 일반화할 수는 없다. 동일한 입력과 GNU sort 버전에서도 로케일 선택만으로 줄 순서가 달라진다는 것이 이 비교에서 확인한 사실이다. 기본 환경·C.utf8·POSIX의 출력은 C 결과와 같았다.

10은 두 결과 모두 2보다 앞이었다. 숫자의 크기로 정렬하는 실험은 아니다. C 결과는 입력을 바이트 단위로 별도 정렬해 계산한 순서와도 일치했다. C의 문자열 비교가 바이트 순서를 따르는 근거는 glibc의 Collation Functions (새 탭에서 열림)와 strcmp 설명 (새 탭에서 열림)에서 확인할 수 있다. UTF-8은 문자를 바이트로 표현하는 방식이며, 이름에 UTF-8이 있다고 비교 규칙까지 같은 것은 아니다.

정렬한 명령과 검사하는 명령의 기준을 맞춘다

비교용 파일을 만들 때는 양쪽 서버의 정렬 명령에 같은 설정을 붙인다. 아래에서 names.txt는 실제 입력 파일로 바꾸고, byte-sorted.txt는 입력과 다른 새 출력 이름을 쓴다. 첫 명령으로 바이트 정렬 결과를 만들고, 둘째 명령으로 그 기준에 맞는 순서인지 검사한다.

LC_ALL=C sort ./names.txt > ./byte-sorted.txt
LC_ALL=C sort -c ./byte-sorted.txt

이번 실행에서 두 명령은 종료 코드 0이었고, 검사 명령은 아무것도 출력하지 않았다. 하지만 같은 C 정렬 결과를 en_US.UTF-8 기준의 sort -c로 검사하면 6행의 a에서 순서 위반을 보고하고 종료 코드 1을 반환했다. 반대 방향으로 en_US 정렬 결과를 C 기준으로 검사하면 2행의 10에서 실패했다. -c는 다시 정렬하지 않고 현재 줄 순서를 검사하며, 그 검사도 선택한 로케일을 따른다. 정렬된 파일이라는 말에는 비교 기준이 함께 있어야 한다.

명령 앞의 LC_ALL=C는 그 호출에 적용된다. 비교 작업 때문에 로그인 셸이나 시스템 전체의 로케일을 바꿀 필요는 없다. 같은 LC_ALL=C와 같은 옵션을 썼는데도 결과가 다르다면 입력 바이트·인코딩·줄바꿈과 sort 구현을 대조한다. 이 지정은 인코딩이나 Unicode 표현을 통일하지 않는다.

이번 검증은 한 WSL 환경에서 같은 UTF-8·LF 파일을 전체 행으로 정렬한 범위다. 실제 서버 두 곳, 다른 glibc 버전의 로케일 데이터, -n·-f·-k·-u 옵션의 조합은 검증하지 않았다. 사람이 읽을 목록에는 필요한 언어권의 정렬을 선택하고, 바이트 비교용 산출물을 만드는 명령과 검사에는 LC_ALL=C를 함께 명시한다.

← 전체 글 보기