TECH 으로 돌아가기
TECH HACKER NEWS 오늘 8분 읽기 21 READS

strtod의 오류 감지, 표준 C에는 이식 가능한 방법이 없다

C로 숫자 문자열을 파싱할 때 많은 개발자가 무심코 strtod, strtof, strtold 같은 함수를 쓴다. 설정 파일에서 읽은 값, 사용자 입력, 네트워크로 받은 텍스트를 실수(float)로 바꾸는 가장 기본적인 수단이기 때문이다. 그런데 이 함수들이 "변환에 실패했는지" 혹은 "값이 표현 범위를 벗어났는지"를 이식 가능한 방식으로 판정하는 일이 생각보다 까다롭다는 점은 잘 알려져 있지 않다. 한 개발자가 자신의 블로그에서 C 표준과 POSIX, 그리고 glibc·musl의 실제 동작을 대조하며 이 문제를 정리했는데, 결론부터 말하면 표준 C만으로는 오류를 확실히 감지할 깔끔한 방법이 없다.

math_errhandling에 묶인 오버플로 처리

문제의 뿌리는 이 변환 함수들이 수학 함수의 오류 처리 규약을 공유한다는 데 있다. C 표준에는 math_errhandling이라는 매크로가 있어, 구현이 어떤 오류 보고 방식을 지원하는지를 나타낸다. MATH_ERRNO는 전역 변수 errno를, MATH_ERREXCEPT는 부동소수점 예외를 의미한다. 표준은 strtod 계열이 오버플로를 일으키면 HUGE_VAL 등을 반환하되, errno에 ERANGE를 넣을지 혹은 "overflow" 예외를 올릴지는 전적으로 math_errhandling이 무엇을 지원하는가에 달려 있다고 규정한다. 즉 musl처럼 errno 지원을 광고하지 않는 구현에서는, 이 함수들이 오버플로가 나도 errno를 건드리지 않는다.

반환값만으로는 오류 여부를 알 수 없다는 점이 핵심이다. HUGE_VAL은 정상적인 큰 값으로도 나올 수 있으므로, 오버플로를 판정하려면 반드시 두 오류 보고 메커니즘 중 하나를 확인해야 한다. 그런데 어느 쪽이 유효한지는 다시 math_errhandling을 봐야 알 수 있으니, 진정으로 이식 가능한 코드라면 호출 전에 errno를 0으로 초기화하고 feclearexcept(FE_OVERFLOW)를 호출한 뒤, 반환값이 HUGE_VAL일 때 math_errhandling 값에 따라 errno == ERANGE와 fetestexcept(FE_OVERFLOW) 중 맞는 쪽을 검사해야 한다. 언더플로는 사정이 더 나쁜데, 표준은 언더플로를 보고하는 것 자체를 구현의 선택 사항으로 두고 있어 아예 감지하지 못할 수도 있다.

POSIX는 다르게 말한다

리눅스 맨페이지가 이 복잡성을 전혀 언급하지 않는 데에는 이유가 있다. POSIX는 같은 함수를 표준 C보다 엄격하게 규정하기 때문이다. POSIX에서는 값이 표현 범위를 벗어나거나 언더플로가 발생하면 math_errhandling과 무관하게 무조건 errno를 ERANGE로 설정해야 한다. 덕분에 POSIX 환경만 겨냥한다면 "errno를 0으로 두고 호출 후 ERANGE인지 확인"하는 익숙한 패턴이 통한다. 다만 글쓴이는 맨페이지(strtod(3)나 strtod(3p))가 이것을 표준 C에 대한 확장이라고 어디에도 명시하지 않는다는 점을 문제 삼는다. 더 나아가 표준 C의 errno.h 조항을 근거로 보면, 함수 설명에 errno 사용이 문서화돼 있는데도 math_errhandling & MATH_ERRNO가 0일 때 errno를 설정하는 POSIX 동작은 표준에 부합하지 않을 소지가 있다고 본다.

파싱 실패는 또 다른 함정

지금까지는 모두 오버플로와 언더플로 이야기다. 문자열이 아예 숫자 형태가 아니어서 변환이 이뤄지지 않은 경우, 표준 C는 어떤 오류도 규정하지 않는다. 이때는 그냥 0이 반환될 뿐이며, 오류를 알려면 endptr 인자를 넘긴 뒤 endptr == nptr인지, 즉 시작 위치에서 포인터가 한 글자도 전진하지 않았는지를 확인해야 한다. 흔히 쓰는 "errno를 0으로 초기화하고 호출 후 0이 아니면 오류"라는 관용구가 여기서 깨진다. POSIX조차 변환 실패 시 errno를 EINVAL로 설정할 수 있다(may)고만 해서 사실상 구현에 맡겨 두기 때문이다.

실제 차이는 glibc와 musl에서 그대로 드러난다. 파싱할 수 없는 입력을 넘기면 glibc의 strtod는 errno를 전혀 건드리지 않아 0이 찍히는 반면, musl은 EINVAL을 설정해 값 22가 찍힌다. 이 동작 차이는 리눅스 맨페이지에 문서화돼 있지 않다. 글쓴이는 musl의 동작 역시 표준 C 관점에서는 비적합일 수 있다고 보지만, musl 쪽이 "수학 함수에서 errno를 쓰지 않으니 strtod의 errno 조항도 적용되지 않는다"고 방어할 여지는 있다고 덧붙인다. 어느 쪽이든 표준의 문구가 더 명확해질 필요가 있다는 것이 그의 결론이다.

실무자가 가져갈 지침

정리하면, 변환 실패를 확인하는 유일하게 믿을 만한 방법은 endptr을 넘겨 endptr == nptr인지 보는 것이다. 이때 errno는 신뢰할 수 없다. 오버플로는 POSIX만 대상으로 한다면 errno == ERANGE로 간단히 잡히지만, 여러 libc를 아우르려면 errno와 부동소수점 예외를 모두 준비하고 math_errhandling에 따라 분기해야 한다. fetestexcept 계열을 쓸 때는 컴파일러가 부동소수점 환경 접근을 인지하도록 gcc의 -ftrapping-math(기본값이며 -ffast-math에서 꺼짐)를 유지하거나 #pragma STDC FENV_ACCESS ON을 고려해야 한다. 다만 이 프래그마는 gcc가 지원하지 않는다는 한계가 있다.

언더플로 감지는 사실상 포기하는 편이 현실적이다. 보고 여부가 구현 정의 사항이라 표준상 문서화가 요구되지만, 실제로는 clang처럼 그런 세부를 문서화하지 않는 구현이 많다. 꼭 필요하다면 결과가 0.0일 때 입력 문자열에 지수부 앞 0이 아닌 숫자가 있었는지 직접 검사하는 우회법이 있으나, 엣지 케이스가 많아 정확히 구현하기 어렵다. 결국 한국의 서버·임베디드 개발자에게 이 글의 교훈은 분명하다. 입력 검증이 중요한 파서라면 표준 라이브러리 함수의 반환값이나 막연히 "0이 아닌 errno"에 기대지 말고, endptr 확인을 기본으로 삼되 플랫폼별 오버플로 처리 방식을 명시적으로 다뤄야 한다는 것이다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://sebsite.pw/w/20261009-strtod.html
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기 →
처리 중...