gwimong's blog Software Engineer

Javascript에서 9로 나눈 나머지가 틀리는 이유

부동소수점 정밀도 문제로 인한 모듈로 연산 오류


1. 문제 발견

코딩 테스트 문제 하나를 풀고 있다.

큰 숫자를 9로 나눈 나머지를 구하는 문제

로직은 간단하다. 금방 통과할 줄 알았다.

그런데 일부 케이스가 계속 실패한다.

예상: 모든 테스트 통과
실제: 특정 케이스만 실패

코드를 확인해도 문제가 보이지 않는다.

계산기로 직접 확인 → 내 답이 맞다 → Javascript가 틀린 값을 낸다.

2. 문제 재현

const num = 9007199254740992;
console.log(num % 9);  // 예상: 1, 실제: 0

계산기로 확인해보면 분명 1이 나와야 한다.

하지만 Javascript는 0을 반환한다.

다른 숫자들도 테스트해봤다.

console.log(9007199254740993 % 9);  // 예상: 2, 실제: 1
console.log(9007199254740994 % 9);  // 예상: 3, 실제: 2

모든 결과가 1씩 작게 나왔다.

3. 원인 분석

3.1 Number.MAX_SAFE_INTEGER

Javascript의 Number 타입은 IEEE 754 부동소수점을 사용한다.

정수를 안전하게 표현할 수 있는 최댓값이 정해져 있다.

console.log(Number.MAX_SAFE_INTEGER);
// 9007199254740991 (2^53 - 1)

이 값을 넘어가면 정밀도 문제가 발생한다.

3.2 정밀도 손실

const large = 9007199254740992;  // 2^53
console.log(large);              // 9007199254740992
console.log(large + 1);          // 9007199254740992 (!)
console.log(large + 2);          // 9007199254740994

MAX_SAFE_INTEGER를 넘으면 1씩 증가하지 않는다.

2씩 건너뛰면서 표현된다.

그래서 나머지 연산도 잘못된 결과를 낸다.

왜 이런 일이 생기나

Javascript의 Number는 IEEE 754 배정밀도 부동소수점이다.

64비트를 이렇게 나눠 쓴다.

1비트: 부호
11비트: 지수
52비트: 가수 (실제 숫자 값)

정수를 표현할 때는 가수부 52비트 + 암묵적 1비트를 사용한다.

그래서 2^53 - 1까지는 모든 정수를 정확히 표현할 수 있다.

하지만 2^53부터는 가수부로 표현할 수 없는 정수가 생긴다.

// 2^53 = 9007199254740992는 표현 가능
// 2^53 + 1 = 9007199254740993는 표현 불가능
// 가장 가까운 표현 가능한 숫자인 2^53으로 반올림됨

console.log(9007199254740992 + 1 === 9007199254740992);  // true

* 2^53 이후로는 2 간격으로만 표현된다.
* 2^54 이후로는 4 간격, 2^55 이후로는 8 간격이다.

표현할 수 없는 숫자는 가장 가까운 값으로 자동 반올림된다.

부동소수점의 가수부 한계 때문에 큰 정수는 일부만 표현 가능하다.

3.3 왜 9로 나눈 나머지에서 문제가 됐나

코딩 테스트에서 큰 숫자를 9로 나눈 나머지를 구하는 문제였다.

입력값이 2^53을 넘어가는 케이스가 있었다.

처음에는 로직 문제인 줄 알았는데 자료형 한계였다.

Javascript의 Number 타입은 2^53 - 1까지만 정확하게 표현할 수 있다.

4. 해결 방법

4.1 BigInt 사용

BigInt는 임의 정밀도 정수를 다룰 수 있다.

const num = 9007199254740992n;  // 끝에 n을 붙인다
console.log(num % 9n);           // 1n (올바른 결과)

연산자 양쪽 모두 BigInt여야 한다.

const num = 9007199254740992n;
console.log(num % 9);   // Error: Cannot mix BigInt and other types
console.log(num % 9n);  // 1n (정상)

4.2 문자열로 입력받는 경우

코딩 테스트에서는 큰 숫자를 문자열로 받는 경우가 많다.

const input = "9007199254740992";
const num = BigInt(input);
console.log(num % 9n);  // 1n

결과를 Number로 변환할 때도 주의한다.

const result = num % 9n;
console.log(Number(result));  // 1 (안전한 범위이므로 변환 가능)

4.3 주의사항

BigInt는 부동소수점 연산을 지원하지 않는다.

const big = 10n;
console.log(big / 3n);    // 3n (소수점 버림)
console.log(big / 3);     // Error

JSON 직렬화도 지원하지 않는다.

JSON.stringify({ value: 10n });  // Error

필요하다면 직접 변환 로직을 작성해야 한다.

BigInt는 정수 연산만 지원하며, Number와 혼용할 수 없다.

5. 정리

Javascript의 Number는 부동소수점이라 큰 정수를 다룰 때 정밀도 문제가 생긴다.

2^53 - 1을 넘어가면 모든 정수를 정확히 표현할 수 없다.

핵심:

  • Number.MAX_SAFE_INTEGER는 9007199254740991
  • 이 값을 넘으면 정밀도 손실 발생
  • 큰 정수 연산에는 BigInt 사용
  • BigInt는 끝에 n을 붙여 표기
  • 양쪽 모두 BigInt여야 연산 가능

코딩 테스트에서 큰 숫자를 다룬다면 BigInt를 먼저 고려한다.

특히 나머지 연산이나 정확한 정수 계산이 필요한 경우 필수다.

입력 범위를 항상 확인하고, 2^53을 넘을 가능성이 있다면 BigInt를 사용한다.


Comments

Content