본문 바로가기
개발일지/TypeScript

타입스크립트에서 사용하는 Enum과 DTO(Data Transfer Object)

by Peter.JH 2024. 11. 27.
728x90
반응형

출처: https://ko.wikipedia.org/wiki/%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8

 

타입스크립트(TypeScript)는 자바스크립트의 슈퍼셋으로, 정적 타입을 제공하여 코드의 안정성과 유지보수성을 향상시킵니다. 백엔드 개발에서는 데이터 모델링과 상태 관리, 데이터 전송을 효율적으로 하기 위해 enumDTO(Data Transfer Object)를 자주 사용합니다. 이번 포스트에서는 이 두 가지 개념을 깊이 있게 살펴보며, 이를 효과적으로 활용하는 방법을 알아보겠습니다.

 


 

타입스크립트 Enum 이해하기

 

Enum의 정의 및 기본 사용법

Enum은 관련된 상수 집합을 정의할 때 사용하는 타입입니다. 이를 통해 코드의 가독성을 높이고, 값의 집합을 명확하게 표현할 수 있습니다.

enum Direction {
    Up,
    Down,
    Left,
    Right
}

let move: Direction = Direction.Up;
console.log(move); // 출력: 0

 

숫자 Enum (Numeric Enum)

 

기본적으로 타입스크립트의 enum은 숫자 값을 가집니다. 첫 번째 멤버는 0부터 시작하며, 이후 멤버는 자동으로 증가합니다.

enum Status {
    Active,    // 0
    Inactive,  // 1
    Pending    // 2
}

console.log(Status.Active);   // 출력: 0
console.log(Status.Inactive); // 출력: 1
console.log(Status.Pending);  // 출력: 2

 

문자열 Enum (String Enum)

 

각 멤버에 문자열 값을 직접 할당할 수도 있습니다. 문자열 enum은 더 직관적이고, 디버깅 시 유용합니다.

enum Response {
    Success = "SUCCESS",
    Failure = "FAILURE",
    Pending = "PENDING"
}

console.log(Response.Success); // 출력: SUCCESS
console.log(Response.Failure); // 출력: FAILURE
console.log(Response.Pending); // 출력: PENDING

 

이질적인 Enum (Heterogeneous Enum)

 

숫자와 문자열 값을 혼합하여 사용할 수 있지만, 일반적으로 권장되지 않습니다.

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES"
}

console.log(BooleanLikeHeterogeneousEnum.No);  // 출력: 0
console.log(BooleanLikeHeterogeneousEnum.Yes); // 출력: YES

 

Const Enum

 

const enum은 컴파일 시에 인라인으로 대체되어 성능을 최적화할 수 있습니다. 단, const enum은 선언된 멤버에 메타 데이터를 포함하지 않습니다.

const enum Size {
    Small,
    Medium,
    Large
}

let size: Size = Size.Medium;
console.log(size); // 컴파일 후: 1

 

Enum의 역매핑 (Reverse Mapping)

 

숫자 enum의 경우, 값으로부터 이름을 조회할 수 있습니다. 문자열 enum은 역매핑을 지원하지 않습니다.

enum Color {
    Red,
    Green,
    Blue
}

let colorName: string = Color[2];
console.log(colorName); // 출력: Blue

 

Best Practices for Enums

  • 명확한 이름 사용: enum과 멤버의 이름을 명확하게 지어 가독성을 높입니다.
  • 문자열 Enum 선호: 디버깅과 로깅 시 유용하므로 가능한 경우 문자열 enum을 사용하는 것이 좋습니다.
  • 이질적인 Enum 피하기: 숫자와 문자열을 혼합하지 않고 일관된 타입을 유지합니다.
  • const enum 사용 고려: 성능 최적화를 위해 const enum을 고려할 수 있지만, 외부 라이브러리와의 호환성을 확인해야 합니다.

 


 

DTO(Data Transfer Object) 이해하기

 

DTO의 정의 및 역할

DTO(Data Transfer Object)는 데이터 전송을 위해 사용되는 객체로, 주로 클라이언트와 서버 간의 데이터 교환에 사용됩니다. DTO는 모델과는 달리 데이터의 전송과 관련된 요구사항에 맞게 설계되며, 불필요한 데이터를 제외하고 필요한 데이터만 포함합니다.

 

DTO와 Model의 차이점

  • 목적:
    • Model: 데이터베이스와의 상호작용을 위해 사용됩니다. 데이터의 구조와 관계를 정의합니다.
    • DTO: 데이터 전송을 위해 사용됩니다. 클라이언트와 서버 간에 필요한 데이터만 전송합니다.
  • 구조:
    • Model: 데이터베이스 스키마와 일치하며, 추가적인 비즈니스 로직을 포함할 수 있습니다.
    • DTO: 클라이언트 요구사항에 맞게 간결하게 설계되며, 불필요한 속성을 제외합니다.
  • 변환:
    • Model ↔ DTO: 데이터를 전송할 때 모델을 DTO로 변환하고, 수신된 DTO를 모델로 변환합니다. 이 과정에서 데이터 유효성 검증과 변환 로직이 포함될 수 있습니다.

 

DTO 사용 예제

아래는 사용자 등록 시 사용되는 DTO 예제입니다.

// UserRegistrationDTO.ts
export interface UserRegistrationDTO {
    username: string;
    email: string;
    password: string;
}

// UserResponseDTO.ts
export interface UserResponseDTO {
    id: number;
    username: string;
    email: string;
    role: UserRole;
    createdAt: Date;
}

 

DTO 유효성 검증

 

DTO를 사용할 때는 데이터의 유효성을 검증하는 것이 중요합니다. 이를 위해 class-validator와 같은 라이브러리를 사용할 수 있습니다.

// CreateUserDTO.ts
import { IsEmail, IsEnum, IsNotEmpty, MinLength } from 'class-validator';
import { UserRole } from './UserRole';

export class CreateUserDTO {
    @IsNotEmpty()
    username!: string;

    @IsEmail()
    email!: string;

    @MinLength(6)
    password!: string;

    @IsEnum(UserRole)
    role!: UserRole;
}

 

 


 

Enum과 DTO의 통합 사용

 

실제 사용 사례

enumDTO를 함께 사용하면 데이터의 상태나 유형을 명확하게 정의하고, 데이터 전송을 타입 안전하게 관리할 수 있습니다. 예를 들어, 사용자 관리 시스템에서 역할 기반 접근 제어를 구현할 때 이 두 가지를 통합하여 사용할 수 있습니다.

 

예제: 사용자 역할 관리 및 데이터 전송

// UserRole.ts
export enum UserRole {
    Admin = "ADMIN",
    User = "USER",
    Guest = "GUEST"
}

// UserModel.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from "typeorm";
import { UserRole } from "./UserRole";

@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id: number;

    @Column({ unique: true })
    username: string;

    @Column()
    email: string;

    @Column({
        type: "enum",
        enum: UserRole,
        default: UserRole.User
    })
    role: UserRole;

    @CreateDateColumn()
    createdAt: Date;
}

// CreateUserDTO.ts
import { IsEmail, IsEnum, IsNotEmpty, MinLength } from 'class-validator';
import { UserRole } from './UserRole';

export class CreateUserDTO {
    @IsNotEmpty()
    username!: string;

    @IsEmail()
    email!: string;

    @MinLength(6)
    password!: string;

    @IsEnum(UserRole)
    role!: UserRole;
}

// UserResponseDTO.ts
export interface UserResponseDTO {
    id: number;
    username: string;
    email: string;
    role: UserRole;
    createdAt: Date;
}

// UserService.ts
import { User } from './UserModel';
import { CreateUserDTO } from './CreateUserDTO';
import { UserResponseDTO } from './UserResponseDTO';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';

export class UserService {
    async createUser(createUserDTO: CreateUserDTO): Promise<UserResponseDTO> {
        // DTO 유효성 검증
        const dto = plainToClass(CreateUserDTO, createUserDTO);
        const errors = await validate(dto);
        if (errors.length > 0) {
            throw new Error('Validation failed!');
        }

        // 모델 생성 및 데이터베이스 저장
        const user = new User();
        user.username = createUserDTO.username;
        user.email = createUserDTO.email;
        user.role = createUserDTO.role;
        // 비밀번호 해싱 로직 추가 필요
        // user.password = hash(createUserDTO.password);
        await user.save();

        // 응답 DTO 생성
        const userResponse: UserResponseDTO = {
            id: user.id,
            username: user.username,
            email: user.email,
            role: user.role,
            createdAt: user.createdAt
        };

        return userResponse;
    }
}

 

위 예제에서는 enum을 사용하여 사용자 역할을 정의하고, 이를 DTO와 통합하여 사용자 등록과 응답 과정을 타입 안전하게 관리하고 있습니다. enum을 사용함으로써 역할의 가능한 값을 명확하게 정의하고, DTO를 통해 데이터 전송 시 유효성을 검증하며, UserService에서 이를 효과적으로 활용하여 안전하고 유지보수 가능한 코드를 작성할 수 있습니다.

 

 


 

Enum 확장 및 유틸리티 함수

enum을 확장하거나, 유틸리티 함수를 통해 enum 값을 쉽게 변환하거나 활용할 수 있습니다.

enum Status {
    Active = "ACTIVE",
    Inactive = "INACTIVE",
    Pending = "PENDING"
}

function getStatusDescription(status: Status): string {
    switch(status) {
        case Status.Active:
            return "활성 상태";
        case Status.Inactive:
            return "비활성 상태";
        case Status.Pending:
            return "대기 중";
        default:
            return "알 수 없는 상태";
    }
}

console.log(getStatusDescription(Status.Active)); // 출력: 활성 상태

 

타입 안전한 DTO 설계

 

타입스크립트를 활용하여 DTO의 타입 안전성을 극대화하고, 런타임 오류를 줄일 수 있습니다.

enum OrderStatus {
    Pending = "PENDING",
    Completed = "COMPLETED",
    Cancelled = "CANCELLED"
}

interface IOrder {
    id: number;
    userId: number;
    amount: number;
    status: OrderStatus;
    createdAt: Date;
}

class Order implements IOrder {
    constructor(
        public id: number,
        public userId: number,
        public amount: number,
        public status: OrderStatus,
        public createdAt: Date = new Date()
    ) {}

    completeOrder(): void {
        if (this.status !== OrderStatus.Pending) {
            throw new Error("Only pending orders can be completed.");
        }
        this.status = OrderStatus.Completed;
    }
}

const order = new Order(1, 1, 100, OrderStatus.Pending);
order.completeOrder();
console.log(order.status); // 출력: COMPLETED

 

자동 변환 및 매핑

 

모델과 DTO 간의 변환을 자동화하여 코드의 중복을 줄이고, 변환 로직을 일관되게 유지할 수 있습니다. 이를 위해 class-transformerautomapper와 같은 라이브러리를 사용할 수 있습니다.

import { plainToClass } from 'class-transformer';
import { UserResponseDTO } from './UserResponseDTO';
import { User } from './UserModel';

function transformToDTO(user: User): UserResponseDTO {
    return plainToClass(UserResponseDTO, user, { excludeExtraneousValues: true });
}

const user = new User(1, "john_doe", "john@example.com", UserRole.Admin);
const userDTO = transformToDTO(user);
console.log(userDTO);

 

class-transformer를 사용하면 객체 간의 변환을 간단하게 처리할 수 있으며, 이를 통해 모델과 DTO 간의 매핑을 자동화할 수 있습니다.

 

 

 


 

 

타입스크립트의 enumDTO는 백엔드 개발에서 데이터의 상태와 유형을 명확하게 정의하고, 데이터 전송을 타입 안전하게 관리하는 데 중요한 역할을 합니다. enum을 활용하여 관련 상수들을 그룹화하고, DTO를 통해 데이터 전송을 효율적으로 관리함으로써 코드의 가독성과 유지보수성을 크게 향상시킬 수 있습니다. 또한, 유효성 검증과 자동 매핑 도구를 활용하여 더욱 안정적이고 효율적인 애플리케이션을 개발할 수 있습니다.

 

 

 

 

 

참고 자료

https://www.typescriptlang.org/ko/docs/handbook/enums.html

 

Handbook - Enums

How TypeScript enums work

www.typescriptlang.org

 

https://www.typescriptlang.org/docs/handbook/interfaces.html

 

Handbook - Interfaces

How to write an interface with TypeScript

www.typescriptlang.org

 

https://amirmustafaofficial.medium.com/data-transfer-objects-dto-pattern-in-programming-53f2a51a5cc9

 

Data Transfer Objects (DTO) Pattern in Programming

→ DTOs are a common concept in Software Development eg. Nest.js framework of Node.js, Java Spring Boot, etc.

amirmustafaofficial.medium.com

 

 

 

728x90
반응형