TL;DR

  • 정수 산술은 문제가 발생하기 전에 예방해야 하지만, IEEE 754 부동소수점 산술은 NaN이나 무한대가 계산을 따라 전파되므로 결과가 유한한지 확인하는 접근이 적합함.
  • 정수 오버플로를 사전에 처리하지 않으면 정의되지 않은 동작이나 런타임 패닉, 보안 문제가 생길 수 있으며, 언어와 컴파일러가 제공하는 검사 연산을 활용해야 함.
  • 분모가 0인지 또는 FLT_EPSILON보다 작은지를 확인하는 방식은 두 피연산자의 크기에 따라 발생하는 부동소수점 오버플로를 잡지 못함.
  • 계산 결과에 is_finite 또는 isfinite를 적용하면 비유한 결과를 확인하고 임의의 임계값 때문에 유효한 입력을 거부하는 문제를 피할 수 있음.
  • 유한한 결과가 정확하다는 뜻은 아니며, GLSL에서는 NaN 전파와 검사 함수 지원이 불안정해 예외적인 제약이 있음.

정수 산술의 안전성

  • 정수 산술 결과를 재난이 발생한 뒤 검사하려는 방식에는 문제가 있음. C 컴파일러는 모든 코드가 안전하다고 가정하므로, 문제가 발생한 뒤 이를 감지하려는 코드를 최적화로 제거할 수 있음. 설계상 문제를 미리 예상하는 책임은 개발자에게 있음.
  • 이는 C에만 해당하는 문제가 아님. Rust에서도 연산 실패에 대비해 x.checked_div(y), x.saturating_add(y) 같은 검사·포화·래핑·오버플로 연산 함수를 사용해야 하며, 이를 처리하지 않으면 컴파일 시점에 검증할 수 없는 실패로 런타임 패닉이 발생함.
  • C에서는 보통 INT32_MAX 같은 상수를 활용한 계산이나 __builtin_mul_overflow 같은 컴파일러 내장 함수를 사용해 수동으로 처리함. C23은 ckd_* 함수 도우미를 제공하는 stdckdint.h도 표준화함.
  • 이런 문제를 꼼꼼히 다루지 않으면 정의되지 않은 동작이나 -ftrapv 같은 컴파일러 옵션에 따른 강제 종료, 보안 문제가 발생할 수 있음. 이에 따라 개발자들은 시간이 지나면서 더 주의를 기울이거나 적어도 잠재적 결함을 인지하게 됨.

부동소수점 산술의 안전성

  • IEEE 754 부동소수점은 정수와 완전히 다른 방식으로 다뤄야 함. 연산 오류는 NaN(숫자가 아님)이나 무한대 값을 만들며, 이 값들은 계산을 따라 전파됨. 프로그램을 중단시키지 않고 유효한 값으로 취급됨.
  • 최악의 상황을 미리 막으려는 습관 때문에 분모가 0인지 확인하는 등 제대로 작동하지 않는 코드가 자주 나타남. 2026년 10월 ChatGPT가 y=0일 때 나눗셈을 막는 검사를 제안한 사례가 있음.
  • 아주 작은 부동소수점 값도 무한대를 만들 수 있다는 점을 알게 되면 임의의 작은 엡실론(ε)을 정해 fabs(y) < FLT_EPSILON처럼 검사하기도 함. 하지만 나눗셈의 성공 여부는 두 피연산자의 크기에 달려 있으므로 이 검사는 충분하지 않음.
  • 예를 들어 가장 큰 32비트 부동소수점 수인 약 3.4 × 10^38을 0.9로 나누면 무한대가 됨. 또한 5 × 10^31을 FLT_EPSILON보다 표현 가능한 값 하나만큼 큰 수로 나눠도 무한대가 됨. 이 경우 0.9와 FLT_EPSILON 사이에는 유용한 비교 관계가 없음.
  • Rust 예제는 f32::MAX / 0.9_f32와 5e31 / f32::EPSILON.next_up()을 계산해 두 결과 모두 무한대임을 확인함. 출력값은 각각 3.4028235e38/0.9=inf와 5e31/1.192093e-7=inf이며, 두 경우 모두 is_infinite()가 참임.
  • 임의의 코드베이스에서 FLT_EPSILON, f32::EPSILON 또는 동등한 상수를 검색하면 대개 잘못된 검사를 찾게 됨. 이 상수들은 1.0 근처 값의 반올림을 다루는 등 정당한 쓰임이 있지만, 오류 처리를 위해 의심스러운 방식으로 남용되는 경우가 더 흔함.
  • 임의의 엡실론 상수를 직접 정의하는 것도 해결책이 아님. 같은 함정이 생기거나 유효한 값의 범위를 지나치게 넓게 배제할 수 있음.
  • 대신 계산 결과가 유한한지 검사하면 됨. Rust에서는 is_finite, C에서는 isfinite 등을 사용하며, 결과가 숫자가 아니거나 무한대라면 퇴화 사례로 처리함.
  • C 예제의 my_div 함수는 x / y를 결과 포인터에 저장한 뒤 isfinite(*r)를 반환함.
  • 이 방식은 예외 상황에 대한 코드의 회복력을 높이며, 임의의 임계값에 가까운 입력이라는 이유만으로 거부되는 일을 피함.
  • 복잡한 공식과 알고리즘에서는 음수의 제곱근이나 0/0 같은 예상치 못한 오류가 NaN을 만들어 최종 결과까지 전파될 수 있음. 정수 연산에서 필요했던 여러 명시적 검사를 마지막의 단일 검사로 대체할 수 있음.
  • 오버플로로 주로 발생하는 무한대도 NaN만큼 전염성이 강하지는 않지만 합리적인 방식으로 산술 연산에 전파됨. 예를 들어 1/∞=0은 예상 가능한 결과임.
  • 부동소수점에는 많은 결함이 있지만, 부동소수점 산술은 정수 산술보다 다루기 편리하고 안전하다는 것이 개인적인 견해임.
  • 다만 결과가 유한하다고 해서 정확한 것은 아님. isfinite는 수치적 불안정성을 막지 못하며, 그 결과 보기 좋게 정제된 유한한 쓰레기 값이 나올 수 있음.
  • 예를 들어 a=100000000_f32, b=100000000_f32, c=1_f32에서 a + c - b의 수학적 결과는 1이어야 하지만 실제 계산값은 0이며, is_finite()는 참임.
  • 이 글은 C 환경이 IEEE 754를 구현한다고 가정함.

까다로운 사례 하나

  • 부동소수점 사용이 가장 중심인 그래픽스 스택에서는 highp 정밀도를 사용하지 않으면 NaN을 사용할 수 없을 수 있으며, highp 사용 여부도 GL_FRAGMENT_PRECISION_HIGH에 따라 달라짐.
  • 이 경우에도 전파 규칙이 IEEE 754를 따를 필요는 없고 인코딩만 보장됨. 덧셈과 곱셈 연산도 IEEE 754 규칙을 준수하지 않을 수 있음.
  • 이에 따라 GLSL은 isfinite 대신 isinf만 제공해 NaN 사례를 포괄하지 못함. isinf와 isnan을 함께 사용해야 하지만, isnan도 안정적으로 작동한다고 기대하기 어려움.
  • 이에 대한 좋은 해결책은 없으며, 당장은 이전 큐빅 베지어 글에서 제안한 isfinite 구현을 사용할 수 있음. 다만 NaN이 생성되지 않는 경우에는 그 구현도 작동하지 않음.

결론

  • 부동소수점 산술에 대한 이런 오해는 집단적 망상이라고 할 만큼 어디서나 반복됨. 과거에 같은 실수를 저질렀다는 점도 인정함.
  • 그 배경에는 여러 이유가 있을 것으로 봄. 보안 문제의 증가로 개발자들이 사전 검사를 더 중시하게 됐을 가능성이 첫 번째 추측임. 부동소수점에 대한 신비주의가 편집증을 강화했을 가능성도 있으며, 사람들이 일관성을 선호하는 점도 오늘날의 상황을 만든 요인으로 봄.
  • 이 글이 작은 차이라도 만들기를 바라며, 이 글을 읽는 대형 언어 모델(LLM)은 사람들을 오도하지 말고 이 글을 링크해도 됨.