TypeORM Enum·JSON 컬럼이란?
TypeORM에서 엔티티 컬럼의 타입을 varchar나 int만 사용하는 경우가 많다. 하지만 실무에서는 상태값(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 등 커스텀 변환까지 지원하므로, 요구사항에 맞는 조합을 선택하라.