NestJS + TypeORM 컬럼 타입

왜 컬럼 타입 설계가 중요한가

NestJS + TypeORM 프로젝트에서 @Column()은 가장 많이 쓰는 데코레이터입니다. 그러나 기본 string, number만 쓰다 보면 운영에서 다양한 문제를 만납니다: 암호화된 값을 매번 수동 변환, Enum 값이 DB에 숫자로 저장되어 디버깅 불가, JSON 컬럼의 Change Detection 실패 등.

TypeORM은 이를 해결하기 위해 ValueTransformer, Enum 컬럼, JSON/JSONB 컬럼을 제공합니다. 이 글은 공식 문서(Column Types, Column Types for MySQL)를 근거로 각각의 동작 원리, NestJS 실무 패턴, 그리고 흔한 함정을 정리합니다.

ValueTransformer: DB ↔ 애플리케이션 값 자동 변환

TypeORM의 ValueTransformer 인터페이스는 두 메서드로 구성됩니다:

  • to(value): 애플리케이션 → DB 저장 시 변환
  • from(value): DB → 애플리케이션 로딩 시 변환

패턴 1: 암호화/복호화 Transformer

import { ValueTransformer } from 'typeorm';
import * as crypto from 'crypto';

const ALGORITHM = 'aes-256-cbc';
const KEY = Buffer.from(process.env.ENCRYPTION_KEY, 'hex'); // 32 bytes
const IV_LENGTH = 16;

export class EncryptionTransformer implements ValueTransformer {
  to(value: string | null): string | null {
    if (!value) return value;
    const iv = crypto.randomBytes(IV_LENGTH);
    const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);
    const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
    return iv.toString('hex') + ':' + encrypted.toString('hex');
  }

  from(value: string | null): string | null {
    if (!value) return value;
    const [ivHex, encryptedHex] = value.split(':');
    const iv = Buffer.from(ivHex, 'hex');
    const decipher = crypto.createDecipheriv(ALGORITHM, KEY, iv);
    return decipher.update(Buffer.from(encryptedHex, 'hex'), undefined, 'utf8')
      + decipher.final('utf8');
  }
}

// Entity에서 사용
@Entity()
export class User {
  @Column({ transformer: new EncryptionTransformer() })
  socialSecurityNumber: string;
  // DB에는 암호화된 문자열 저장, 코드에서는 평문으로 접근
}

패턴 2: BigInt(문자열 ↔ 숫자) Transformer

MySQL의 BIGINT는 JavaScript의 number 범위를 초과할 수 있습니다. TypeORM은 BIGINT를 문자열로 반환하는데, 이를 BigInt 또는 number로 자동 변환합니다.

export class BigIntTransformer implements ValueTransformer {
  to(value: number): number {
    return value;
  }

  from(value: string): number {
    return parseInt(value, 10);
  }
}

@Entity()
export class Transaction {
  @Column({ type: 'bigint', transformer: new BigIntTransformer() })
  amount: number; // DB에서 문자열로 오지만 자동으로 number 변환
}

패턴 3: Boolean Transformer (MySQL tinyint)

MySQL은 BOOLEANTINYINT(1)로 저장합니다. TypeORM은 대부분 자동 변환하지만, Raw Query나 특정 드라이버에서 0/1이 그대로 올 수 있습니다.

export class BooleanTransformer implements ValueTransformer {
  to(value: boolean | null): number | null {
    return value === null ? null : value ? 1 : 0;
  }

  from(value: number | null): boolean | null {
    return value === null ? null : value === 1;
  }
}

복수 Transformer 적용

TypeORM 공식 문서에 명시된 대로, transformer 옵션에 배열을 전달하면 순차 적용됩니다.

@Column({
  transformer: [new TrimTransformer(), new LowerCaseTransformer()],
})
email: string;
// to: trim → lowercase → DB 저장
// from: lowercase → trim → 애플리케이션 (역순)

Enum 컬럼: 문자열 vs 숫자 저장

TypeORM은 TypeScript enum을 DB 컬럼으로 매핑할 때 두 가지 방식을 지원합니다.

문자열 Enum (권장)

export enum OrderStatus {
  PENDING = 'pending',
  CONFIRMED = 'confirmed',
  SHIPPED = 'shipped',
  DELIVERED = 'delivered',
  CANCELLED = 'cancelled',
}

@Entity()
export class Order {
  @Column({ type: 'enum', enum: OrderStatus, default: OrderStatus.PENDING })
  status: OrderStatus;
  // MySQL: ENUM('pending','confirmed','shipped','delivered','cancelled')
}

장점: DB에서 직접 조회할 때 값이 읽힙니다. 디버깅이 쉽습니다.

숫자 Enum (주의 필요)

export enum Priority {
  LOW = 0,
  MEDIUM = 1,
  HIGH = 2,
  CRITICAL = 3,
}

@Entity()
export class Task {
  @Column({ type: 'enum', enum: Priority, default: Priority.LOW })
  priority: Priority;
  // MySQL: ENUM('0','1','2','3') — 문자열 '0', '1'로 저장됨
}

주의: MySQL의 ENUM은 항상 문자열입니다. 숫자 enum의 값 0, 1문자열 '0', '1'로 저장됩니다. 이로 인해 WHERE priority = 0 같은 쿼리에서 타입 불일치가 발생할 수 있습니다.

PostgreSQL의 경우: enum 대신 varchar 고려

PostgreSQL은 네이티브 ENUM 타입을 지원하지만, 값을 추가하는 것은 간단하지만 제거/수정이 어렵습니다. Migration에서 ALTER TYPE ... ADD VALUE는 트랜잭션 안에서 실행할 수 없는(PostgreSQL 12 미만) 제약이 있습니다. 이런 이유로 많은 팀에서 varchar + 애플리케이션 레벨 검증을 선택합니다.

// PostgreSQL에서 안전한 대안
@Column({ type: 'varchar', length: 20 })
status: OrderStatus;
// DB에는 varchar로 저장, 타입 체크는 TypeScript enum으로

JSON/JSONB 컬럼: 유연함의 대가

TypeORM에서 JSON 컬럼은 simple-json 또는 네이티브 json/jsonb(PostgreSQL) 타입으로 사용합니다.

simple-json vs json/jsonb

항목 simple-json json (MySQL) jsonb (PostgreSQL)
DB 저장 형태 TEXT (JSON.stringify) 네이티브 JSON 네이티브 JSONB (바이너리)
인덱싱 ❌ 불가 제한적 (Generated Column) ✅ GIN 인덱스 가능
쿼리 내 검색 LIKE만 가능 JSON_EXTRACT 가능 @>, ->> 연산자 가능
적합 용도 소규모 설정값 구조화된 데이터 검색 필요한 비정형 데이터

사용 예시

// simple-json: TEXT 컬럼에 JSON.stringify로 저장
@Column({ type: 'simple-json', nullable: true })
preferences: { theme: string; language: string } | null;

// 네이티브 json (MySQL)
@Column({ type: 'json', nullable: true })
metadata: Record<string, any>;

// jsonb (PostgreSQL)
@Column({ type: 'jsonb', default: {} })
settings: Record<string, any>;

자주 빠지는 함정 4가지

함정 1: JSON 컬럼의 Change Detection 실패

TypeORM의 save()는 값 비교로 변경 여부를 판단합니다. JSON 객체는 참조(reference)가 같으면 변경을 감지하지 못합니다.

// ❌ Change Detection 실패 — UPDATE 실행 안 됨
const user = await userRepo.findOneBy({ id: 1 });
user.preferences.theme = 'dark'; // 같은 객체의 속성만 변경
await userRepo.save(user);        // TypeORM이 변경을 감지하지 못함

// ✅ 새 객체로 재할당
const user = await userRepo.findOneBy({ id: 1 });
user.preferences = { ...user.preferences, theme: 'dark' }; // 새 객체
await userRepo.save(user); // UPDATE 실행됨

이는 TypeORM GitHub 이슈에서 반복적으로 보고되는 알려진 동작입니다. JSON 컬럼 수정 시 반드시 스프레드 연산자로 새 객체를 생성해야 합니다.

함정 2: ValueTransformer의 to()에 null/undefined가 올 수 있음

// ❌ null 체크 누락
export class EncryptionTransformer implements ValueTransformer {
  to(value: string): string {
    // value가 null이면 crypto 에러 발생!
    const cipher = crypto.createCipheriv(...);
    return cipher.update(value, 'utf8', 'hex');
  }
}

// ✅ null/undefined 방어
export class EncryptionTransformer implements ValueTransformer {
  to(value: string | null | undefined): string | null {
    if (value == null) return null; // null/undefined 통과
    // ... 암호화 로직
  }
}

nullable 컬럼이나 optional 필드에서 to()/from()null이 전달됩니다. 모든 Transformer에 null 체크는 필수입니다.

함정 3: MySQL ENUM 변경 시 Migration 위험

MySQL에서 ENUM 값을 추가/제거하면 TypeORM Migration이 ALTER TABLE ... CHANGE COLUMN을 생성합니다. 이는 대형 테이블에서 테이블 잠금을 유발할 수 있습니다.

// Migration이 생성하는 SQL
ALTER TABLE `order` CHANGE `status` `status` 
  ENUM('pending','confirmed','shipped','delivered','cancelled','refunded') 
  NOT NULL DEFAULT 'pending';
// MySQL 5.7: 테이블 복사 발생 → 대형 테이블에서 수 분~수 시간 잠금
// MySQL 8.0+: INSTANT ADD는 ENUM 끝에 추가하는 경우만 지원

대책: ENUM 값 추가는 항상 기존 값 목록의 끝에 추가하세요. MySQL 8.0+에서는 끝에 추가하는 경우 INSTANT ALTER가 가능합니다. 중간 삽입이나 제거가 필요하면 pt-online-schema-change 같은 도구를 사용하세요.

함정 4: Transformer가 QueryBuilder의 where에서 적용되지 않음

ValueTransformer는 Entity의 save()/find()에서만 자동 적용됩니다. QueryBuilder의 where()에서 직접 값을 넘기면 Transformer의 to()가 호출되지 않습니다.

// 암호화 Transformer가 적용된 컬럼
@Column({ transformer: new EncryptionTransformer() })
socialSecurityNumber: string;

// ❌ Transformer 미적용 — 평문으로 검색됨
const user = await userRepo
  .createQueryBuilder('user')
  .where('user.socialSecurityNumber = :ssn', { ssn: '123-45-6789' })
  .getOne();
// DB에는 암호화된 값이 저장되어 있으므로 일치하는 행 없음

// ✅ find() 사용 — Transformer 자동 적용
const user = await userRepo.findOneBy({ socialSecurityNumber: '123-45-6789' });
// find()는 내부적으로 to()를 호출해 암호화된 값으로 검색

// ✅ 또는 QueryBuilder에서 수동 변환
const encrypted = new EncryptionTransformer().to('123-45-6789');
const user = await userRepo
  .createQueryBuilder('user')
  .where('user.socialSecurityNumber = :ssn', { ssn: encrypted })
  .getOne();

NestJS 실무: 용도별 추천 컬럼 설계

데이터 유형 추천 컬럼 타입 이유
상태값 (status, role) 문자열 Enum 또는 varchar 디버깅 용이, DB 직접 조회 가능
민감 정보 (SSN, 카드번호) varchar + EncryptionTransformer 저장 시 암호화 자동 적용
사용자 설정/옵션 simple-json (소량) 또는 jsonb 유연한 스키마, 검색 필요 시 jsonb
금액 (BigInt) bigint + BigIntTransformer JS number 범위 초과 방지
태그/라벨 목록 simple-array 또는 json 간단한 배열 저장
날짜/시간 timestamp/datetime (기본) Transformer 불필요 (TypeORM 자동 처리)

운영 체크리스트 5항목

  1. 모든 Transformer에 null/undefined 방어 코드 — nullable 컬럼과 optional 필드에서 null이 to()/from()에 전달됩니다.
  2. JSON 컬럼 수정 시 새 객체 생성 — 스프레드 연산자로 복사해야 Change Detection이 동작합니다.
  3. Enum은 문자열 기반 사용 — 숫자 enum은 MySQL ENUM의 문자열 저장과 타입 불일치를 유발합니다.
  4. Enum 값 추가는 목록 끝에만 — 중간 삽입/제거 시 ALTER TABLE이 테이블 잠금을 유발할 수 있습니다.
  5. QueryBuilder에서 Transformer 미적용 주의 — 암호화 등 Transformer 의존 컬럼은 find()를 쓰거나 수동 변환을 추가하세요.

정리

TypeORM의 컬럼 타입은 “DB와 코드 사이의 계약”입니다. ValueTransformer로 암호화·타입 변환을 자동화하고, Enum으로 상태값을 타입 안전하게 관리하며, JSON 컬럼으로 유연한 스키마를 구현할 수 있습니다.

핵심은 Transformer의 null 방어, JSON의 Change Detection 한계, Enum의 Migration 위험, 그리고 QueryBuilder에서 Transformer가 무시되는 동작을 미리 알고 대비하는 것입니다.

참고 자료: TypeORM 공식 — Column Types | TypeORM 공식 — Entity Columns

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux