TypeIs 타입 시스템
TypeIs는 ASAPJS의 핵심 혁신입니다. 필드를 한 번만 선언하면 프레임워크가 그 단일 선언으로부터 데이터베이스 컬럼 정의, Swagger 문서, 런타임 타입 강제 변환기를 모두 도출할 수 있게 해주는 메커니즘입니다.
핵심 아이디어
일반적인 Node.js 백엔드에서는 모든 필드에 대해 최소 세 가지 별도의 설명을 유지해야 합니다:
- Sequelize 컬럼 정의 (
DataTypes.STRING(255)) - Swagger 스키마 프로퍼티 (
{ type: 'string', description: '...' }) - 요청 처리 시 유효성 검사 또는 강제 변환 단계
이 세 가지 표현은 시간이 지남에 따라 서로 어긋나기 마련입니다. TypeIs는 세 가지 출력을 모두 단일 데코레이터 호출의 결과로 만들어 이 문제를 근본적으로 해결합니다.
@TypeIs.STRING({ comment: 'User email' })
email: string;내부적으로 이 하나의 선언은 세 가지를 생성합니다:
1. Sequelize 컬럼 정의 (toSequelize())
{
type: DataTypes.STRING(),
comment: 'User email',
}모델이 initSequelizeModule에 등록될 때, ASAPJS는 toSequelize() 출력을 읽어 Sequelize의 컬럼 정의 메커니즘에 전달합니다. 별도의 @Column 데코레이터가 필요하지 않습니다.
2. Swagger 스키마 프로퍼티 (toSwagger())
{
type: 'string',
description: 'User email',
}라우트 데코레이터가 body:, query:, response:를 통해 DTO 또는 엔티티 클래스를 참조할 때, ASAPJS는 TypeIs 데코레이터가 달린 모든 필드에 대해 toSwagger()를 호출하여 OpenAPI 스키마를 인라인으로 빌드합니다.
3. 런타임 타입 강제 변환기 (fixValue())
(o: any) => (o === undefined || o === null ? o : String(o))fixValue는 들어오는 값을 기대하는 기본 타입으로 변환합니다. STRING의 경우 String(o)를 호출하고, INT의 경우 parseInt를 호출합니다. ASAPJS가 요청 데이터를 DTO를 통해 매핑할 때 자동으로 실행됩니다.
아키텍처: @asapjs/schema와 TypeIsDecorators
TypeIs 시스템은 두 개의 패키지 레이어로 구성됩니다:
@asapjs/schema — 타입 정의 코어
@asapjs/schema 패키지는 타입 시스템의 핵심 엔진입니다. 플러그인 기반 아키텍처를 통해 하나의 타입 정의에서 여러 출력(Swagger, Sequelize 등)을 생성합니다.
BaseSchemaType<T>— 모든 타입의 추상 기본 클래스.validate(),parse(),toSwagger(),toSequelize()메서드를 제공합니다.SchemaPluginRegistry—swagger,sequelize등의 플러그인을 등록하고,BaseSchemaType.to(target)호출 시 해당 플러그인의transform()을 실행합니다.TypeIs— 등록된 모든 타입 팩토리(INT,STRING,BOOLEAN등)를 포함하는 네임스페이스 객체입니다.
@asapjs/sequelize — 데코레이터 브릿지
@asapjs/sequelize에서 export하는 TypeIs는 실제로 TypeIsDecorators라는 래퍼입니다. 이 래퍼는 @asapjs/schema의 TypeIs 타입 팩토리들을 순회하며, 각각을 TypeScript 프로퍼티 데코레이터로 사용할 수 있는 형태로 변환합니다.
// packages/sequelize/src/types/decorators.ts (간략화)
import { TypeIs, registry, sequelizePlugin } from '@asapjs/schema';
// @asapjs/schema의 모든 TypeIs 타입에 대해 데코레이터 wrapper 생성
function createTypeIsDecorators() {
const decorators: any = {};
for (const key in TypeIs) {
decorators[key] = createDecorator(key);
}
return decorators;
}
export const TypeIsDecorators = createTypeIsDecorators();// packages/sequelize/src/index.ts
export { TypeIsDecorators as TypeIs } from './types/decorators';사용자 관점에서는 import { TypeIs } from '@asapjs/sequelize'로 import하여 동일하게 사용합니다. 내부적으로 @asapjs/schema의 BaseSchemaType을 기존 typeGenerator 호환 형태(TypeIsData)로 변환하여, Sequelize 메타데이터 시스템과의 호환성을 유지합니다.
또한 @asapjs/sequelize는 Sequelize 전용 타입(QUERY, FOREIGNKEY, BELONGSTO)을 registerSequelizeTypes()를 통해 @asapjs/schema의 TypeIs에 동적으로 등록합니다.
TypeIs 내부 동작 방식
각 TypeIs.* 타입은 내부적으로 TypeIsData 레코드를 생성합니다:
// Simplified view of TypeIsData
type TypeIsData = {
__name: string;
toSwagger?: (...args: any[]) => any;
toSequelize?: (...args: any[]) => any;
fixValue?: (o: any) => any;
};데코레이터로 사용될 때, 이 레코드는 클래스 메타데이터 레지스트리에 저장됩니다. 레지스트리는 클래스 이름을 키로 사용하므로, ASAPJS는 런타임에 어떤 클래스든 검사하여 모든 TypeIs 필드를 열거할 수 있습니다. 이것이 단일 DTO 클래스가 Sequelize 모델 동기화와 Swagger 스펙 빌더 모두에 활용될 수 있는 원리입니다.
사용 컨텍스트
TypeIs 데코레이터는 세 가지 컨텍스트에서 작동하며, 각 컨텍스트는 세 가지 출력 중 서로 다른 부분을 사용합니다.
@Table 엔티티 — 데이터베이스 컬럼
@Table을 확장한 Model의 프로퍼티에 TypeIs.* 데코레이터를 붙이면, ASAPJS는 모델 등록 시 toSequelize()를 읽어 그 결과를 Sequelize에 컬럼 정의로 전달합니다.
// example/src/user/domain/entity/UsersTable.ts
@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 address (unique)' })
email: string;
@TypeIs.PASSWORD({ comment: 'bcrypt-hashed password' })
password: string;
@TypeIs.DATETIME({ comment: 'Record created at' })
created_at: Date;
}unique, primaryKey, autoIncrement 옵션은 표준 Sequelize ModelAttributeColumnOptions이며 그대로 전달됩니다.
ExtendableDto 서브클래스 — API 스키마 및 쿼리 설정
DTO는 ExtendableDto를 확장하며 동일한 TypeIs.* 데코레이터를 사용합니다. 이 컨텍스트에서 ASAPJS는 toSwagger()를 읽어 DTO의 OpenAPI 스키마를 빌드하고, fixValue()를 사용하여 들어오는 요청 필드 값을 강제 변환합니다.
// example/src/user/dto/UserInfoDto.ts
import { ExtendableDto, TypeIs } from '@asapjs/sequelize';
import UsersTable from '../domain/entity/UsersTable';
export default class UserInfoDto extends ExtendableDto {
@TypeIs.INT({ comment: 'User ID' })
id: number;
@TypeIs.STRING({ comment: 'Email address' })
email: string;
@TypeIs.STRING({ comment: 'Display name' })
name: string;
}defineTable 참조는 이 DTO가 어떤 엔티티의 프로젝션인지를 ASAPJS에 알려주며, 프레임워크가 시작 시 필드 이름의 유효성을 검사할 수 있게 합니다.
IOptions.response / IOptions.body — 인라인 Swagger 스키마
응답에 단순한 boolean이나 기본 타입으로 충분한 경우, DTO 클래스를 별도로 정의하지 않고도 TypeIs.* 표현식을 IOptions에 직접 전달할 수 있습니다. ASAPJS는 해당 표현식의 toSwagger()를 호출하여 인라인 스키마를 생성합니다.
@Get('/active', {
title: 'Is service active',
response: TypeIs.BOOLEAN(),
})
async isActive({}: ExecuteArgs) {
return { value: true };
}TypeIs.ARRAY나 TypeIs.PAGING 같은 조합 타입도 동일하게 작동합니다:
// 문자열 배열 인라인
response: TypeIs.ARRAY(TypeIs.STRING())
// DTO의 페이지네이션 목록
response: TypeIs.PAGING(PostInfoDto)사용 가능한 타입
| 카테고리 | 타입 |
|---|---|
| 숫자형 | INT, BIGINT, LONG, FLOAT, DECIMAL, DOUBLE |
| 문자열형 | STRING, TEXT, PASSWORD |
| 특수형 | ENUM, JSON, BOOLEAN, BASE64, BINARY |
| 날짜/시간형 | DATEONLY, DATETIME |
| 관계형 | FOREIGNKEY, BELONGSTO |
| 조합형 | DTO, QUERY, ARRAY, PAGING |
FOREIGNKEY와 BELONGSTO는 엔티티 전용이며 Swagger 출력이 없습니다. DTO, QUERY, ARRAY, PAGING는 DTO/라우터 전용이며 Sequelize 컬럼 출력이 없습니다.
전체 레퍼런스 — 모든 타입, 모든 옵션, 모든 출력 매핑 — 는 데이터 모델링 API 레퍼런스를 참고하세요.
요약
TypeIs는 ASAPJS가 진정한 저보일러플레이트 워크플로우를 제공할 수 있는 이유입니다. 단일 데코레이터가 Sequelize, Swagger, 강제 변환을 동시에 인코딩하기 때문에, 시스템에 새 필드를 추가한다는 것은 정확히 하나의 파일에서 정확히 한 줄만 수정하는 것을 의미합니다. 프레임워크의 나머지 부분은 그 레코드에서 자동으로 읽어갑니다.