Skip to Content
문서시작하기프로젝트 구조

프로젝트 구조

ASAPJS는 도메인 기반 계층화 아키텍처를 중심으로 설계되었습니다. 애플리케이션의 각 기능은 자체 도메인 폴더에 위치하며, 각 도메인 내에서 코드는 Controller, Application, Entity의 세 가지 레이어로 분리됩니다.

권장 폴더 레이아웃

src/ user/ application/ UserApplication.ts # 비즈니스 로직 controller/ UserController.ts # HTTP 라우트 핸들러 domain/ entity/ UsersTable.ts # Sequelize 모델 (자동 탐색됨) dto/ CreateUserDto.ts # 요청 본문 형태 (자동 탐색됨) UserInfoDto.ts # 응답 형태 (자동 탐색됨) errors/ UserErrors.ts # 도메인별 에러 정의 (@asapjs/error) infra/ UserTableRepository.ts # Repository 패턴 (선택사항) post/ application/ PostApplication.ts controller/ PostController.ts domain/ entity/ PostsTable.ts dto/ CreatePostDto.ts PostInfoDto.ts utils/ # 공유 유틸리티 (jwt 헬퍼 등) jwt.ts index.ts # 애플리케이션 진입점 route.ts # 컨트롤러 레지스트리

Socket.io를 사용하는 프로젝트의 경우, controller/ 옆에 socket/ 폴더를 추가하세요:

src/ chat/ socket/ ChatSocket.ts # Socket 핸들러 (자동 탐색됨) controller/ ChatController.ts

세 가지 레이어

ASAPJS는 세 개의 핵심 레이어에 걸쳐 엄격한 관심사 분리를 강제합니다. 각 레이어는 하나의 역할을 담당하며 바로 아래 레이어하고만 통신해야 합니다. 선택적으로 infra/ 폴더에 Repository 패턴을 도입하여 Application과 Entity 사이에 데이터 접근 레이어를 추가할 수 있습니다 (데이터베이스 관리 참조).

Controller (*Controller.ts)

Controller는 도메인의 HTTP 경계입니다. 다음을 담당합니다:

  • @asapjs/routerRouterController를 확장
  • 데코레이터 (@Get, @Post, @Put, @Delete)를 사용하여 Express 라우트 등록
  • ExecuteArgs를 통해 들어오는 요청에서 body, query, user (JWT 페이로드), paging 추출
  • 모든 비즈니스 로직을 Application 레이어에 위임
  • 데이터베이스에 직접 접근하지 않음
// src/user/controller/UserController.ts import { RouterController, Get, Post, ExecuteArgs } from '@asapjs/router'; import UserApplication from '../application/UserApplication'; import CreateUserDto from '../dto/CreateUserDto'; import UserInfoDto from '../dto/UserInfoDto'; export default class UserController extends RouterController { public tag = 'User'; // Swagger 태그 그룹 public basePath = '/users'; // 라우트 접두사: /api/users private userService: UserApplication; constructor() { super(); this.registerRoutes(); // 필수: 데코레이팅된 모든 메서드를 등록 this.userService = new UserApplication(); } @Post('/register', { title: 'Register user', body: CreateUserDto, response: UserInfoDto, auth: false, // 공개 엔드포인트 }) async register({ body }: ExecuteArgs) { return await this.userService.register(body as CreateUserDto); } @Get('/me', { title: 'Get current user', response: UserInfoDto, auth: true, // JWT 토큰 필요 }) async getMe({ user }: ExecuteArgs) { return await this.userService.getUserInfo(user); } }

Application (*Application.ts)

Application 클래스는 비즈니스 로직을 담습니다. 다음을 담당합니다:

  • 일반 TypeScript 클래스 (특별한 기본 클래스 불필요)
  • Controller로부터 DTO 또는 원시 값을 받음
  • 하나 이상의 Table 엔티티에 대한 호출 조율
  • 프레임워크의 에러 래퍼가 HTTP 응답으로 포맷하는 설명적인 오류 발생
// src/user/application/UserApplication.ts import UsersTable from '../domain/entity/UsersTable'; import CreateUserDto from '../dto/CreateUserDto'; export default class UserApplication { private users = UsersTable; async register(dto: CreateUserDto) { const existing = await this.users.findOne({ where: { email: dto.email } }); if (existing) { throw new Error('Email already exists'); } return this.users.create({ email: dto.email, name: dto.name } as any); } async getUserInfo(user: any) { if (!user?.id) throw new Error('Unauthorized'); return this.users.findByPk(user.id); } }

Entity (*Table.ts)

Entity는 Sequelize 모델입니다. 다음을 담당합니다:

  • sequelize-typescriptModel을 확장
  • @asapjs/sequelize@Table로 데코레이팅
  • 컬럼 타입, Swagger 스키마, 유효성 검사를 한 곳에서 정의하기 위해 TypeIs.* 데코레이터 사용
  • 시작 시 Sequelize 익스텐션에 의해 자동 탐색됨 (아래 명명 규칙 참조)
// 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 }) id: number; @TypeIs.STRING({ unique: true, comment: 'Email address' }) email: string; @TypeIs.STRING({ comment: 'Display name' }) name: string; @TypeIs.PASSWORD({ comment: 'Hashed password' }) password: string; @TypeIs.DATETIME({ comment: 'Created at' }) created_at: Date; }

route.ts 파일

route.ts (또는 route.js)는 컨트롤러 레지스트리입니다. 다음 조건을 충족해야 합니다:

  • 진입점 (index.ts)과 같은 디렉토리에 위치
  • 컨트롤러 인스턴스의 기본 배열을 내보내기
// src/route.ts import UserController from './user/controller/UserController'; import PostController from './post/controller/PostController'; export default [new UserController(), new PostController()];

ASAPJS는 시작 시 이 파일을 읽고 모든 컨트롤러의 라우트를 Express에 등록합니다. 배열의 항목 순서는 라우팅에 영향을 주지 않습니다.


파일 명명 규칙

ASAPJS는 특정 파일을 자동으로 탐색하기 위해 파일명 패턴을 사용합니다. 수동으로 임포트하거나 등록할 필요가 없습니다 — 명명 규칙만 따르면 됩니다.

*Table.ts — Sequelize 엔티티

Table.ts로 끝나는 모든 파일 (src/ 아래 어디서든)은 config.extensions@asapjs/sequelize가 나열되어 있을 때 자동으로 Sequelize 모델로 등록됩니다. 예시:

UsersTable.ts PostsTable.ts CommentsTable.ts

*Dto.ts — 데이터 전송 객체

Dto.ts로 끝나는 파일은 스키마 검사와 Swagger 생성을 위해 Sequelize 익스텐션에 의해 자동 탐색됩니다. 예시:

UserInfoDto.ts CreateUserDto.ts PaginatedPostsDto.ts

DTO는 @asapjs/sequelizeExtendableDto를 확장하고 @Dto 데코레이터를 적용하여 자동 탐색에 참여합니다. @Dto는 테이블 연결과 Swagger 스키마 등록을 자동으로 처리합니다.

*Socket.ts — Socket.io 핸들러

@asapjs/socket이 로드되면, src/ 아래에서 Socket.ts로 끝나는 파일이 Socket.io 이벤트 핸들러로 자동 등록됩니다. 예시:

ChatSocket.ts NotificationSocket.ts

진입점 (index.ts)

진입점은 모든 것을 연결합니다. 다음 역할을 담당합니다:

  1. 가장 첫 번째 줄에 reflect-metadata 임포트
  2. 애플리케이션 config 객체 정의
  3. __dirnameconfigApplication 인스턴스 생성
  4. 시작 후 작업(예: 데이터베이스 동기화, 시딩)을 위한 선택적 비동기 콜백과 함께 app.run() 호출
// src/index.ts import 'reflect-metadata'; import dotenv from 'dotenv'; import { Application } from '@asapjs/core'; dotenv.config(); const config = { name: 'my-api', port: Number(process.env.PORT) || 3000, basePath: 'api', extensions: ['@asapjs/router', '@asapjs/sequelize'], auth: { jwt_access_token_secret: process.env.JWT_SECRET || 'dev-secret', }, db: { username: process.env.DB_USER || 'root', password: process.env.DB_PASSWORD || '', database: process.env.DB_NAME || 'myapp', host: process.env.DB_HOST || 'localhost', port: parseInt(process.env.DB_PORT || '3306', 10), dialect: 'mysql', logging: false, }, }; const app = new Application(__dirname, config); app.run(async () => { console.log(`Running at http://localhost:${config.port}/${config.basePath}`); });

__dirname 인자는 중요합니다: ASAPJS는 이것을 route.ts, *Table.ts, *Dto.ts, *Socket.ts 파일을 스캔할 루트 경로로 사용합니다.


여러 도메인으로 확장하기

애플리케이션이 성장함에 따라 user/post/ 옆에 새 도메인 폴더를 추가하면 됩니다. 새 컨트롤러는 route.ts에 등록하세요. 엔티티와 DTO 파일은 자동으로 탐색됩니다.

src/ user/ # 기존 도메인 post/ # 기존 도메인 comment/ # 새 도메인 — route.ts에 CommentController 추가 notification/ # 새 도메인 — 실시간 이벤트를 위해 NotificationSocket.ts 추가 index.ts route.ts

각 레이어에 대한 더 깊은 탐구를 원한다면 RouterController, TypeIs, Application의 API 참조 페이지를 확인하세요.

Last updated on