설계 철학
ASAPJS는 **“As Simple As Possible”**을 지향하는 TypeScript 웹 프레임워크입니다. 반복적인 보일러플레이트를 제거하고, 하나의 정의로 여러 관심사를 동시에 해결하는 것을 목표로 합니다.
핵심 원칙
1. 한 번 정의, 여러 곳에 적용 (Single Source of Truth)
ASAPJS의 가장 핵심적인 설계 원칙은 하나의 정의가 데이터베이스 스키마와 API 문서를 동시에 생성하는 것입니다. TypeIs 데코레이터가 이 원칙을 구현합니다.
// packages/sequelize/src/types/ 에서 정의된 TypeIs 시스템
// 하나의 TypeIs.STRING() 데코레이터가 두 가지 역할을 수행합니다:
@TypeIs.STRING({ unique: true, comment: '이메일 (고유)' })
email: string;
// 1) Sequelize 컬럼 정의 → toSequelize()
// { type: DataTypes.STRING, unique: true, comment: '이메일 (고유)' }
// 2) Swagger 스키마 정의 → toSwagger()
// { type: 'string', description: '이메일 (고유)' }이 패턴은 DB 스키마와 API 문서가 항상 동기화되도록 보장합니다. 필드를 추가하거나 수정할 때 한 곳만 변경하면 됩니다.
TypeIs 시스템은 다음과 같은 타입들을 제공합니다:
| TypeIs | Sequelize | Swagger | 용도 |
|---|---|---|---|
TypeIs.INT | DataTypes.INTEGER | { type: 'integer' } | 정수 |
TypeIs.STRING | DataTypes.STRING | { type: 'string' } | 문자열 |
TypeIs.PASSWORD | DataTypes.STRING | { type: 'string', format: 'password' } | 비밀번호 |
TypeIs.BOOLEAN | DataTypes.BOOLEAN | { type: 'boolean' } | 불리언 |
TypeIs.DATETIME | DataTypes.DATE | { type: 'string', format: 'date-time' } | 날짜/시간 |
TypeIs.FOREIGNKEY | Foreign key 관계 | { type: 'integer' } | 외래키 |
TypeIs.BELONGSTO | BelongsTo 관계 | 관계 스키마 | 소속 관계 |
TypeIs.ENUM | DataTypes.ENUM | { enum: [...] } | 열거형 |
TypeIs.JSON | DataTypes.JSON | { type: 'object' } | JSON |
TypeIs.TEXT | DataTypes.TEXT | { type: 'string' } | 장문 텍스트 |
2. 데코레이터 기반 선언적 프로그래밍
ASAPJS는 TypeScript 데코레이터를 적극 활용하여 “무엇을” 정의하는 데 집중하고, “어떻게” 동작하는지는 프레임워크가 처리합니다.
라우트 데코레이터 — Express 라우트와 Swagger 문서를 하나의 데코레이터로 선언합니다:
// packages/router/src/decorator/index.ts
@Post('/register', {
title: '회원 가입',
body: CreateUserDto, // 요청 바디 스키마 → Swagger + 유효성 검사
response: UserInfoDto, // 응답 스키마 → Swagger
auth: false, // JWT 인증 불필요
})
async register({ body }: ExecuteArgs) {
return await this.userService.register(body);
}이 한 줄의 데코레이터가 내부적으로 수행하는 작업:
- Express 라우터에
POST /register핸들러 등록 jwtVerification(false)미들웨어 적용Wrapper로 요청/응답 포맷팅 및 에러 핸들링 래핑- Swagger에 경로, 요청/응답 스키마 자동 등록
테이블 데코레이터 — Sequelize 모델과 메타데이터를 선언적으로 정의합니다:
// packages/sequelize/src/table/index.ts 에서 처리
@Table({ tableName: 'users', timestamps: true })
export default class UsersTable extends Model {
@TypeIs.INT({ primaryKey: true, autoIncrement: true })
id: number;
}
// → Sequelize 컬럼 정의, 타임스탬프 설정, DBML 생성, 콘솔 등록이 모두 자동 처리3. Controller → Application → Entity 레이어드 아키텍처
ASAPJS는 명확한 3계층 구조를 권장합니다:
Controller (HTTP 인터페이스)
↓ ExecuteArgs { body, query, user, paging }
Application (비즈니스 로직)
↓ Sequelize API
Entity (데이터 접근, @Table 모델)각 레이어의 책임:
| 레이어 | 위치 | 책임 |
|---|---|---|
| Controller | controller/UserController.ts | HTTP 요청 수신, 라우트 데코레이터, Application 호출 |
| Application | application/UserApplication.ts | 비즈니스 로직, 검증, 트랜잭션 관리 |
| Entity | domain/entity/UsersTable.ts | DB 스키마 정의, Sequelize 모델 |
이 구조의 장점:
- 테스트 용이성: Application 레이어를 HTTP 서버 없이 직접 테스트 가능
- 관심사 분리: 각 레이어가 독립적으로 변경 가능
- 명확한 데이터 흐름: 요청 → 컨트롤러 → 비즈니스 로직 → DB → 응답
4. 자동 탐색 (Auto-Discovery)
ASAPJS는 파일 이름 규칙을 통해 모듈을 자동으로 발견하고 등록합니다. 수동 임포트나 등록 코드가 필요 없습니다.
// packages/sequelize/src/sequelize/index.ts 에서 구현
const loadPath = async (path: string, subPath: string) => {
const files = fs.readdirSync(path + subPath, { withFileTypes: true });
for (const file of files) {
if (file.isDirectory()) {
await loadPath(path, `${subPath}/${file.name}`);
} else if (extensionInclude('Table', file.name)) {
models.push(require(`${path}/${subPath}/${file.name}`).default);
} else if (extensionInclude('Dto', file.name)) {
dtos.push(require(`${path}/${subPath}/${file.name}`).default);
}
}
};자동 탐색 규칙:
| 파일 패턴 | 발견 시점 | 용도 |
|---|---|---|
*Table.ts | initSequelizeModule() | Sequelize 모델로 등록 |
*Dto.ts | initSequelizeModule() | Swagger 스키마로 등록 |
*Socket.ts | initSocketModule() | Socket.IO 핸들러로 등록 |
route.ts | RouterModule() | 컨트롤러 라우트 로딩 |
5. 통일된 요청/응답 래핑 (Wrapper Pattern)
모든 라우트 핸들러는 Wrapper 함수로 감싸집니다. 이 패턴은 반복적인 요청 파싱과 에러 처리를 추상화합니다.
// packages/router/src/utils/wrapper.ts
export interface ExecuteArgs<P = {}, Q = {}, B = {}> {
req: Request;
res: Response;
path?: P; // URL 파라미터 (/users/:id → { id: '1' })
query: Q; // 쿼리 스트링 (?page=0&limit=20)
body: B; // 요청 바디
files?: any; // 업로드된 파일
user?: any; // JWT 디코딩된 사용자 정보
paging: PaginationQueryDto; // ?page=&limit= 자동 추출
}핸들러는 ExecuteArgs를 받아 결과를 반환하기만 하면 됩니다. Wrapper가 자동으로:
req.body,req.query,req.params를 구조화된ExecuteArgs로 변환?page=0&limit=20에서 페이지네이션 정보 자동 추출- 반환값을
res.status(200).json(output)으로 응답 - 에러 발생 시
HttpException의 status/message로 응답 - 500 에러 시 Sentry 연동 (설정된 경우)
6. 모노레포 — 관심사별 패키지 분리
ASAPJS는 Lerna + Yarn Workspaces 기반 모노레포로 구성됩니다. 각 패키지는 독립적인 npm 패키지로 발행 가능하며, 필요한 기능만 선택적으로 사용할 수 있습니다.
packages/
core/ @asapjs/core — Application, 설정, 로거 (필수)
router/ @asapjs/router — HTTP 라우팅, 데코레이터, Swagger, JWT (필수)
sequelize/ @asapjs/sequelize — ORM, TypeIs, DTO, Repository (선택)
socket/ @asapjs/socket — Socket.IO 통합 (선택)extensions 배열을 통해 필요한 패키지만 활성화합니다:
const config = {
extensions: ['@asapjs/sequelize'], // Sequelize만 사용
// extensions: ['@asapjs/sequelize', '@asapjs/socket'], // + Socket.IO
};프레임워크 초기화 흐름
Application.run()이 호출되면 다음 순서로 모듈이 초기화됩니다:
Application.run()
│
├─ RouterModule(dirname)
│ ├─ initMiddlewares() CORS, bodyParser, 커스텀 미들웨어
│ ├─ require(route.ts) 컨트롤러 로딩
│ ├─ registerRoutes() @Get/@Post/@Put/@Delete 처리
│ └─ Swagger UI 마운트 /docs/swagger-ui.html
│
├─ initSequelizeModule(dirname) [extensions에 포함 시]
│ ├─ dbInit() DB 연결
│ ├─ loadPath() *Table.ts, *Dto.ts 자동 탐색
│ ├─ addModelsToSequelize() 모델 등록
│ └─ addDtosToSwagger() DTO → Swagger 스키마
│
├─ initSocketModule(server) [extensions에 포함 시]
│ ├─ loadPath() *Socket.ts 자동 탐색
│ └─ socketInit() Socket.IO 서버 시작
│
├─ initBeforeStartServer() 사용자 콜백 (DB sync 등)
│
└─ server.listen(port) HTTP 서버 시작관련 문서
- Bootstrap —
Application클래스와 초기화 상세 - Swagger — TypeIs → OpenAPI 스키마 자동 생성
- Your First API — 레이어드 아키텍처 실습