최근에 Node.js에서 BigInt값을 처리하게 되면서 오류가 발생하였습니다. 그에 따라 해당 문제가 왜 발생하는지, 어떻게 해결해야 하는지에 대한 트러블 슈팅을 기록하고자 합니다.
왜 발생하였는가?
해당 문제가 발생한 이유는 MySQL의 BigInt와 Node.js상에서의 number값에서 처리할 수 있는 범위가 달라 발생한 문제였습니다.
보편적으로 MySQL에서의 PK의 타입은 Int나 BigInt로 저장하여 Auto Increment를 사용하게 됩니다. PK의 경우 대부분 BigInt를 사용합니다. 저는 기존에는 Number 타입을 통해 해당 값을 처리해왔는데요. 현재 시스템 상 BigInt를 처리하기 위해서는 Number 타입을 사용하는 것이 아닌 BigInt를 사용해야 합니다.
왜 BigInt를 사용해야 하는가에 대해 알아보기 위해서는 우선 Number와 BigInt의 차이점에 대해서 알아보아야 합니다. 우선 Number 타입의 경우 53비트까지만 정수를 안정적으로 처리할 수 있습니다. 이는 Number타입이 IEEE 754 배정밀도 부동소수점을 사용하기 때문입니다.
IEEE 754 배정밀도 부동소수점?
배정밀도 부동소수점은 부동소수점 숫자를 64비트(8바이트)로 저장하는 방식입 니다. 일반적으로 double 타입으로 사용됩니다. 세부 구조는 다음과 같습니다.
S | EEEEEEEEEEE | MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
S: 1비트(부호: 0=양수, 1=음수)
E: 11비트(지수: 수의 크기를 결정, 2의 거듭제곱)
M: 52비트(가수: 실제 숫자의 유효숫자, 정밀도)
실제로 값을 저장하는 부분인 가수의 크기는 총 52비트이지만 첫번째 1이 암묵적으로 존재하기 때문에 최대 53비트까지 처리됩니다. 따라서 2^53 = 9,007,199,254,740,992 까지 안정적으로 처리할 수 있고 그 이상의 값이 들어오는 경우 부동소수점의 오차가 발생하게 됩니다. 이는 다른 프로그래밍 언어에서도 발생하고 있는 문제입니다.
어떻게 해결해야 하는가?
저는 요청시 해당 값을 예외처리하기 위해 다음과 같은 코드를 추가적으로 작성하였습니다.
import {
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
registerDecorator,
} from "class-validator";
@ValidatorConstraint({ async: false })
export class IsBigIntConstraint implements ValidatorConstraintInterface {
validate(value: any): boolean {
// 값이 BigInt이거나 BigInt로 변환 가능해야 함
return typeof value === "bigint" || (!isNaN(value) && BigInt(value).toString() === value.toString());
}
defaultMessage(): string {
return "Value must be a BigInt.";
}
}
export function IsBigInt(validationOptions?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsBigIntConstraint,
});
};
}
class-vaildator를 통해 예외처리를 진행하고 있어서 해당 요청값이 들어오는 경우 예외처리를 하고 있습니다.
하지만 /api/v1/order/1 처럼 path param으로 BigInt 값을 처리해야하는 경우는 처리하지 못합니다. 이 경우 다음과 같이 처리합니다.
import { BadRequestException, Injectable, PipeTransform } from "@nestjs/common";
@Injectable()
export class ParseBigIntPipe implements PipeTransform<string, bigint> {
transform(value: string): bigint {
try {
return BigInt(value);
} catch (error) {
throw new BadRequestException(`Validation failed. "${value}" is not a valid bigint.`);
}
}
}
값을 변환하는 과정에서 오류가 발생하기 때문에 Pipe를 사용하여 값을 변환하도록 하였습니다.
결론
식별자가 2^53 = 9,007,199,254,740,992을 넘어가지 않으면 Number 타입을 써도 상관없습니다. 하지만 데이터 범위가 2^53을 넘는 값을 다루어야 하는 경우 BigInt를 사용하는 것을 고려해야합니다.
'개발 > Node.js (NestJS)' 카테고리의 다른 글
[Node.js] Prisma VS MikroORM VS Drizzle (2) | 2024.11.08 |
---|---|
[Node.js] 413 Payload Too Large 오류 해결 (0) | 2024.11.01 |
[Node.js] Prisma 한방 쿼리로 성능 개선 (0) | 2024.07.25 |
[Node.js] puppeteer libatk-bridge-2.0.so.0 이슈 해결 (0) | 2023.03.02 |
[Node.js] Express에 HTTPS 적용하기 (0) | 2022.03.04 |