서론

시스템 운영자의 평온한 일상을 깨트리는 것은 바로 “장애”입니다. 방금 전 까지 잘 동작하던 서비스가 이상 동작을 하게 되는 것을 발견하는 즉시, 원인을 찾고 서비스 복구를 위해 움직여야 합니다. 원인은 여러가지가 있지만 가장 최근에 수행한 작업의 영향이 있었을 가능성이 가장 큽니다. 가장 마지막 수행한 작업이 무엇인지 복기해서 작업 전 환경으로 원복해서 서비스를 복구하는 것이 가장 기본적입니다.

문제의 발단

Dashing 은 Ruby 기반의 오래 된 데이터 대시보드 도구입니다. CSV Data 파일을 이용해서 대시보드를 구성했고 잘 동작했습니다만 갑자기 CSV Data Parsing 이 안되는 문제가 발생했습니다. CSV Data 를 자동 생성하는 측에서의 변경은 없었다고 합니다. Linux 에서 Windows Docker 기반의 장비로 이전/설치하는 과정에서 발생했기에 OS 변경에 따른 Code 동작 이상이 있는 줄 알고는 Code 도 변경했지만 Parsing 오류는 계속되었고, 더더욱 문제가 꼬여갔습니다.

AD

문제 해결의 실마리를 찾다

보통 CSV Data 는 Excel 내보내기를 이용한다는 것에 착안했서 Excel 내보내기 과정에서 Encoding 이슈가 있는지 시험을 해 보았습니다. Excel 2019 저장 옵션에는 CSV(쉼표로 분리)CSV UTF-8 (쉼표로 분리) 의 두 가지가 있습니다.

두 가지 방식으로 저장한 후, Notepad 에서 다시 읽어 들여서 확인을 했습니다. CSV(쉼표로 분리) 로 저장한 경우, ANSI 형태로 저장합니다. CSV UTF-8 (쉼표로 분리) 로 저장한 경우, UTF-8(BOM) 인 것을 확인했습니다. Parsing 오류를 발생 시킨 이유가 BOM (Byte Order Mark) 문제일 가능성이 높아 보입니다.

Linux 에서 file 명령을 이용해서 2개의 CSV 파일의 Encoding 을 확인하면 다음과 같습니다.

# CSV(쉼표로 분리) 
$ file -i csv_comma.csv
csv_comma.csv: application/csv; charset=iso-8859-1

# CSV UTF-8 (쉼표로 분리)
$ file -i csv_utf8.csv
csv_utf8.csv: application/csv; charset=utf-8

iconv 를 이용해서 CSV(쉼표로 분리) 저장 파일을 UTF-8 로 변환했습니다.

 file -i csv_comma_to_utf8.csv
csv_comma_to_utf8.csv: application/csv; charset=utf-8

이제 CSV 두 개 파일은 모두 UTF-8 입니다만 cmp 명령을 이용해서 비교해 보면 다음과 같이 서로 틀리다고 나옵니다. 즉, Linux 에서는 2개 파일을 단순 구분하기가 쉽지 않습니다.

$ cmp -b csv_comma_to_utf8.csv csv_utf8.csv
csv_comma_to_utf8.csv csv_utf8.csv differ: byte 1, line 1 is 352 M-j 357 M-o

결론

UTF-8 with BOM 파일인 경우, 파일의 첫 번째 3 bytes 를 확인해야만 일반적인 UTF-8 파일과 구분이 가능합니다. Ruby 2.7 에서 이를 식별하는 Code 는 https://stackoverflow.com/questions/44171895/ruby-check-for-byte-order-marker 이용하면 됩니다. 파일 Encoding 식별 예제 Ruby 코드는 다음과 같습니다.

# CSV 파일 encoding 확인
def detect_encoding(file_with_fullpath)
	# charset 정보 획득
	enc = file -i #{file_with_fullpath}.strip.split('charset=').last

	# UTF-8 with BOM 여부 확인
	case enc
	when "utf-8"
		# refer to https://stackoverflow.com/questions/44171895/ruby-check-for-byte-order-marker
		is_bom = File.open(file_with_fullpath) { |f| f.read(3).bytes == [239, 187, 191] }
		if is_bom

                        # UTF-8 with BOM
			enc = 'bom|utf-8'
		else
                        # UTF-8
			enc = 'utf-8'
		end
	when "iso-8859-1"
                # ANSI
		enc = "euc-kr:utf-8"
	end

	return enc
end

AD