MikroORM Entity 정의
ORM(Object-Relational Mapping)은 데이터베이스 테이블을 코드의 클래스로 표현한다. MikroORM에서 이 클래스를 Entity라 부른다. Entity 하나가 테이블 하나에 대응하고, Entity의 속성이 컬럼에 대응한다.
@Entity 데코레이터
클래스에 @Entity() 데코레이터를 붙이면 MikroORM이 이 클래스를 데이터베이스 테이블로 인식한다.
@Entity()
export class Store {
// ...
}
기본적으로 클래스 이름이 테이블 이름이 된다. Store 클래스는 store 테이블에 매핑된다. 테이블 이름을 직접 지정하려면 tableName 옵션을 쓴다.
@Entity({ tableName: 'sales' })
export class Sale {
// ...
}
Sale 클래스를 sales 테이블에 매핑한다. 클래스 이름과 테이블 이름이 다를 때 사용한다.
@PrimaryKey
모든 Entity에는 기본 키가 필요하다. @PrimaryKey() 데코레이터로 지정한다.
@PrimaryKey({ type: 'uuid' })
storeId: string = randomUUID();
type: 'uuid'는 PostgreSQL의 UUID 타입에 매핑된다. randomUUID()로 기본값을 생성하므로, Entity를 만들 때 ID를 직접 지정하지 않아도 자동으로 고유한 값이 부여된다. 자동 증가 정수(auto-increment)와 달리 UUID는 분산 환경에서도 충돌 없이 ID를 생성할 수 있다.
@Property
일반 컬럼을 정의한다. 타입, 길이, 기본값, nullable 여부 등을 지정할 수 있다.
@Property({ length: 255 })
name!: string;
@Property({ type: 'text' })
address!: string;
@Property({ length: 50, nullable: true })
phone?: string;
| 옵션 | 설명 |
|---|---|
type | DB 컬럼 타입 ('text', 'int', 'decimal', 'uuid', 'json', 'timestamptz' 등) |
length | 문자열 최대 길이 (varchar) |
nullable | null 허용 여부 (기본값: false) |
default | 컬럼 기본값 |
defaultRaw | SQL 표현식으로 기본값 지정 ('CURRENT_TIMESTAMP' 등) |
unique | 고유 제약 조건 |
fieldName | DB 컬럼 이름 (TypeScript 속성 이름과 다를 때) |
TypeScript에서 !는 "이 속성은 반드시 값이 있다"는 선언이다. ?는 optional이다. nullable: true와 ?는 함께 쓰인다.
fieldName
TypeScript는 camelCase, SQL은 snake_case를 관례로 쓴다. fieldName으로 이 차이를 매핑한다.
@Property({ type: 'timestamptz', fieldName: 'created_at', defaultRaw: 'CURRENT_TIMESTAMP' })
createdAt: Date = new Date();
코드에서는 createdAt으로 접근하지만, 데이터베이스에서는 created_at 컬럼에 저장된다.
타임스탬프 자동 관리
onUpdate 옵션을 사용하면 Entity가 수정될 때 자동으로 값이 갱신된다.
@Property({
type: 'timestamptz',
fieldName: 'updated_at',
defaultRaw: 'CURRENT_TIMESTAMP',
onUpdate: () => new Date(),
})
updatedAt: Date = new Date();
createdAt은 defaultRaw: 'CURRENT_TIMESTAMP'로 생성 시점만 기록하고, updatedAt은 onUpdate: () => new Date()를 추가해서 수정할 때마다 현재 시간으로 갱신한다.
금액과 소수점
금액처럼 정밀한 소수 처리가 필요한 컬럼은 decimal 타입을 쓴다.
@Property({ type: 'decimal', precision: 10, scale: 2, nullable: true })
unitPrice?: number;
precision: 10은 전체 자릿수, scale: 2는 소수점 이하 자릿수다. 최대 99999999.99까지 표현할 수 있다. float이나 double은 부동소수점 오류가 있어서 금액에는 decimal을 쓴다.
JSON 컬럼
구조가 유동적인 데이터는 JSON 타입으로 저장한다.
@Property({ type: 'json', default: '{}' })
config: Record<string, unknown> = {};
PostgreSQL의 json 또는 jsonb 타입에 매핑된다. 디바이스별 설정처럼 스키마가 고정되지 않은 데이터를 저장할 때 유용하다.
@Enum
TypeScript enum을 데이터베이스 컬럼에 매핑한다.
export enum SessionStatus {
CREATED = 'created',
PAYMENT_PENDING = 'payment_pending',
SHOOTING = 'shooting',
PROCESSING = 'processing',
COMPLETED = 'completed',
CANCELLED = 'cancelled',
EXPIRED = 'expired',
}
@Enum(() => SessionStatus)
@Property({ default: SessionStatus.CREATED })
status: SessionStatus = SessionStatus.CREATED;
@Enum(() => SessionStatus)은 MikroORM에게 이 속성이 enum이라고 알려준다. 데이터베이스에는 문자열('created', 'shooting' 등)로 저장되지만, 코드에서는 SessionStatus.CREATED처럼 타입 안전하게 사용할 수 있다.
옵션 형태로 기본값을 함께 지정할 수도 있다.
@Enum({ items: () => PaymentGateway, default: PaymentGateway.KIS })
paymentGateway: PaymentGateway = PaymentGateway.KIS;
관계 매핑
Entity 간의 관계는 데코레이터로 표현한다. 데이터베이스의 외래 키(Foreign Key)가 코드에서는 객체 참조가 된다.
@ManyToOne
여러 Entity가 하나의 Entity를 참조하는 관계다. 외래 키를 가진 쪽에 붙인다.
@ManyToOne({ entity: () => Store, fieldName: 'store_id' })
store!: Store;
@Property({ type: 'uuid', fieldName: 'store_id', persist: false })
storeId!: string;
Device는 하나의 Store에 속한다. 데이터베이스에는 store_id 외래 키가 저장된다.
persist: false인 storeId 속성은 편의를 위한 것이다. 관계를 로드하지 않아도 외래 키 값에 접근할 수 있다. persist: false이므로 이 속성의 변경은 DB에 반영되지 않는다. 실제 저장은 store 속성을 통해서만 이루어진다.
entity: () => Store처럼 화살표 함수로 감싸는 이유는 순환 참조 문제를 방지하기 위해서다. 파일이 로드되는 시점에 참조 대상 클래스가 아직 정의되지 않았을 수 있는데, 화살표 함수로 감싸면 실제로 필요한 시점에 평가된다.
@OneToMany
하나의 Entity가 여러 Entity에 참조되는 관계다. @ManyToOne의 반대편에 붙인다.
@OneToMany(() => Device, (device) => device.store)
devices = new Collection<Device>(this);
Store는 여러 Device를 가질 수 있다. Collection은 MikroORM이 제공하는 컬렉션 타입으로, 관계를 로드하기 전에는 빈 상태이다가 populate 옵션으로 로드하면 데이터가 채워진다.
orphanRemoval: true 옵션을 추가하면, 부모 Entity에서 컬렉션 항목을 제거할 때 자식 Entity도 DB에서 삭제된다.
@OneToMany(() => Shot, (shot) => shot.session, { orphanRemoval: true })
shots = new Collection<Shot>(this);
@OneToOne
1:1 관계다. owner: true를 가진 쪽이 외래 키를 소유한다.
// Payment가 외래 키를 소유 (owner: true)
@OneToOne({ entity: () => Session, fieldName: 'session_id', owner: true })
session!: Session;
// Session에서 역방향 참조 (owner: false)
@OneToOne('Payment', (payment: Payment) => payment.session, {
nullable: true,
owner: false,
})
payment?: Payment;
Payment 테이블에 session_id 컬럼이 존재하고, Session 테이블에는 payment_id 컬럼이 없다. owner 설정이 외래 키의 물리적 위치를 결정한다.
삭제 규칙
관계에 deleteRule을 지정하면 부모 Entity 삭제 시 자식 Entity의 처리 방식을 정할 수 있다.
@ManyToOne({
entity: () => Session,
fieldName: 'session_id',
deleteRule: 'cascade',
})
session!: Session;
deleteRule: 'cascade'는 Session이 삭제되면 해당 Shot도 함께 삭제된다는 의미다.
인덱스
조회 성능을 위해 자주 검색하는 컬럼에 인덱스를 추가한다.
@ManyToOne({ entity: () => Session, fieldName: 'session_id' })
@Index()
session!: Session;
@Property({ default: false })
@Index()
selected: boolean = false;
@Index()는 해당 컬럼에 단일 인덱스를 생성한다. session_id로 Shot을 자주 조회하므로 인덱스를 추가한 것이다.
복합 유니크 제약 조건은 @Unique() 데코레이터로 클래스 레벨에서 지정한다.
@Entity()
@Unique({ properties: ['session', 'sequence'] })
export class Shot {
// ...
}
같은 Session 내에서 Shot의 sequence가 중복되지 않도록 보장한다.
생성자
Entity의 생성자는 필수 관계와 속성을 받는다.
export class Device {
constructor(serialNumber: string, store: Store, name: string) {
this.serialNumber = serialNumber;
this.store = store;
this.storeId = store.storeId;
this.name = name;
}
}
new Device('SN-001', store, '1번 키오스크')처럼 호출하면, 필수 속성이 모두 설정된 Entity 인스턴스가 만들어진다. 선택 속성(location, description 등)은 생성 후 별도로 설정한다. 이렇게 하면 필수 데이터 없이 Entity를 만드는 실수를 컴파일 시점에 방지할 수 있다.
왜 MikroORM Entity인가
TypeORM도 데코레이터 기반 Entity를 지원하지만 몇 가지 차이가 있다. TypeORM은 Active Record와 Data Mapper 패턴을 모두 지원하는 대신 내부 일관성이 떨어지고, Unit of Work 구현이 불완전하다. MikroORM은 Data Mapper + Unit of Work를 일관되게 구현하며, Identity Map으로 같은 Entity의 중복 인스턴스를 방지한다. persist: false로 가상 속성을 선언하거나, @Enum(() => ...) 같은 타입 안전 문법도 MikroORM이 더 직관적이다. Prisma는 데코레이터 대신 스키마 파일(.prisma)을 쓰는데, 코드와 스키마가 분리되는 장점이 있지만 Entity 클래스에 비즈니스 로직을 넣기 어렵다.
정리
- @Entity, @Property, @Enum, @ManyToOne 등 데코레이터로 클래스-테이블 매핑을 선언하고, fieldName으로 camelCase-snake_case 차이를 해소한다
- 금액은 decimal, 유동 구조는 json, 상태값은 Enum으로 타입을 정확히 지정하고, nullable/unique/default로 제약을 코드에 표현한다
- persist: false 가상 속성으로 FK 직접 접근, deleteRule로 삭제 전파, 생성자로 필수 속성을 강제해서 런타임 오류를 줄인다