데이터 모델링
ASAPJS 데이터 모델링은 두 가지 상호 보완적인 추상화를 기반으로 합니다. 클래스를 Sequelize 모델로 변환하는 @Table 데코레이터와, 컬럼 메타데이터, Swagger 스키마, 런타임 타입 변환 규칙을 하나의 데코레이터 호출로 정의하는 TypeIs 타입 시스템이 그것입니다.
@Table 데코레이터
@asapjs/sequelize에서 임포트합니다:
import { Table } from '@asapjs/sequelize';시그니처
function Table(options: ModelOptions): ClassDecoratorModelOptions는 Sequelize 고유의 타입입니다. 이 데코레이터는 Sequelize 모델 옵션 객체 전체를 받아 여러 가지 자동 동작을 추가로 적용합니다.
옵션
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
tableName | string | 클래스 이름 | 실제 데이터베이스 테이블 이름입니다. |
timestamps | boolean | true | true이거나 생략하면 created_at과 updated_at 컬럼이 자동으로 추가됩니다. 비활성화하려면 false로 설정하세요. |
charset | string | 'utf8mb4' | 항상 utf8mb4로 강제됩니다 — 변경할 수 없습니다. |
collate | string | 'utf8mb4_general_ci' | 항상 utf8mb4_general_ci로 강제됩니다 — 변경할 수 없습니다. |
그 외 모든 ModelOptions 필드(예: paranoid, underscored, indexes)는 Sequelize에 그대로 전달됩니다.
자동 타임스탬프
timestamps가 false로 설정되지 않으면 데코레이터가 다음을 추가합니다:
| 컬럼 | 매핑 필드 | Sequelize 옵션 |
|---|---|---|
created_at | createdAt | timestamps: true |
updated_at | updatedAt | timestamps: true |
컬럼 이름은 Sequelize의 기본 camelCase 별칭이 아닌 snake_case(created_at, updated_at)를 사용합니다.
데코레이터 내부 동작
getTypesData를 통해 클래스의 모든TypeIs데코레이터가 적용된 필드를 읽습니다.- 각 필드를
addAttribute로 Sequelize 컬럼 속성으로 등록합니다(FOREIGNKEY/BELONGSTO타입의 경우addForeignKey/addAssociation사용). timestamps !== false이면created_at/updated_at타임스탬프 옵션을 추가합니다.- DBML 생성 및 콘솔 출력을 위한 테이블 메타데이터를 등록합니다.
sequelize-typescript의setModelName과addOptions를 호출하여 모델 등록을 완료합니다.
사용법
import { Model } from 'sequelize-typescript';
import { Table, TypeIs } from '@asapjs/sequelize';
@Table({
tableName: 'users',
timestamps: true,
})
export default class UsersTable extends Model {
@TypeIs.INT({ primaryKey: true, autoIncrement: true, comment: 'User ID' })
id: number;
@TypeIs.STRING({ unique: true, comment: 'Email (unique)' })
email: string;
@TypeIs.PASSWORD({ comment: 'Password (bcrypt)' })
password: string;
@TypeIs.STRING({ comment: 'Display name' })
name: string;
@TypeIs.DATETIME({ comment: 'Created at' })
created_at: Date;
@TypeIs.DATETIME({ comment: 'Updated at' })
updated_at: Date;
}타임스탬프를 비활성화하려면:
@Table({ tableName: 'audit_logs', timestamps: false })
export default class AuditLogsTable extends Model {
@TypeIs.STRING({ comment: 'Log message' })
message: string;
}TypeIs 타입 시스템
TypeIs는 하나의 데코레이터 호출로 세 가지를 인코딩하는 통합 타입 시스템입니다:
- Sequelize 컬럼 정의 (
toSequelize()) - OpenAPI/Swagger 스키마 (
toSwagger()) - 런타임 값 변환 함수 (
fixValue())
@asapjs/sequelize에서 임포트합니다:
import { TypeIs } from '@asapjs/sequelize';이중 사용법: 데코레이터 vs. 값
모든 TypeIs.* 함수는 두 가지 방식으로 동작합니다.
프로퍼티 데코레이터로 (엔티티 또는 DTO 클래스에서):
@TypeIs.STRING({ comment: 'Email address' })
email: string;일반 값으로 (라우트 IOptions 또는 복합 타입의 인수로):
// 라우트 응답 옵션
response: TypeIs.BOOLEAN()
// 타입 조합
TypeIs.ARRAY(TypeIs.INT())
TypeIs.PAGING(UserInfoDto)TypeIs 전체 레퍼런스
숫자 타입
TypeIs.INT
TypeIs.INT(options?: {
decimals?: number;
precision?: number;
scale?: number;
comment?: string;
// + Sequelize ModelAttributeColumnOptions (primaryKey, autoIncrement, unique, allowNull, defaultValue, …)
})| 항목 | 값 |
|---|---|
| Sequelize 타입 | DataTypes.INTEGER |
| Swagger 타입 | integer / 포맷 int32 |
fixValue | parseInt(String(o), 10) — 32비트 정수로 변환; null/undefined는 그대로 반환 |
| 엔티티 | 예 |
| DTO | 예 |
| 라우트 옵션 | 예 |
@TypeIs.INT({ primaryKey: true, autoIncrement: true, comment: 'User ID' })
id: number;TypeIs.BIGINT
TypeIs.BIGINT(options?: {
decimals?: number;
precision?: number;
scale?: number;
comment?: string;
})| 항목 | 값 |
|---|---|
| Sequelize 타입 | DataTypes.BIGINT |
| Swagger 타입 | integer / 포맷 int32 |
fixValue | parseInt(String(o), 10) |
| 엔티티 | 예 |
| DTO | 예 |
| 라우트 옵션 | 예 |
TypeIs.LONG
TypeIs.LONG(options?: {
decimals?: number;
precision?: number;
scale?: number;
comment?: string;
})| 항목 | 값 |
|---|---|
| Sequelize 타입 | DataTypes.INTEGER |
| Swagger 타입 | integer / 포맷 int64 |
fixValue | parseInt(String(o), 10) |
| 엔티티 | 예 |
| DTO | 예 |
| 라우트 옵션 | 예 |
Swagger 포맷이 int64여야 하는 경우(예: Unix 타임스탬프) INT 대신 LONG을 사용하세요.
TypeIs.FLOAT
TypeIs.FLOAT(options?: {
length?: number;
decimals?: number;
comment?: string;
})| 항목 | 값 |
|---|---|
| Sequelize 타입 | DataTypes.FLOAT(length, decimals) |
| Swagger 타입 | integer / 포맷 float |
fixValue | parseFloat(String(o)) |
| 엔티티 | 예 |
| DTO | 예 |
| 라우트 옵션 | 예 |
TypeIs.DECIMAL
TypeIs.DECIMAL(options: {
precision: number; // 필수
scale: number; // 필수
comment?: string;
})| 항목 | 값 |
|---|---|
| Sequelize 타입 | DataTypes.DECIMAL({ precision, scale }) |
| Swagger 타입 | integer / 포맷 float |
fixValue | parseFloat(String(o)) |
| 엔티티 | 예 |
| DTO | 예 |
| 라우트 옵션 | 예 |
DataTypes.DECIMAL은 컬럼 타입을 정확하게 설정하기 위해 precision과 scale이 반드시 필요합니다.
@TypeIs.DECIMAL({ precision: 10, scale: 2, comment: 'Price' })
price: number;TypeIs.DOUBLE
TypeIs.DOUBLE(options?: {
length?: number;
decimals?: number;
comment?: string;
})| 항목 | 값 |
|---|---|
| Sequelize 타입 | DataTypes.DOUBLE(length, decimals) |
| Swagger 타입 | number / 포맷 double |
fixValue | parseFloat(String(o)) |
| 엔티티 | 예 |
| DTO | 예 |
| 라우트 옵션 | 예 |
문자열 타입
TypeIs.STRING
TypeIs.STRING(options?: {
length?: number;
binary?: boolean;
comment?: string;
})| 항목 | 값 |
|---|---|
| Sequelize 타입 | DataTypes.STRING(length, binary) |
| Swagger 타입 | string |
fixValue | String(o) — 임의의 값을 문자열로 변환; null/undefined는 그대로 반환 |
| 엔티티 | 예 |
| DTO | 예 |
| 라우트 옵션 | 예 |
기본 STRING은 대부분의 데이터베이스에서 VARCHAR(255)로 매핑됩니다.
@TypeIs.STRING({ unique: true, comment: 'Email (unique)' })
email: string;TypeIs.TEXT
TypeIs.TEXT(options?: {
length?: TextLength; // 'tiny' | 'medium' | 'long' | undefined
comment?: string;
})| 항목 | 값 |
|---|---|
| Sequelize 타입 | DataTypes.TEXT({ length }) |
| Swagger 타입 | string |
fixValue | String(o) (STRING과 동일) |
| 엔티티 | 예 |
| DTO | 예 |
| 라우트 옵션 | 예 |
VARCHAR(255)로 부족한 긴 텍스트 콘텐츠(블로그 본문, 설명 등)에는 TEXT를 사용하세요.
@TypeIs.TEXT({ comment: 'Post content' })
content: string;TypeIs.PASSWORD
TypeIs.PASSWORD(options?: {
comment?: string;
})| 항목 | 값 |
|---|---|
| Sequelize 타입 | DataTypes.STRING(512) |
| Swagger 포맷 | string / 포맷 password |
fixValue | 없음 (변환 없음 — 패스워드 값을 그대로 반환) |
| 엔티티 | 예 |
| DTO | 예 |
| 라우트 옵션 | 예 |
PASSWORD는 데이터베이스에 항상 STRING(512)로 저장됩니다. Swagger에서는 format: 'password'로 렌더링되어 UI 클라이언트가 입력 필드를 마스킹합니다.
@TypeIs.PASSWORD({ comment: 'Hashed password (bcrypt)' })
password: string;특수 값 타입
TypeIs.ENUM
TypeIs.ENUM(options: {
values: string[]; // 필수 — 허용 값 목록
comment?: string;
})| 항목 | 값 |
|---|---|
| Sequelize 타입 | DataTypes.ENUM({ values }) |
| Swagger 타입 | string with enum 배열 |
fixValue | values에 존재하면 해당 값 반환, 없으면 undefined |
| 엔티티 | 예 |
| DTO | 예 |
| 라우트 옵션 | 예 |
enum UserRole { ADMIN = 'ADMIN', USER = 'USER' }
@TypeIs.ENUM({
values: Object.keys(UserRole),
comment: 'User role',
})
role: UserRole;TypeIs.JSON
TypeIs.JSON(options?: {
comment?: string;
})| 항목 | 값 |
|---|---|
| Sequelize 타입 | DataTypes.JSON |
| Swagger 타입 | object |
fixValue | 값이 문자열이면 JSON.parse 시도; 그렇지 않으면 그대로 반환. 파싱 실패 시 원본 문자열 반환. |
| 엔티티 | 예 |
| DTO | 예 |
| 라우트 옵션 | 예 |
@TypeIs.JSON({ comment: 'Arbitrary metadata' })
metadata: Record<string, unknown>;TypeIs.BASE64
TypeIs.BASE64(options?: {
length?: BlobSize; // 'tiny' | 'medium' | 'long' | undefined
comment?: string;
})| 항목 | 값 |
|---|---|
| Sequelize 타입 | DataTypes.BLOB(length) |
| Swagger 타입 | string / 포맷 byte |
fixValue | 없음 |
| 엔티티 | 예 |
| DTO | 예 |
| 라우트 옵션 | 예 |
JSON으로 전송되는 base64 인코딩된 바이너리 데이터에 사용합니다. Swagger 포맷 byte는 값이 base64임을 나타냅니다.
TypeIs.BINARY
TypeIs.BINARY(options?: {
length?: BlobSize;
comment?: string;
})| 항목 | 값 |
|---|---|
| Sequelize 타입 | DataTypes.BLOB(length) |
| Swagger 포맷 | string / 포맷 binary |
fixValue | 없음 |
| 엔티티 | 예 |
| DTO | 예 |
| 라우트 옵션 | 예 |
원시 바이너리 / 파일 업로드 필드에 사용합니다. Swagger 포맷 binary는 octet stream을 나타냅니다.
TypeIs.BOOLEAN
TypeIs.BOOLEAN(options?: {
comment?: string;
})| 항목 | 값 |
|---|---|
| Sequelize 타입 | DataTypes.BOOLEAN |
| Swagger 타입 | boolean |
fixValue | 'true' → true, 'false' → false, 그 외 → !!o 후 Boolean(v) |
| 엔티티 | 예 |
| DTO | 예 |
| 라우트 옵션 | 예 |
이 변환은 불리언이 문자열로 전달되는 쿼리 스트링 파라미터를 처리합니다.
@TypeIs.BOOLEAN({ comment: 'Whether the account is active' })
is_active: boolean;날짜 / 시간 타입
TypeIs.DATEONLY
TypeIs.DATEONLY(options?: {
comment?: string;
})| 항목 | 값 |
|---|---|
| Sequelize 타입 | DataTypes.DATEONLY() |
| Swagger 타입 | string / 포맷 date |
fixValue | 없음 |
| 엔티티 | 예 |
| DTO | 예 |
| 라우트 옵션 | 예 |
시간 없이 날짜만 저장합니다 (YYYY-MM-DD).
TypeIs.DATETIME
TypeIs.DATETIME(options?: {
comment?: string;
})| 항목 | 값 |
|---|---|
| Sequelize 타입 | DataTypes.DATE() |
| Swagger 타입 | string / 포맷 date-time |
fixValue | 없음 |
| 엔티티 | 예 |
| DTO | 예 |
| 라우트 옵션 | 예 |
전체 타임스탬프를 저장합니다. 명시적으로 선언하는 created_at과 updated_at 컬럼에 사용됩니다.
@TypeIs.DATETIME({ comment: 'Created at' })
created_at: Date;관계 타입
TypeIs.FOREIGNKEY
TypeIs.FOREIGNKEY(options: { table: () => typeof Model; comment?: string; [key: string]: any })| 항목 | 값 |
|---|---|
| Sequelize | sequelize-typescript의 addForeignKey 호출; extraData에 타입이 없으면 기본값으로 TypeIs.INT() 사용 |
| Swagger | Swagger 스키마에 포함되지 않음 |
fixValue | 없음 |
| 엔티티 | 예 |
| DTO | 아니오 |
| 라우트 옵션 | 아니오 |
객체 인자로 table과 추가 컬럼 옵션을 전달합니다. 모듈 평가 시 순환 임포트 문제를 피하기 위해 table은 반드시 썽크(() => Model) 형태여야 합니다:
// 예제 앱의 표준 사용법
@TypeIs.FOREIGNKEY({ table: () => UsersTable, comment: '작성자 ID' })
user_id: number;TypeIs.BELONGSTO
TypeIs.BELONGSTO(associatedClassGetter: () => typeof Model, optionsOrForeignKey?: string | AssociationOptions)| 항목 | 값 |
|---|---|
| Sequelize | BelongsToAssociation으로 addAssociation 호출; as는 기본적으로 프로퍼티 이름 사용 |
| Swagger | Swagger 스키마에 포함되지 않음 |
fixValue | 없음 |
| 엔티티 | 예 |
| DTO | 아니오 |
| 라우트 옵션 | 아니오 |
두 번째 인수는 외래 키 문자열이거나 전체 AssociationOptions 객체입니다. 옵션에 as가 없으면 프로퍼티 이름이 사용됩니다.
@TypeIs.FOREIGNKEY({ table: () => UsersTable, comment: 'Author ID' })
user_id: number;
@TypeIs.BELONGSTO(() => UsersTable, 'user_id')
user: UsersTable;복합 / DTO 전용 타입
이 타입들은 Sequelize 컬럼 매핑이 없습니다. DTO 클래스와 라우트 IOptions에서만 사용됩니다.
TypeIs.DTO
TypeIs.DTO(options: {
dto: typeof ExtendableDto;
as: string;
comment?: string;
})| 항목 | 값 |
|---|---|
| Sequelize | middleware() 호출 시 include 항목 생성 |
| Swagger | 중첩된 DTO의 스키마를 가리키는 $ref |
fixValue | 없음 — map()이 중첩 DTO의 자체 map()을 호출하여 DTO 필드를 처리 |
| 엔티티 | 아니오 |
| DTO | 예 |
| 라우트 옵션 | 예 |
as는 엔티티의 BELONGSTO 선언에서 사용한 Sequelize 연관 별칭과 일치해야 합니다.
@TypeIs.DTO({ dto: UserInfoDto, as: 'user', comment: 'Post author' })
user: UserInfoDto;TypeIs.QUERY
TypeIs.QUERY(options: {
query: (props: { user?: any; association?: string }) => string;
type: () => TypeIsData;
})| 항목 | 값 |
|---|---|
| Sequelize | Sequelize.literal(...) 속성 항목 생성 |
| Swagger | type TypeIs 함수 자체의 Swagger 출력에 위임 |
fixValue | type TypeIs 함수 자체의 fixValue에 위임 |
| 엔티티 | 아니오 |
| DTO | 예 |
| 라우트 옵션 | 아니오 |
원시 SQL 표현식이 필요한 계산 컬럼에 QUERY를 사용합니다. query 함수는 현재 association 경로와 JWT 페이로드의 user 객체를 받습니다.
@TypeIs.QUERY({
query: ({ association }) => `(SELECT COUNT(*) FROM posts WHERE posts.user_id = \`${association}\`.\`id\`)`,
type: () => TypeIs.INT(),
})
post_count: number;TypeIs.ARRAY
TypeIs.ARRAY(options: DtoOrTypeIs)DtoOrTypeIs는 typeof ExtendableDto | (() => TypeIsData)입니다.
| 항목 | 값 |
|---|---|
| Sequelize | 컬럼 매핑 없음 |
| Swagger | { type: 'array', items: ... }로 네임드 스키마 컴포넌트에 등록 |
fixValue | 값이 문자열이면 JSON.parse 시도; 그렇지 않으면 그대로 반환 |
| 엔티티 | 아니오 |
| DTO | 예 |
| 라우트 옵션 | 예 |
// DTO 클래스 사용
response: TypeIs.ARRAY(UserInfoDto)
// 기본 TypeIs 사용
response: TypeIs.ARRAY(TypeIs.INT())
TypeIs.ARRAY(TypeIs.STRING())TypeIs.PAGING
TypeIs.PAGING(options: DtoOrTypeIs)| 항목 | 값 |
|---|---|
| Sequelize | 컬럼 매핑 없음 |
| Swagger | data(배열), page, page_size, max_page, has_prev, has_next, total_elements 프로퍼티를 가진 object |
fixValue | 없음 |
| 엔티티 | 아니오 |
| DTO | 예 |
| 라우트 옵션 | 예 |
PAGING은 DTO 또는 TypeIs를 표준 페이지네이션 엔벨로프 스키마로 감쌉니다. Swagger 스키마에는 모든 페이지네이션 메타데이터 필드가 포함됩니다:
| 필드 | 타입 | 설명 |
|---|---|---|
data | array | 항목 타입의 배열 |
page | integer (int32) | 현재 페이지 번호 |
page_size | integer (int32) | 페이지당 항목 수 |
max_page | integer (int32) | 전체 페이지 수 |
has_prev | boolean | 이전 페이지 존재 여부 |
has_next | boolean | 다음 페이지 존재 여부 |
total_elements | integer (int32) | 전체 레코드 수 |
참고: 현재 Swagger 스키마(
paging.ts)에서는has_Next(대문자 N)로 정의되어 있으나,Repository의 실제 반환값은has_next(소문자 n)입니다. 향후 코드 수정으로 통일될 예정입니다.
response: TypeIs.PAGING(PostInfoDto)TypeIs 빠른 참조 테이블
| TypeIs | Sequelize 타입 | Swagger 타입 | Swagger 포맷 | fixValue 동작 | 엔티티 | DTO | 라우트 |
|---|---|---|---|---|---|---|---|
INT | INTEGER | integer | int32 | parseInt(str, 10) | 예 | 예 | 예 |
BIGINT | BIGINT | integer | int32 | parseInt(str, 10) | 예 | 예 | 예 |
LONG | INTEGER | integer | int64 | parseInt(str, 10) | 예 | 예 | 예 |
FLOAT | FLOAT | integer | float | parseFloat(str) | 예 | 예 | 예 |
DECIMAL | DECIMAL | integer | float | parseFloat(str) | 예 | 예 | 예 |
DOUBLE | DOUBLE | number | double | parseFloat(str) | 예 | 예 | 예 |
STRING | STRING | string | — | String(o) | 예 | 예 | 예 |
TEXT | TEXT | string | — | String(o) | 예 | 예 | 예 |
PASSWORD | STRING(512) | string | password | 없음 | 예 | 예 | 예 |
ENUM | ENUM | string (enum) | — | values에 있으면 반환, 없으면 undefined | 예 | 예 | 예 |
JSON | JSON | object | — | 문자열이면 JSON.parse; 오류 시 그대로 반환 | 예 | 예 | 예 |
BASE64 | BLOB | string | byte | 없음 | 예 | 예 | 예 |
BINARY | BLOB | string | binary | 없음 | 예 | 예 | 예 |
BOOLEAN | BOOLEAN | boolean | — | 'true'/'false' → bool, 그 외 Boolean(!!o) | 예 | 예 | 예 |
DATEONLY | DATEONLY | string | date | 없음 | 예 | 예 | 예 |
DATETIME | DATE | string | date-time | 없음 | 예 | 예 | 예 |
FOREIGNKEY | (FK 제약) | — | — | 없음 | 예 | 아니오 | 아니오 |
BELONGSTO | (연관) | — | — | 없음 | 예 | 아니오 | 아니오 |
DTO | (include) | $ref | — | 없음 (중첩 map() 사용) | 아니오 | 예 | 예 |
QUERY | literal(...) | (type에서) | — | type에 위임 | 아니오 | 예 | 아니오 |
ARRAY | — | array | — | 문자열이면 JSON.parse | 아니오 | 예 | 예 |
PAGING | — | object | — | 없음 | 아니오 | 예 | 예 |
전체 엔티티 예제
다음 두 엔티티는 참조 구현에서 직접 가져온 것입니다. FOREIGNKEY와 BELONGSTO를 사용한 사용자와 게시글 간의 일대다 관계를 보여줍니다.
// packages/example/src/user/domain/entity/UsersTable.ts
import { Model } from 'sequelize-typescript';
import { Table, TypeIs } from '@asapjs/sequelize';
@Table({
tableName: 'users',
timestamps: true,
})
export default class UsersTable extends Model {
@TypeIs.INT({ primaryKey: true, autoIncrement: true, comment: 'User ID' })
id: number;
@TypeIs.STRING({ unique: true, comment: 'Email (unique)' })
email: string;
@TypeIs.PASSWORD({ comment: 'Password (bcrypt)' })
password: string;
@TypeIs.STRING({ comment: 'Display name' })
name: string;
@TypeIs.DATETIME({ comment: 'Created at' })
created_at: Date;
@TypeIs.DATETIME({ comment: 'Updated at' })
updated_at: Date;
}// packages/example/src/post/domain/entity/PostsTable.ts
import { Model } from 'sequelize-typescript';
import { Table, TypeIs } from '@asapjs/sequelize';
import UsersTable from '../../../user/domain/entity/UsersTable';
@Table({
tableName: 'posts',
timestamps: true,
})
export default class PostsTable extends Model {
@TypeIs.INT({ primaryKey: true, autoIncrement: true, comment: 'Post ID' })
id: number;
@TypeIs.STRING({ comment: 'Post title' })
title: string;
@TypeIs.TEXT({ comment: 'Post content' })
content: string;
// 외래 키 컬럼 — 정수 컬럼 생성 및 FK 제약 등록
@TypeIs.FOREIGNKEY({ table: () => UsersTable, comment: 'Author user ID' })
user_id: number;
// 연관 — 컬럼 없음, BelongsTo 관계 등록
@TypeIs.BELONGSTO(() => UsersTable, 'user_id')
user: UsersTable;
@TypeIs.DATETIME({ comment: 'Created at' })
created_at: Date;
@TypeIs.DATETIME({ comment: 'Updated at' })
updated_at: Date;
}관련 항목
- DTO (데이터 전송 객체) — 엔티티 데이터를 응답/요청 형태로 변환하는 방법
- 라우팅 — 라우트
IOptions에서 DTO 및 TypeIs 값을 사용하는 방법