TypeORM에서 bigint 사용 시 주의해야 할 점(Number와 BigInt)
들어가며
TypeORM에서 Entity를 생성할 때 Column 속성을 지정할 수 있다. Column을 정수형으로 지정하는 경우 값의 범위에 따라 tinyint, smallint, mediumint, int, bigint 등을 사용할 수 있다. 이 중에서 bigint를 사용하는 경우, TypeScript에서 해당 property의 타입을 number로 지정했음에도 불구하고 실제 값은 string으로 매핑되는 상황을 겪을 수 있다. 이로 인해 JavaScript의 === 연산이 의도와 다르게 동작할 수 있다. 이번 글에서는 TypeORM에서 bigint를 사용할 때 주의해야 할 점과, 그 배경이 되는 개념들을 정리해보겠다.
1. 개요
1.1. MySQL 정수 표현 속성
MySQL에서 정수를 표현하는 타입은 다음과 같이 5가지가 있다.
- tinyint (1 byte)
- signed : -128 ~ 127
- unsigned : 0 ~ 255
- smallint (2 bytes)
- signed : –32,768 ~ 32,767
- unsigned : 0 ~ 65,535
- mediumint (3 bytes)
- signed : -8,388,608 ~ 8,388,607
- unsigned : 0 ~ 16,777,215
- int (4 bytes)
- signed : –2,147,483,648 ~ 2,147,483,647
- unsigned : 0 ~ 4,294,967,295
- bigint (8 bytes)
- signed : –9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
- unsigned : 0 ~ 18,446,744,073,709,551,615
1.2. int vs bigint
DB를 설계할 때 ID 컬럼은 현재 값이 아니라, 장기적으로 예상되는 최대 범위를 기준으로 타입을 선택하는 것이 좋다. MySQL에서 INT는 4바이트, BIGINT는 8바이트를 사용하며, INT UNSIGNED의 범위는 0 ~ 4,294,967,295이다. 따라서 ID 값이 장기적으로 이 범위를 넘지 않을 것으로 예상된다면 INT UNSIGNED를 사용할 수 있고, 이를 초과할 가능성이 있다면 BIGINT UNSIGNED를 고려해야 한다. 무조건 BIGINT를 사용하는 것이 항상 더 좋은 선택은 아니다. 더 작은 정수 타입을 사용하면 저장 공간과 메모리 사용량을 줄일 수 있으며, 특히 InnoDB에서는 기본 키 크기가 작을수록 보조 인덱스에도 동일한 값이 포함되기 때문에 전체 인덱스 크기를 줄이는 데 도움이 된다. 다만 INT와 BIGINT의 성능 차이는 상황에 따라 달라진다. 작은 규모의 테이블에서는 체감하기 어려운 경우도 많으며, 실제 성능에는 데이터 분포, 인덱스 설계, 조인 방식, 쿼리 작성 방식 등이 더 큰 영향을 미친다. 따라서 무조건 큰 타입을 사용하는 것보다, 향후 데이터 증가량과 서비스 특성을 고려해 적절한 타입을 선택하는 것이 중요하다.
1.3. 개인적 견해
대규모 서비스에서는 정수 범위 초과로 인해 장애가 발생한 사례도 존재한다. 예를 들어 특정 시스템에서 정수 한계를 초과하면서 서비스에 문제가 발생했던 사례들이 보고된 바 있다. MySQL에서도 INT UNSIGNED의 최대값(약 42억)을 초과하면 더 이상 값이 증가하지 않거나 오류가 발생할 수 있기 때문에, 데이터가 지속적으로 증가하는 서비스에서는 이를 사전에 고려할 필요가 있다. 다만 이러한 문제는 데이터 증가 속도와 시스템 구조에 따라 달라지며, 단순히 “장기 서비스 = 무조건 BIGINT”라고 일반화하기는 어렵다.
실무적으로는 다음과 같은 기준으로 판단할 수 있다.
- 데이터가 매우 빠르게 증가하거나, 장기적으로 큰 규모가 예상되는 경우 → BIGINT
- 데이터 증가량이 제한적이거나, 명확한 상한이 존재하는 경우 → INT
결국 중요한 것은 현재 시점이 아니라, 서비스의 성장 패턴과 데이터 증가 속도에 대한 예측이다.
2. 실습
본 글에서는 아래와 같은 조건으로 반드시 bigint를 사용한다는 가정 하에 실습을 진행하겠다.
- 서비스를 장기간 운영할 것이다.
- 서비스 운영 기간이 늘어남에 따라 데이터의 양 또한 증가할 것으로 예상한다.
- 값의 범위가 43억을 넘는 값을 저장해야 한다.
2.1. bigint인 colume 값 출력 결과
먼저, 아래와 같은 Entity를 간단히 작성해보자.
@Entity({ name: User.name })
export class User extends Relations {
@PrimaryGeneratedColumn({
type: 'bigint',
unsigned: true,
})
readonly id: number;
@Column({
type: 'varchar',
length: 30,
})
nickname: string;
@CreateDateColumn()
readonly createdAt: Date;
@UpdateDateColumn()
readonly updatedAt: Date;
@DeleteDateColumn()
readonly deletedAt: Date | null;
}그리고 아래와 같은 코드를 작성하여 synchronize의 힘을 빌려 테이블을 만들고, User 테이블에 데이터를 입력한 후 출력해보자.
const main = async () => {
const dataSource = new DataSource({
type: 'mysql',
host: 'localhost',
port: 3306,
userame: 'root',
password: 'root_password',
database: 'test',
synchronize: true,
autoLoadEntities: true,
entities: [User],
});
await dataSource.initialize();
const userRepository = dataSource.getRepository(User);
const user = userRepository.create({ nickname: 'choewy' });
await userRepository.save(user);
console.log(user.id);
};
main();그 결과 user의 id는 1이 아닌 '1'로 출력되는 것을 알 수 있다. 이와 같은 이유는 Javascript의 Number 타입으로는 bigint의 모든 값을 제대로 나타낼 수 없기 때문이다. 실제로 아래와 같이 bigint unsigned 범위의 최대값을 출력하는 코드를 실행해보면 다음과 같은 결과를 확인할 수 있다.
>> console.log(18446744073709551615);
>> 18446744073709552000MDN 문서에 따르면 Number로 표현 가능한 값의 범위는 다음과 같고 기재되어 있다.
const biggestInt = Number.MAX_SAFE_INTEGER; // (2**53 - 1) => 9007199254740991
const smallestInt = Number.MIN_SAFE_INTEGER; // -(2**53 - 1) => -90071992547409912.2. Javascript의 BigInt
MDN 문서에 따르면 primitive로 표현하기에 너무 큰 값은 BigInt라는 객체를 사용하도록 안내되어 있다. BigInt 객체의 Prototype은 Object이며, 값을 출력하면 뒤에 n이 붙는다.
typeof 1n === 'bigint'; // true;
typeof BigInt('1') === 'bigint'; // true;
typeof Object('1n') === 'object'; // true;
1n === 1; // false
1n == 1; // trueMDN 문서에 자세히 기재되어 있으나, 이 중에서 주로 실수할 것 같은 BigInt를 다룰 때 주의할 점으로는 다음과 같다.
- BigInt를 Number로 강제 변환할 수 있으나, 이 경우 정밀도가 손실될 수 있다.
- BigInt는 일부 연산자(관계, 항등, 논리)를 제외한 산술 연산자, 비트 연산자, 단항 부정 연산자, 증감연산자를 사용할 때에는 피연산자도 BigInt일 때에만 사용할 수 있다.
마치며
PK로 bigint를 사용하는 경우에는 JavaScript 환경에서의 정밀도 문제로 인해 값이 string으로 반환될 수 있으므로 이를 고려해야 한다.
실무에서는 다음과 같은 방식 중 하나를 선택하는 경우가 많다.
- PK를 string으로 다루기
- transformer를 사용해 BigInt로 변환하기
- 또는 범위가 충분히 작다면 int를 사용하는 방향으로 설계하기
특히 bigint를 일반 값으로 사용하는 경우에는 JavaScript의 Number 타입이 표현할 수 있는 안전한 정수 범위(2^53 - 1)를 초과할 수 있으므로, MDN 문서를 참고하여 BigInt 사용 시 주의사항을 충분히 이해하고 사용하는 것이 중요하다.