(14)

[NestJS] ResponseEntity와 정적 팩토리 메서드

필자는 Nest를 개발할 때 Service와 Repository를 거쳐온 결괏값을 Client로 반환할 때 Response DTO 객체로 만들어서 Controller 계층으로 넘기고 Controller에서 클라이언트로 결괏값을 반환할 때 ResponseEntity 객체에 넣어서 반환한다. ResponseEntity를 사용한 이유는 무엇인가요? ResponseEntity는 뭐 특별한 라이브러리나 그런것이 아니다. 원래 초기에는 개발할 때 그냥 return { age: 10, name: '' } 이런 식으로 했었는데 가시성 측면으로도 좋지 않아 보였고, 추후 유지보수적으로도 좋지 않다고 생각했다. Response DTO를 도입한 것도 그 이유다. 그래서 뭔가 객체를 하나 만들어서 반환해준다면 코드적으로 더욱..

[NestJS] Nestia swagger 초기 설정에서 발생하는 문제

지금 NestJS 커뮤니티에 들어와 있는데 samchon 님이 개발하신(?) Nestia 라이브러리를 다들 많이 사용하시는 것 같아서 나도 프로젝트 하면서 부담가지지 않으면서 생각날때마다 조금씩 해보려고 한다. 개인적으로 문서를 보는 능력이 많이 부족하다고 생각되어서 문서를 보면서 직접 공부겸 프로젝트를 해보는 경험도 해보려고 한다. Nestia란? Nestia는 NestJS를 위한 helper library 라고 할 수 있다. Nestia를 사용하게 되면 다음과 같은 도움을 받을 수 있다고 한다. - class-validator에서 런타임 유효성 검사기는 기존보다 20,000배 빠르다. - class-transformer에서 JSON Serialization은 기존보다 200배 빠르다. - Swagge..

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

지금 하는 맵필로그 프로젝트 이전에는 모두의 여행, 트리퍼라는 프로젝트를 했는데, 그 프로젝트는 express와 javascript로 개발을 했었다. 단순히 express로 개발을 할 때는 DTO와 Entity 그리고 OOP에 대해서는 진짜 아예 사용하지도 않았고 무지했던 것 같다. 하지만, Nest를 시작해 보면서 자연스럽게 DTO와 Entity를 알아야 했고, 프레임워크 특성상 OOP의 개념도 가지고 있어야 개발에 차질이 없을 것 같아서 이렇게 공부를 시작하게 되었다. 그래서 이번 포스팅에서는 DTO와 Entity 개념이 간단하게 무엇인지, 그리고 Nest에서 그 둘의 상관관계에 대해서 알아보도록 하자. Entity Entity는 실제 DB 테이블과 매핑되는 핵심 클래스이고, 데이터베이스 테이블에 있..

[NestJS] Repository Pattern에서 find(findOne)을 사용할 때 주의해야 할 사항! (where condition with null value)

async findMarkOnSpecificId( mark: MarkDto, ): Promise { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const markCategoryName = await this.markCategoryRepository.findOne({ select: { title: true, }, where: { id: mark.getMarkCategoryId, status: StatusColumnEnum.ACTIVE, }, }); // metadata 부분 조회하기 const markMetadatas ..

[NestJS] ResponseEntity와 정적 팩토리 메서드

Framework/NestJS 2023. 11. 14. 19:28

필자는 Nest를 개발할 때 Service와 Repository를 거쳐온 결괏값을 Client로 반환할 때 Response DTO 객체로 만들어서 Controller 계층으로 넘기고 Controller에서 클라이언트로 결괏값을 반환할 때 ResponseEntity 객체에 넣어서 반환한다.

 

ResponseEntity를 사용한 이유는 무엇인가요?

ResponseEntity는 뭐 특별한 라이브러리나 그런것이 아니다. 원래 초기에는 개발할 때 그냥 return { age: 10, name: '' } 이런 식으로 했었는데 가시성 측면으로도 좋지 않아 보였고, 추후 유지보수적으로도 좋지 않다고 생각했다. Response DTO를 도입한 것도 그 이유다.

 

그래서 뭔가 객체를 하나 만들어서 반환해준다면 코드적으로 더욱 깔끔할 것 같다고 판단되었다. 그래서 블로그 글들을 많이 찾아봤는데 ResponseEntity를 직접 커스텀해서 만드신 개발자 분이 있었다.

 

하지만 필자가 의도한 이유로 ResponseEntity를 사용한 것이 아니었다. 블로그 개발자님은 원래는 성공 Response를 interceptor를 활용해서 코드를 구성했었는데 Controller 계층에서 전달하고 있는 데이터 타입과 API의 응답 타입이 맞지 않는 문제로 인해 도입하셨다고 했다.

 

그래서 필자는 interceptor를 활용할 생각은 못했지만, 위와 같은 이유로 인해 ResponseEntity를 커스텀했다.

 

ResponseEntity Code

import { Exclude, Expose } from 'class-transformer';

export class ResponseEntity<T> {
  @Exclude() private readonly _isSuccess: boolean;
  @Exclude() private readonly _statusCode: number;
  @Exclude() private readonly _result: T;

  private constructor(
    isSuccess: boolean,
    statusCode: number,
    result?: T | undefined,
  ) {
    this._isSuccess = isSuccess;
    this._statusCode = statusCode;
    this._result = result;
  }

  @Expose()
  get isSuccess() {
    return this._isSuccess;
  }

  @Expose()
  get statusCode() {
    return this._statusCode;
  }

  @Expose()
  get result() {
    return this._result;
  }

  static OK(statusCode: number): ResponseEntity<undefined> {
    return new ResponseEntity(true, statusCode);
  }

  static OK_WITH<T>(statusCode: number, result: T): ResponseEntity<T> {
    return new ResponseEntity<T>(true, statusCode, result);
  }
}

 

필자는 평소에 API Response에서는 공통적으로 isSuccess, statusCode 그리고 result를 반환한다. 그리고 실패 Response인 경우에는 여기에 timestamp, url, error message 등을 추가한다.

 

위의 코드는 성공 Response 객체를 생성할 수 있는 코드만 작성했다. Error Response는 ExceptionFilter가 마무리되면 비슷한 구조로 추가할 예정이다.

 

그리고 글을 작성하면서 생각났지만 위에서 statusCode parameter는 없어도 될 것 같다. 최근에 클린코드 책을 읽고있는데 메서드의 파라미터 개수는 없으면 너무 좋고 1개까지가 좋다고 써져 있어서 최대한 지켜보려고 노력하고 있기 때문에 statusCode 매개변수는 없어도? 충분히 구현이 가능할 것 같다.

 

그래서 위 ResponseEntity를 가지고 Controller에서는 다음과 같이 코드를 작성한다.

 

  @Post()
  @HttpCode(HttpStatus.CREATED)
  async postMarkCategory(
    @UserId() userId: number,
    @Body() body: PostMarkCategoryRequestDto,
  ): Promise<ResponseEntity<PostMarkCategoryResponseDto>> {
    const result = await this.markCategoryService.createMarkCategory(
      userId,
      body.title,
    );
    return ResponseEntity.OK_WITH(HttpStatus.CREATED, result);
  }

 

위와 같이 작성하면 Service에서 어떤 Type의 결과값을 반환했는지도 확인할 수 있고 깔끔하게 코드를 작성할 수 있다.

여기서 눈여겨 볼 수 있는 점은 new 키워드를 사용하지 않았다는 것이다. 이유가 무엇인지 보자!

 

정적 팩토리 메서드

필자는 생각없이 다른 개발자님의 블로그를 따라한 것 같아서 지금이라도 이유를 한번 곰곰이 생각해보려고 한다. 나처럼 하지 마시길 ㅠ

 

정적 팩토리 메서드는 객체 생성의 역할을 하는 클래스 메서드라고 할 수 있다. 그러면 여기서 질문을 던질 수 있는 것이 생성자를 써도 충분히 구현이 가능할텐데 왜 생성자를 private으로 선언하면서 정적 팩토리 메서드를 사용했느냐이다.

 

(1) 이름을 가질 수 있다.

이름을 가질 수 있다는 점이 가장 큰 장점이다. 우리는 new 라는 키워드를 알고 있다. new를 통해 객체를 생성하려고 한다면 Class의 내부 구조를 알고 있어야 객체를 올바르게 생성할 수 있다. 하지만 정적 팩토리 메서드를 사용하게 되면 메서드 이름에 객체의 생성 목적을 부여할 수 있다.

 

위의 코드를 보면 나는 OK와 OK_WITH 이라고 네이밍을 정했다. 물론 여기에 ERROR라는 키워드를 추가할 수 있을 것 같다. 이렇게 정적 팩토리 메서드를 적용하면서 Controller 계층이나 외의 다른 계층에서 성공 응답인지 실패 응답인지 쉽게 구분할 수 있다.

 

만약에 생성자를 통해 객체를 생성했다면 일일이 Depth를 들어가면서 확인을 했어야 했기 때문에 개발적인 측면에서 불편했을 수 있을 것 같다.

 

(2) 객체 생성을 캡슐화할 수 있다.

정적 팩터리 메서드는 객체 생성을 캡슐화하는 방법일 수 있다. 아래 예시 코드를 보자.

 

export class CarDto {
    private readonly name: string;
    private readonly position: number;
    
    constructor(name: string, position: number) {
    	this.name = name;
        this.position = position;
    }
    
    static from(Car car): CarDto {
    	return new CarDto(car.getName(), car.getPosition());
    }
}

// 정적 팩토리 메서드를 사용하는 경우에는?
const carDto = CarDto.from(car);

// 생성자를 사용하는 경우에는?
const carDto = new CarDto(car.getName(), car.getPosition());

 

코드는 이런 느낌이다~라는 정도만 봐주시면 좋을 것 같다. 정적 팩토리 메서드를 사용하게 되면 내부 구조를 모르더라도 쉽게 Entity에서 DTO로 변환할 수 있다. 하지만 생성자를 사용하게 된다면 내부 구조를 모두 드러내게 된다.

 

따라서 가독성도 좋고 객체지향적으로도 코드를 작성할 수 있다. 하지만 팩토리 메서드만 존재하는 클래스를 생성할 경우 상속이 불가능하다는 점은 참고해야 할 것 같다!

 

급 후기..

앗.. 정적 팩토리 메서드에서 from과 of가 있는데 from은 하나의 parameter, of는 여러 개의 parameter를 받아올 때 키워드를 사용하는데 난 반대로.. 했었네? ㅋㅋㅋㅋㅋㅋ ㅠㅠㅠ


참고 자료

- https://overcome-the-limits.tistory.com/714

 

[Project] 프로젝트 삽질기33 (feat 정적 팩토리 메서드)

들어가며 사이드 프로젝트에서 푸시 알림을 활용한 서비스를 개발하고 있습니다. 개발하는 과정에서, 컨트롤러에서 응답 객체를 클라이언트에게 전달할 때, 어떻게 데이터를 전달하면 좋을까

overcome-the-limits.tistory.com

 

iamkanguk

iamkanguk

안녕하세요! 현재 NodeJS로 백엔드 취업을 희망하고 있는 이강욱이라고 합니다. 최대한 블로그 포스팅을 보시는 분들에게 자세한 정보와 즐거움을 드리기 위해 노력하며 포스팅을 쓰고 있습니다 ㅎㅎ 읽어보시고 문제가 있거나 잘못되었다고 생각되는 부분들은 언제든 댓글로 작성해주시면 최대한 빠르게 조치하겠습니다! 감사합니다.

[NestJS] Nestia swagger 초기 설정에서 발생하는 문제

Framework/NestJS 2023. 11. 13. 20:50

지금 NestJS 커뮤니티에 들어와 있는데 samchon 님이 개발하신(?) Nestia 라이브러리를 다들 많이 사용하시는 것 같아서 나도 프로젝트 하면서 부담가지지 않으면서 생각날때마다 조금씩 해보려고 한다.

 

개인적으로 문서를 보는 능력이 많이 부족하다고 생각되어서 문서를 보면서 직접 공부겸 프로젝트를 해보는 경험도 해보려고 한다.

 

Nestia란?

Nestia는 NestJS를 위한 helper library 라고 할 수 있다. Nestia를 사용하게 되면 다음과 같은 도움을 받을 수 있다고 한다.

 

- class-validator에서 런타임 유효성 검사기는 기존보다 20,000배 빠르다.

- class-transformer에서 JSON Serialization은 기존보다 200배 빠르다.

- Swagger 생성 유연함 및 자동 E2E 테스트 기능 생성기

- 클라이언트를 위한 SDK를 생성함으로서 개발에 도움을 줄 수 있다.

 

나중에 어느정도 숙련이 되면 Nestia 초기 세팅할 때는 어떤 명령어를 작성해야 하고, 각 Nestia 라이브러리에는 어떤 것들이 있는지를 좀 더 세세하게 분석해보려고 한다.

 

문제점은?

우리는 root에 nestia.config.ts 파일을 만들고 npx nestia init 명령어를 통해 초기 세팅을 해줄 수 있다.

 

import { INestiaConfig } from '@nestia/sdk';

export const NESTIA_CONFIG: INestiaConfig = {
  /**
   * Accessor of controller classes.
   *
   * You can specify it within two ways.
   *
   *   - Asynchronous function returning `INestApplication` instance
   *   - Specify the path or directory of controller class files
   */
  input: ['src/controllers'],

  /**
   * Building `swagger.json` is also possible.
   *
   * If not specified, you can't build the `swagger.json`.
   */
  swagger: {
    /**
     * Output path of the `swagger.json`.
     *
     * If you've configured only directory, the file name would be the `swagger.json`.
     * Otherwise you've configured the full path with file name and extension, the
     * `swagger.json` file would be renamed to it.
     */
    output: 'dist/swagger.json',
  },

  /**
   * Output directory that SDK would be placed in.
   *
   * If not configured, you can't build the SDK library.
   */
  output: 'src/api',

  /**
   * Target directory that SDK distribution files would be placed in.
   *
   * If you configure this property and runs `npx nestia sdk` command,
   * distribution environments for the SDK library would be generated.
   *
   * After the SDK library generation, move to the `distribute` directory,
   * and runs `npm publish` command, then you can share SDK library with
   * other client (frontend) developers.
   */
  // distribute: "packages/api",

  /**
   * Whether to use propagation mode or not.
   *
   * If being configured, interaction functions of the SDK library would
   * perform the propagation mode. The propagation mode means that never
   * throwing exception even when status code is not 200 (or 201), but just
   * returning the {@link IPropagation} typed instance, which can specify its body
   * type through discriminated union determined by status code.
   *
   * @default false
   */
  // propagate: true,

  /**
   * Allow simulation mode.
   *
   * If you configure this property to be `true`, the SDK library would be contain
   * simulation mode. In the simulation mode, the SDK library would not communicate
   * with the real backend server, but just returns random mock-up data
   * with requestion data validation.
   *
   * For reference, random mock-up data would be generated by `typia.random<T>()`
   * function.
   *
   * @default false
   */
  // simulate: true,
};

export default NESTIA_CONFIG;

 

그러면 다음과 같이 코드가 작성되어 있을 것이다. 여기서 swagger 쪽을 보면 swagger.json이 생성될 위치를 언급해주는 것인데 필자는 dist 파일의 root에 생성되게끔 설정해주었다. 

 

여기서 문제는 생성은 잘되지만 npm run start:dev 명령어를 통해 서버를 구동시키고 나면 dist에 swagger.json이 없어진다는 것이다.

물론 새로 디렉토리를 생성해서 그쪽으로 경로 설정을 해주면 되긴하지만 왜 안되는지 궁금해서 알아보기로 했다.

 

결론

dist는 기본적으로 빌드하면 삭제옵션이 있다고 한다. nest-cli.json 파일을 확인해보면 compilerOptions - deleteOurDir이 true로 되어있을 것이다. 

 

그래서 간단하게 로직을 정리해보자면..

 

- 우리는 npx nestia swagger를 통해 swagger.json을 생성한다. 그러면 dist의 root에 swagger.json이 생성된 것을 확인할 수 있다.

- 만약 이후에 npm run start:dev를 통해 서버를 구동시킬 경우 deleteOurDir가 true이면 dist 파일이 삭제되었다가 새로 생성이 되기 때문에 swagger.json을 읽어오지 못하는 것이다.

- 따라서 deleteOurDir 옵션을 false로 설정해주면 읽어올 수 있다.

 

참고자료

- https://nestia.io/docs/

 

Nestia Guide Documents - Index

NestJS Helper Libraries

nestia.io

- https://docs.nestjs.com/cli/monorepo

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

iamkanguk

iamkanguk

안녕하세요! 현재 NodeJS로 백엔드 취업을 희망하고 있는 이강욱이라고 합니다. 최대한 블로그 포스팅을 보시는 분들에게 자세한 정보와 즐거움을 드리기 위해 노력하며 포스팅을 쓰고 있습니다 ㅎㅎ 읽어보시고 문제가 있거나 잘못되었다고 생각되는 부분들은 언제든 댓글로 작성해주시면 최대한 빠르게 조치하겠습니다! 감사합니다.

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

Framework/NestJS 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

 

iamkanguk

iamkanguk

안녕하세요! 현재 NodeJS로 백엔드 취업을 희망하고 있는 이강욱이라고 합니다. 최대한 블로그 포스팅을 보시는 분들에게 자세한 정보와 즐거움을 드리기 위해 노력하며 포스팅을 쓰고 있습니다 ㅎㅎ 읽어보시고 문제가 있거나 잘못되었다고 생각되는 부분들은 언제든 댓글로 작성해주시면 최대한 빠르게 조치하겠습니다! 감사합니다.

[NestJS] Repository Pattern에서 find(findOne)을 사용할 때 주의해야 할 사항! (where condition with null value)

Framework/NestJS 2023. 11. 10. 04:37
async findMarkOnSpecificId(
    mark: MarkDto,
  ): Promise<GetMarkDetailByIdResponseDto> {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      const markCategoryName = await this.markCategoryRepository.findOne({
        select: {
          title: true,
        },
        where: {
          id: mark.getMarkCategoryId,
          status: StatusColumnEnum.ACTIVE,
        },
      });

      // metadata 부분 조회하기
      const markMetadatas =
        await this.markMetadataRepository.selectMarkMetadatasByMarkId(
          mark.getId,
        );

      await queryRunner.commitTransaction();
      return GetMarkDetailByIdResponseDto.from(markMetadatas);
    } catch (err) {
      this.logger.error(`[findMarkOnSpecificId - transaction error] ${err}`);
      await queryRunner.rollbackTransaction();
      throw err;
    } finally {
      await queryRunner.release();
    }
  }

 

위의 코드는 Query Parameter로 들어온 Mark의 ID를 가지고 Mark를 조회하는 로직이 있는 메서드이다. 위의 코드에서 findOne을 주의 깊게 보자!

 

일단 Mark는 MarkDto라는 타입을 가지고 있다. Mark는 카테고리를 가질 수도 있고 가지지 않을 수도 있기 때문에 nullable한 값이다.

이후 mark 변수에서 getter를 통해 markCategoryId를 가져와서 where 조건에 넣어줬는데...!

 

처음에는 markCategoryName 변수가 null로 할당될 줄 알았다. 하지만 보니까 값이 출력이 되는 것이다. 확인해보니까 find 메서드를 사용하니까 그냥 where true로 되어서 모든 카테고리 값들이 나오는 것이었다. 그래서 findOne은 출력되는 값들 중에 제일 위의 값이 출력이 된 것이었다.

 

우리는 이런 상황을 방지해야 한다. 위의 문제를 파악하지 못하고 그대로 코드를 작성했더라면 문제가 분명히 발생했을 것이다.

그래서 위의 상황을 어떻게 해결할 수 있느냐?

 

TypeORM의 Equal 메서드를 사용하면 된다. id: mark.getMarkCategoryId 부분에서 Equal 메서드를 적용해주면 Filter가 정확히 적용이 되어서 정확하게 원하는 데이터를 얻을 수 있다. 물론 다른 방법도 있는 것 같으니 참고하는 것도 좋을 듯!

 

참고 자료

- https://orkhan.gitbook.io/typeorm/docs/find-options

iamkanguk

iamkanguk

안녕하세요! 현재 NodeJS로 백엔드 취업을 희망하고 있는 이강욱이라고 합니다. 최대한 블로그 포스팅을 보시는 분들에게 자세한 정보와 즐거움을 드리기 위해 노력하며 포스팅을 쓰고 있습니다 ㅎㅎ 읽어보시고 문제가 있거나 잘못되었다고 생각되는 부분들은 언제든 댓글로 작성해주시면 최대한 빠르게 조치하겠습니다! 감사합니다.

방명록