Skip to Content
문서가이드사용자 CRUD

사용자 CRUD

이 가이드에서는 ASAPJS의 레이어드 아키텍처(Controller → Application → Entity)를 따라 사용자 도메인의 회원가입, 로그인, 내 정보 조회 기능을 처음부터 구현합니다.


디렉토리 구조

ASAPJS에서 각 도메인은 다음 구조를 따릅니다.

src/user/ ├── controller/ │ └── UserController.ts # HTTP 경계 — 라우트 데코레이터 ├── application/ │ └── UserApplication.ts # 비즈니스 로직 — 검증, 해싱, 토큰 발급 ├── domain/ │ └── entity/ │ └── UsersTable.ts # 데이터 경계 — @Table 모델 └── dto/ ├── CreateUserDto.ts # 회원가입 요청 형태 ├── LoginRequestDto.ts # 로그인 요청 형태 ├── LoginResponseDto.ts # 로그인 응답 형태 └── UserInfoDto.ts # 사용자 정보 응답 형태

구현은 Entity → DTO → Application → Controller 순서로 진행합니다. 하위 레이어부터 만들어야 상위 레이어에서 import할 수 있습니다.


Step 1: Entity 정의

@TableTypeIs.* 데코레이터로 users 테이블의 스키마를 정의합니다.

// 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: '사용자 ID' }) id: number; @TypeIs.STRING({ unique: true, comment: '이메일 (고유)' }) email: string; @TypeIs.PASSWORD({ comment: '비밀번호 (bcrypt)' }) password: string; @TypeIs.STRING({ comment: '사용자 이름' }) name: string; @TypeIs.DATETIME({ comment: '생성 일시' }) created_at: Date; @TypeIs.DATETIME({ comment: '수정 일시' }) updated_at: Date; }

핵심 포인트

  • @Table({ timestamps: true }): Sequelize가 created_at/updated_at을 자동 관리합니다.
  • TypeIs.INT({ primaryKey: true, autoIncrement: true }): 자동 증가 기본키를 선언합니다.
  • TypeIs.STRING({ unique: true }): 유니크 제약 조건을 추가합니다.
  • TypeIs.PASSWORD: Swagger에서 format: password로 표시되며, Sequelize 컬럼 타입은 STRING과 동일합니다.
  • TypeIs.DATETIME: 날짜/시간 컬럼과 Swagger format: date-time을 동시에 정의합니다.

TypeIs.* 데코레이터는 Sequelize 컬럼 정의Swagger 스키마 속성을 하나의 선언으로 처리합니다.


Step 2: DTO 정의

DTO는 ExtendableDto를 확장하여 요청/응답의 형태를 정의합니다. Entity와 동일한 TypeIs.* 데코레이터를 사용하지만, 필요한 필드만 선택적으로 포함합니다.

회원가입 요청 DTO

// example/src/user/dto/CreateUserDto.ts import { ExtendableDto, Dto, TypeIs } from '@asapjs/sequelize'; import UsersTable from '../domain/entity/UsersTable'; @Dto({ name: 'create_user_dto', defineTable: UsersTable, timestamps: false }) export default class CreateUserDto extends ExtendableDto { @TypeIs.STRING({ comment: '이메일' }) email: string; @TypeIs.PASSWORD({ comment: '비밀번호' }) password: string; @TypeIs.STRING({ comment: '사용자 이름' }) name: string; }

사용자 정보 응답 DTO

// example/src/user/dto/UserInfoDto.ts import { ExtendableDto, Dto, TypeIs } from '@asapjs/sequelize'; import UsersTable from '../domain/entity/UsersTable'; @Dto({ name: 'user_info_dto', defineTable: UsersTable, timestamps: false }) export default class UserInfoDto extends ExtendableDto { @TypeIs.INT({ comment: '사용자 ID' }) id: number; @TypeIs.STRING({ comment: '이메일' }) email: string; @TypeIs.STRING({ comment: '사용자 이름' }) name: string; }

DTO 작성 패턴

모든 DTO는 동일한 구조를 따릅니다:

  1. ExtendableDto를 확장합니다.
  2. @Dto 데코레이터로 연관 테이블과 이름을 지정합니다. @DtoReflect.defineMetadatainit() 호출을 자동으로 처리합니다.
  3. TypeIs.* 데코레이터로 각 필드를 선언합니다.
  4. timestamps: false로 설정하면 created_at/updated_at이 DTO에 포함되지 않습니다.

요청 DTO에는 클라이언트가 보내는 필드만 포함합니다 (email, password, name). 응답 DTO에는 클라이언트에게 반환할 필드만 포함합니다 (id, email, namepassword 제외).


Step 3: Application 레이어

비즈니스 로직을 담당합니다. Express에 대한 의존성이 없으며, 순수 TypeScript 클래스입니다.

// example/src/user/application/UserApplication.ts import bcrypt from 'bcrypt'; import UsersTable from '../domain/entity/UsersTable'; import CreateUserDto from '../dto/CreateUserDto'; import LoginRequestDto from '../dto/LoginRequestDto'; import { jwtSign } from '../../utils/jwt'; export default class UserApplication { private users: typeof UsersTable; constructor() { this.users = UsersTable; } async register(dto: CreateUserDto) { // 중복 체크 const existingUser = await this.users.findOne({ where: { email: dto.email } }); if (existingUser) { throw new Error('Email already exists'); } // 비밀번호 해싱 const hashedPassword = await bcrypt.hash(dto.password, 10); // 사용자 생성 const user = await this.users.create({ email: dto.email, password: hashedPassword, name: dto.name, } as any); return { id: user.id, email: user.email, name: user.name, }; } async login(dto: LoginRequestDto) { // 사용자 조회 const user = await this.users.findOne({ where: { email: dto.email } }); if (!user) { throw new Error('Invalid email or password'); } // 비밀번호 검증 const isPasswordValid = await bcrypt.compare(dto.password, user.password); if (!isPasswordValid) { throw new Error('Invalid email or password'); } // JWT 토큰 생성 const payload = { userId: user.id, email: user.email }; const accessToken = jwtSign(payload, '1d'); const refreshToken = jwtSign(payload, '7d'); return { accessToken, refreshToken, }; } async getUserInfo(user: any) { if (!user || !user.userId) { throw new Error('Unauthorized'); } const userRecord = await this.users.findByPk(user.userId); if (!userRecord) { throw new Error('User not found'); } return { id: userRecord.id, email: userRecord.email, name: userRecord.name, }; } }

설계 원칙

  • Express 무의존: Request, Response, next를 import하지 않습니다. HTTP 없이 단위 테스트가 가능합니다.
  • 에러 위임: throw new Error()로 던진 에러는 Wrapper가 잡아서 HTTP 에러 응답으로 자동 변환합니다.
  • Sequelize 정적 API: findOne, findByPk, create 등 Entity의 정적 메서드를 직접 호출합니다.
  • 보안: register에서 비밀번호를 bcrypt로 해싱하고, login에서 bcrypt.compare로 비교합니다.

Step 4: Controller 레이어

HTTP 경계를 담당합니다. 라우트 데코레이터로 경로와 Swagger 문서를 동시에 정의하고, 모든 로직을 Application에 위임합니다.

// example/src/user/controller/UserController.ts import { RouterController, Get, Post, ExecuteArgs } from '@asapjs/router'; import UserApplication from '../application/UserApplication'; import CreateUserDto from '../dto/CreateUserDto'; import LoginRequestDto from '../dto/LoginRequestDto'; import LoginResponseDto from '../dto/LoginResponseDto'; import UserInfoDto from '../dto/UserInfoDto'; export default class UserController extends RouterController { public tag = 'User'; public basePath = '/users'; private userService: UserApplication; constructor() { super(); this.registerRoutes(); this.userService = new UserApplication(); } @Post('/register', { title: '회원 가입', description: '새로운 사용자를 등록합니다', body: CreateUserDto, response: UserInfoDto, }) async register({ body }: ExecuteArgs) { const dto = body as CreateUserDto; return await this.userService.register(dto); } @Post('/login', { title: '로그인', description: '이메일과 비밀번호로 로그인합니다', body: LoginRequestDto, response: LoginResponseDto, }) async login({ body }: ExecuteArgs) { const dto = body as LoginRequestDto; return await this.userService.login(dto); } @Get('/me', { title: '내 정보 조회', description: '현재 로그인한 사용자의 정보를 조회합니다', auth: true, response: UserInfoDto, }) async getMe({ user }: ExecuteArgs) { return await this.userService.getUserInfo(user); } }

핵심 포인트

  • registerRoutes(): 반드시 constructor에서 super() 다음에 호출해야 합니다. 데코레이터가 수집한 라우트 메타데이터를 Express Router에 바인딩합니다.
  • tag: Swagger UI에서 이 컨트롤러의 모든 라우트를 그룹화하는 라벨입니다.
  • basePath: 모든 라우트의 URL 접두어입니다. /register는 실제로 /users/register가 됩니다.
  • 핸들러 본문: 1~2줄이 이상적입니다. body를 DTO로 캐스팅하고 Application에 위임합니다.
  • auth: true: @Get('/me', ...)에만 설정하여 이 라우트만 JWT 토큰을 요구합니다.

Step 5: 라우트 등록

컨트롤러를 route.ts에 등록하면 RouterModule이 자동으로 Express에 마운트합니다.

// example/src/route.ts import UserController from './user/controller/UserController'; export default [new UserController()];

요청 흐름 추적

POST /api/users/register 요청이 처리되는 과정을 단계별로 살펴봅니다.

POST /api/users/register Content-Type: application/json { "email": "alice@example.com", "password": "secret123", "name": "Alice" }

1. Express 라우트 매칭 RouterModule이 시작 시 등록한 UserController.expressRouter에서 /users/register POST 경로를 찾습니다.

2. jwtVerification 실행 auth 옵션이 설정되지 않았으므로(기본값 false) JWT 검증을 건너뜁니다.

3. Wrapper 실행 req.body, req.query, req.paramsExecuteArgs 객체로 조합하고, PaginationQueryDto를 생성합니다.

4. Controller 핸들러

async register({ body }: ExecuteArgs) { const dto = body as CreateUserDto; return await this.userService.register(dto); }

5. Application 로직 UserApplication.register가 이메일 중복 체크 → 비밀번호 해싱 → UsersTable.create → 결과 반환을 수행합니다.

6. 응답 반환 Wrapper가 반환값을 res.status(200).json(output)으로 전송합니다.

{ "id": 1, "email": "alice@example.com", "name": "Alice" }

에러 경로: Application에서 throw new Error('Email already exists')가 발생하면 Wrapper가 잡아서 { status: 500, message: '알 수 없는 오류가 발생했습니다.' }로 응답합니다.


API 엔드포인트 요약

메서드경로인증설명
POST/api/users/register불필요새 사용자 생성
POST/api/users/login불필요JWT 토큰 발급
GET/api/users/me필수로그인한 사용자 정보 조회

관련 문서

Last updated on