iamkanguk.dev

[NestJS + 맵필로그] Nest에서의 DTO, Entity 그리고 상관관계 (분리하는 이유? 등..) 본문

Framework/NestJS

[NestJS + 맵필로그] Nest에서의 DTO, Entity 그리고 상관관계 (분리하는 이유? 등..)

iamkanguk 2023. 11. 10. 22:58

지금 하는 맵필로그 프로젝트 이전에는 모두의 여행, 트리퍼라는 프로젝트를 했는데, 그 프로젝트는 express와 javascript로 개발을 했었다. 단순히 express로 개발을 할 때는 DTO와 Entity 그리고 OOP에 대해서는 진짜 아예 사용하지도 않았고 무지했던 것 같다.

 

하지만, Nest를 시작해 보면서 자연스럽게 DTO와 Entity를 알아야 했고, 프레임워크 특성상 OOP의 개념도 가지고 있어야 개발에 차질이 없을 것 같아서 이렇게 공부를 시작하게 되었다.

 

그래서 이번 포스팅에서는 DTO와 Entity 개념이 간단하게 무엇인지, 그리고 Nest에서 그 둘의 상관관계에 대해서 알아보도록 하자.

 

Entity

Entity는 실제 DB 테이블과 매핑되는 핵심 클래스이고, 데이터베이스 테이블에 있는 칼럼들을 필드로 가지는 객체이다. 테이블과 1:1로 매핑이 되고 테이블이 가지고 있지 않은 컬럼을 필드로 가지고 있으면 안 된다.

 

그리고 Entity와 실제 데이터베이스의 테이블이 연결이 되어있기 때문에 수정이 되어선 안된다. 그렇기 때문에 setter 메서드를 사용하지 않는다. 이유는 setter 메서드를 사용하게 되면 Entity에 접근이 가능해지고 객체의 일관성과 안전성을 보장하기 힘들어진다.

 

그래서 보통 Entity에서는 setter 메서드 대신에 constructor(생성자)를 사용한다. 생성자를 사용해서 Entity 객체를 초기화하면 단단한 객체 (변하지 않는, 불변한)로 Layer에서 활용이 가능하기 때문에 데이터가 변조되지 않음을 보장할 수 있다.

 

import { Column, Entity, OneToMany } from 'typeorm';
import { ColorCodeLength, ColorNameLength } from '../constants/color.enum';
import { ScheduleEntity } from '../../schedule/entities/schedule.entity';
import { CommonEntity } from 'src/entities/common/common.entity';

@Entity('Color')
export class ColorEntity extends CommonEntity {
  @Column('varchar', { length: ColorNameLength.MAX })
  name: string;

  @Column('varchar', { length: ColorCodeLength.MAX })
  code: string;

  @OneToMany(() => ScheduleEntity, (schedule) => schedule.color)
  schedules: ScheduleEntity[];

  /** <10/30>
   * @deprecated Repository 적용으로 인한 deprecated
   */
  // static async selectColorList(): Promise<ColorEntity[]> {
  //   return await this.createQueryBuilder('color').getMany();
  // }
}

 

위의 코드는 지금 하고 있는 프로젝트에서 색깔 Entity를 구현한 것이다. Nest에서는 위와 같이 정의하면 될 것 같다. 참고로 CommonEntity는 필자가 Custom 한 것인데, 테이블에 보통 필수로 넣는 PK(id), createdAt, updatedAt, deletedAt을 가지고 있는 기본 칼럼들을 의미한다. TypeORM의 BaseEntity를 상속받는다.

 

그냥 Nest에서는 이런 식으로 정의한다~ 고 보여주고 싶어서 올려봤다!

 

DTO (Data Transfer Object)

DTO는 계층 간 데이터 교환이 이루어질 수 있도록 하는 객체이다. 원래는 DAO (Data Access Object) 패턴에서 유래된 단어인데, DB 처리 로직을 숨기고 DTO라는 결괏값을 내보내는 용도로 활용했다.

 

보통 Controller와 같은 클라이언트 단과 근접한 계층에서는 Entity 대신 DTO를 사용해서 데이터를 교환한다. 대표적인 예로는 우리가 클라이언트로부터 Request를 받는데, Body나 Param, Query 등 Parameter를 DTO Class에 매핑시켜서 Nest의 Class-Validator를 통해 유효성 검증을 하곤 한다.

 

그 외에도 다양한 경우나 Layer에서도 DTO를 사용할 수 있지만 보통은 MVC 구조에서 View와 Controller 사이에서 데이터를 주고받을 때 사용한다고 생각하자.

 

참고로 DTO는 Entity와 다르게 getter, setter 메서드를 포함할 수 있다.

 

import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { setValidatorContext } from 'src/common/common';
import { UserSnsTypeEnum } from '../constants/user.enum';
import { CheckColumnEnum } from 'src/constants/enum';
import { CommonExceptionCode } from 'src/common/exception-code/common.exception-code';
import { UserExceptionCode } from 'src/common/exception-code/user.exception-code';

export class LoginOrSignUpRequestDto {
  @IsString(setValidatorContext(CommonExceptionCode.MustStringType))
  @IsNotEmpty(setValidatorContext(UserExceptionCode.SocialAccessTokenEmpty))
  socialAccessToken: string;

  @IsEnum(
    UserSnsTypeEnum,
    setValidatorContext(UserExceptionCode.SocialVendorErrorType),
  )
  @IsNotEmpty(setValidatorContext(UserExceptionCode.SocialVendorEmpty))
  socialVendor: UserSnsTypeEnum;

  @IsString(setValidatorContext(CommonExceptionCode.MustStringType))
  @IsOptional()
  fcmToken?: string | undefined;

  @IsEnum(
    CheckColumnEnum,
    setValidatorContext(CommonExceptionCode.MustCheckColumnType),
  )
  @IsOptional()
  isAlarmAccept?: CheckColumnEnum | undefined;
}

 

위의 코드는 회원가입 그리고 로그인 요청 DTO이다. 아까 위에서 설명한 DTO 대표적인 예시에 대한 코드이다. 이 LoginOrSignUpRequestDto를 회원가입 및 로그인 요청 API의 controller 부분의 Body를 받는 부분에 Type으로 지정해 주면 된다.

 

참고로 setValidatorContext는 그냥 내 개인적인 스타일인데 Nest에서는 에러 메시지를 기본적으로 배열에 담아서 준다. 하지만 나는 클라이언트 분들이랑 협업할 때 메세지 string으로 1개만 주는 것을 원하고 각 에러 메시지에 대한 error code를 할당해 주는 것이 좋아서 이렇게 코드를 작성한다. 조금 더럽긴 하지만...? ㅎㅎ..

 

그래서 위에 보이는 IsString이나 IsEnum 등 각 Class-validator에서 제공하는 decorator에서 context를 지정해 줄 수 있는데 그 context 요소를 만들어주는 custom helper 함수이다.

 

Entity와 DTO를 분리하는 이유는?

(1) MVC 패턴에서 View와 Model의 분리로 인한 코드 가독성과 유지보수 용이함

먼저, DTO는 간단하게 정리해 보면 Entity와 달리 각 계층끼리 주고받는 물건이다. 어떻게 보면 Entity랑 비슷하지 않나?라는 생각이 들 수 있지만 DTO는 마음대로 쓰고~ 버리고 할 수 있는 일회성 객체라고 생각하면 좋을 것 같다.

 

조금 어렵게 설명하자면 DTO는 View와 Controller 간의 인터페이스 역할을 하고, Entity는 Model의 역할을 한다. 이렇게 분리를 하게 되면 MVC 패턴에 적용되어 코드 가독성과 유지보수성을 용이하게 가져갈 수 있다.

(2) Entity 구조가 변경되어도 괜찮다!

Entity 구조가 변경되어도 DTO를 사용해서 데이터를 처리하게 되면 변경사항이 클라이언트에게 직접적으로 영향을 미치지는 않는다. 따라서 DTO를 사용하게 되면 Entity의 변경으로 인한 영향을 최소화할 수 있고, 클라이언트에게 필요한 데이터만 전달할 수 있다.

 

Layered Architecture에서 DTO와 Entity

지금 내가 참여해 있는 맵필로그 프로젝트에서는 Layered Architecture를 사용하고 있다. Layered Architecture를 적용한 이유는 내가 개인적으로 생각하기에 관심사를 분리하기에도 좋은 것 같고, 다른 아키텍처에 비해 개발하는 데에 어려움이 덜한 것 같다고 생각한다.

 

Layered Architecture에서 Controller는 DTO로 변환된 Request Parameter를 가지고 Service Layer로 넘어가게 되고, Service에서는 DTO를 비즈니스 로직을 거쳐서 Entity로 변환된 객체를 Repository를 이용해서 DB에 저장하게 된다.

 

일단 요즘 느끼는 건 정답은 없다고 생각한다. 물론 이런 글들이 많이 올라오는 건 그만큼 검증이 된 것이기 때문이라고 생각은 하지만..

필자는 Repository 계층에도 DTO를 사용해도 될 것 같다고 생각하는 편이긴 한다.

 

그래서 나는 그냥 필요에 따라서 Service -> Repository에서 DTO를 사용해야겠다고도 생각한다.

 

위의 그림을 보면 DTO에서 Entity로 변환을 해야 하는 타이밍이 온다. Service Layer에서 Entity로 변환을 해주고 Repository로 넘겨서 DB 작업을 해주면 된다.

 

class UserEntity {
	// ...
    
    static from(email: string, password: string, name: string): UserEntity {
    	const user = new UserEntity();
        
        user.email = email;
        user.password = password;
        user.name = name;
        
        return user;
    }
}

class CreateUserDto {
	// ... (변수들은 private으로 선언)
    
	toEntity(): UserEntity {
    	return UserEntity.from(this.email, this.password, this.name)
    }
}

 

DTO 파일에서 변수들은 private으로 선언해 줘서 Service에서 DTO 필드로 접근할 수 없게 하면서 DTO에서 자기의 Property 들을 가지고 UserEntity를 만들어 주면 된다. 위와 같이 코드를 작성해 주면 각각의 객체가 책임과 역할을 가지게 되고 Service Layer에서 toEntity 메서드를 통해 쉽게 Entity로 변환할 수 있다.

 

참고로 위의 내용은 https://seungtaek-overflow.tistory.com/14에서 참고했다. 너무 좋은 내용이라고 생각되어서 한 번씩 읽어보시면 아주 좋을 것 같다!

 

참고자료

- https://seungtaek-overflow.tistory.com/14

 

[OOP] DTO, Entity와 객체지향적 사고

express, mongoose 두 스택을 사용할 때는 DTO와 Entity를 사용하는 법은커녕 개념조차 깊게 이해하고 있지 못했다. 그러다가 Nest.js, TypeORM 스택을 이용해서 개발을 하다보니 DTO와 Entity에 대해 알게 되고

seungtaek-overflow.tistory.com

- https://stella-ul.tistory.com/163

 

DTO의 사용범위는 어디까지? 또, DTO 변환은 어디서?

전에 Controller와 Service 계층 간 데이터 전달을 어떻게 해야 좋을지에 대한 포스팅을 작성했었다. 뭔가 숲보다 나무를 먼저 본? 느낌이긴한데... 하하; 그때 배우던 예제에선 Service가 Entity를 반환했

stella-ul.tistory.com