TypeORM Enum·JSON 컬럼 심화

TypeORM Enum·JSON 컬럼이란?

TypeORM에서 엔티티 컬럼의 타입을 varcharint만 사용하는 경우가 많다. 하지만 실무에서는 상태값(Enum)비정형 데이터(JSON)를 저장해야 하는 경우가 빈번하다. TypeORM은 이 두 가지를 네이티브로 지원하며, 올바르게 사용하면 타입 안전성과 유연성을 동시에 확보할 수 있다.

Enum 컬럼: 3가지 방식

1. TypeScript Enum + DB Enum

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

@Entity()
export class Order {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({
    type: 'enum',
    enum: OrderStatus,
    default: OrderStatus.PENDING,
  })
  status: OrderStatus;
}

PostgreSQL에서는 CREATE TYPE으로 DB 레벨 enum이 생성된다. 장점: DB가 유효하지 않은 값을 거부한다. 단점: enum 값 추가 시 마이그레이션이 필요하다.

2. TypeScript Enum + varchar 저장

@Column({
  type: 'varchar',
  length: 20,
  default: OrderStatus.PENDING,
})
status: OrderStatus;

DB에는 일반 varchar로 저장되지만, TypeScript에서는 enum 타입으로 사용한다. 장점: enum 값 추가 시 마이그레이션 불필요. 단점: DB 레벨 검증이 없다.

3. Numeric Enum

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

@Column({
  type: 'smallint',
  default: Priority.MEDIUM,
})
priority: Priority;

숫자 enum은 저장 공간이 작고 인덱싱이 빠르지만, DB를 직접 조회할 때 가독성이 떨어진다.

어떤 방식을 선택할까?

방식 DB 검증 마이그레이션 권장 사용처
DB enum ✅ 강력 값 추가마다 필요 거의 변하지 않는 상태값
varchar ❌ 없음 불필요 자주 변경되는 enum
smallint ❌ 없음 불필요 성능 최적화, 대량 데이터

JSON 컬럼: 비정형 데이터 저장

기본 JSON 컬럼

// 타입 정의
interface OrderMetadata {
  source: 'web' | 'mobile' | 'api';
  ipAddress?: string;
  userAgent?: string;
  couponCode?: string;
  notes?: string;
}

@Entity()
export class Order {
  @Column({
    type: 'jsonb',  // PostgreSQL: jsonb (인덱싱 가능)
    // type: 'json', // MySQL: json
    nullable: true,
  })
  metadata: OrderMetadata;
}

JSON vs JSONB (PostgreSQL)

항목 json jsonb
저장 텍스트 그대로 바이너리 파싱
인덱싱 불가 GIN 인덱스 가능
쿼리 성능 느림 (매번 파싱) 빠름
키 순서 보존 보존 미보존

결론: PostgreSQL이면 항상 jsonb를 사용하라.

JSON 컬럼 쿼리

// QueryBuilder로 JSON 필드 쿼리
const webOrders = await orderRepository
  .createQueryBuilder('order')
  .where("order.metadata->>'source' = :source", { source: 'web' })
  .getMany();

// 중첩 필드 접근
const withCoupon = await orderRepository
  .createQueryBuilder('order')
  .where("order.metadata->>'couponCode' IS NOT NULL")
  .getMany();

// JSON 배열 포함 검색 (PostgreSQL jsonb)
const tagged = await orderRepository
  .createQueryBuilder('order')
  .where("order.metadata->'tags' @> :tags", { tags: JSON.stringify(['vip']) })
  .getMany();

ValueTransformer: 커스텀 변환

DB 저장/조회 시 값을 자동 변환하는 패턴이다:

// 암호화 Transformer
export class EncryptTransformer implements ValueTransformer {
  to(value: string): string {
    if (!value) return value;
    return encrypt(value);  // 저장 시 암호화
  }

  from(value: string): string {
    if (!value) return value;
    return decrypt(value);  // 조회 시 복호화
  }
}

@Entity()
export class User {
  @Column({
    type: 'varchar',
    transformer: new EncryptTransformer(),
  })
  ssn: string;  // 코드에서는 평문, DB에서는 암호화
}
// BigInt Transformer (JS number 범위 초과 시)
export class BigIntTransformer implements ValueTransformer {
  to(value: bigint): string {
    return value?.toString();
  }

  from(value: string): bigint {
    return value ? BigInt(value) : null;
  }
}

@Column({
  type: 'bigint',
  transformer: new BigIntTransformer(),
})
totalAmount: bigint;

배열 컬럼 (PostgreSQL)

@Entity()
export class Product {
  // PostgreSQL 네이티브 배열
  @Column('text', { array: true, default: '{}' })
  tags: string[];

  @Column('int', { array: true, nullable: true })
  categoryIds: number[];
}

// 쿼리: 배열에 특정 값 포함
const products = await productRepository
  .createQueryBuilder('p')
  .where(':tag = ANY(p.tags)', { tag: 'sale' })
  .getMany();

JSON 배열 vs PostgreSQL 배열: 단순 목록이면 네이티브 배열이 인덱싱과 쿼리가 더 효율적이다. 구조가 있는 데이터면 JSON을 사용하라.

Enum + JSON 조합 실전 패턴

export enum NotificationType {
  EMAIL = 'email',
  SMS = 'sms',
  PUSH = 'push',
  WEBHOOK = 'webhook',
}

interface NotificationConfig {
  email?: { templateId: string; cc?: string[] };
  sms?: { provider: 'twilio' | 'aws-sns' };
  push?: { sound: boolean; badge: boolean };
  webhook?: { url: string; headers: Record<string, string> };
}

@Entity()
export class NotificationRule {
  @Column({
    type: 'enum',
    enum: NotificationType,
    array: true,  // PostgreSQL: enum 배열
  })
  channels: NotificationType[];

  @Column({ type: 'jsonb' })
  config: NotificationConfig;
}

// 사용
const rule = new NotificationRule();
rule.channels = [NotificationType.EMAIL, NotificationType.PUSH];
rule.config = {
  email: { templateId: 'welcome-v2', cc: ['admin@example.com'] },
  push: { sound: true, badge: true },
};

마이그레이션 주의사항

TypeORM Migration에서 enum 변경 시 주의할 점:

// PostgreSQL: enum에 새 값 추가
public async up(queryRunner: QueryRunner): Promise<void> {
  // ALTER TYPE으로 값 추가 (PostgreSQL 전용)
  await queryRunner.query(`
    ALTER TYPE order_status_enum ADD VALUE IF NOT EXISTS 'refunded'
  `);
}

// ⚠️ PostgreSQL에서 enum 값 제거는 불가능!
// 제거가 필요하면: 새 타입 생성 → 컬럼 변환 → 구 타입 삭제

정리

TypeORM의 Enum 컬럼은 상태값을 타입 안전하게 관리하고, JSON/JSONB 컬럼은 비정형 데이터를 유연하게 저장한다. DB enum은 강력한 검증을, varchar enum은 유연한 변경을 제공한다. JSONB는 GIN 인덱스와 결합하면 쿼리 성능도 확보된다. ValueTransformer로 암호화·BigInt 등 커스텀 변환까지 지원하므로, 요구사항에 맞는 조합을 선택하라.

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