kakasoo

Response DTO를 이용한 응답 직렬화 (Serialization) 본문

프로그래밍/NestJS

Response DTO를 이용한 응답 직렬화 (Serialization)

카카수(kakasoo) 2022. 11. 27. 17:03
반응형

 

Response DTO를 이용한 응답 직렬화 (Serialization)

초기 기능 개발에는 서비스 로직으로 Response의 형태를 가공해서 제공해주곤 했다.

하지만 기능이 추가됨에 따라, 서비스는 유저 서비스와 판매자 서비스 등, 역할에 따라 모듈이 분리되기 시작했고,

모듈마다 동일한 형태의 데이터를 다루게 될 때마다, 서로 다른 모듈의 서비스를, 데이터 형태 가공 용으로 호출해도 되는지 의문이 됐다.

따라서, 서비스 로직 외에 응답 형태만을 가공하기 위한 레이어가 필요하다는 생각에 다다랐는데, 이 결론이 직렬화였다.

1. class-transformer와 글로벌 인터셉터

npm install reflect-metadata class-transformer

 

  • reflect-metadata는 코드에 메타 데이터를 넣기 위한 라이브러리다.
    • '코드에 메타 데이터를 넣는다'는 것은, 마치 type이란 이름의 property를 객체에 추가하는 것과 동일하다.
    • 우리는 데코레이터의 응답만을 보기 때문에 코드 실행에서 이 과정을 볼 수 없지만, NestJS에서는 빈번하게 사용된다.

 

app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));

 

  • main.ts에서 HTTP 요청에 대한 직렬화를 위해 interceptor를 추가한다.

 

2. ProductDto를 이용한 예제

2.1. @Exclude()와 @Expose()

export class ProductDto {
  @ApiProperty()
  @Expose()
  public readonly id: number;// 상품의 아이디@ApiProperty()
  @Expose()
  private readonly defaultPrice: number;// 상품의 기본 가격, 옵션이 많을 경우 우선적으로 보여줄 대표 가격@ApiProperty()
  @Expose()
  private readonly isSale: boolean;// 일시 품절 여부@ApiProperty()
  @Expose()
  public readonly isRegisted: boolean;// 상품 판매에 대한 공개 여부@ApiProperty()
  @Expose()
  private readonly productName: string;// 상품 이름@ApiProperty()
  @Expose()
  @Transform(({ value }) => setProductImageBucketName(value))
  private readonly productImage: string;// 상품 이미지@Exclude()
  private readonly originType: OriginType;

  @Exclude()
  private readonly feeType: FeeType;

  @Exclude()
  private readonly methodType: MethodType;
}

 

차례대로 정리해보자.

 

일단 ApiProperty()는 응답 객체 역시 스웨거 문서 상에서 표현이 가능하기 때문에 추가로 달아놓았다.각 칼럼들은 모두 private나 public으로 지정되어, 외부에서 접근 가능한지를 지정해주었다.우리가 DTO를 만들던 방식에서 크게 달라진 게 없다.다만 다른 점이 있다면, @Expose()와 @Exclude()이다.

  • Exclude()로 지정된 파라미터는 모두 숨김 상태가 된다.
  • Expose()로 지정된 파라미터는 응답 객체에서 직렬화되어 보여진다.

내부에서 사용하기 위한 값과 외부에 보여져도 상관없는 값을 두 데코레이터를 통해서 표현한다.

 

2.2. 생성자를 통한 인스턴스 생성

export class ProductDto {
/*
 * 위에 프로퍼티들이 작성되었다고 가정한다.
*/

  constructor(product: {
      id: number;
      defaultPrice: number;
      isSale: boolean;
      isRegisted: boolean;
      productName: string;
      originType: OriginType;
      feeType: FeeType;
      methodType: MethodType;
      productImage: string;
  }) {
      this.id = product.id;
      this.defaultPrice = product.defaultPrice;
      this.isSale = product.isSale;
      this.isRegisted = product.isRegisted;
      this.productName = product.productName;
      this.originType = product.originType;
      this.feeType = product.feeType;
      this.methodType = product.methodType;
      this.productImage = product.productImage;
  }
}

 

TypeScript에서 타입을 정했다고 한들 그게 실제 인스턴스로 변환되는 것은 아니다.JavaScript에서는 단순히 객체로 인식되기 때문에, 직렬화를 하고자 한다면 인스턴스 형태로 변환할 수 있게 생성자를 정의해주어야 한다.생성자는 파라미터를 받아서, 미리 지정한 프로퍼티들에 대입해주기만 하면 된다.이 과정은 Object.assign 을 이용해서 더 편하게 할 수 있지만, 나는 일일히 대입하는 것을 더 선호한다.

 

2.3. 추가적인 프로퍼티를 getter 메서드로 정의하기

export class ProductDto {
/*
 * 위에 프로퍼티들이 작성되었다고 가정한다.
 * 위에 생성자가 작성되었다고 가정한다.
*/

@ApiProperty({ type: ProductTag })
  @Expose()
  get allTags(): ProductTag {
    return {
      deliveryFree: this.feeType === FeeType.Free ? true : false,
      isOversea: this.originType === OriginType.Overseas ? true : false,
    };
	}
}

 

getter로 정의한 프로퍼티는 중첩된 객체 구조 nested object 로 작성된다.따라서 depth를 두어 정보를 나누고 싶다면, 이처럼 Expose()를 이용한 getter 메서드를 정의해주고, 원래의 프로퍼티를 Exclude 하면 된다.여기서는 배송비가 무료인지, 해외 배송인지 여부만을 보여주고 싶고, 클라이언트에 굳이 enum 값을 전달하고 싶지 않아 이처럼 표현해보았다.

 

2.4. getter 메서드의 타입 정의하기

import { ApiProperty } from '@nestjs/swagger';
import { setProductImageBucketName } from '@root/common/common.function';
import { FeeType, MethodType } from '@root/entities/delivery.entity';
import { OriginType } from '@root/entities/product-normal.entity';

export class ProductTag {
  @ApiProperty()
  deliveryFree: boolean;

  @ApiProperty()
  isOversea: boolean;
}

 

getter 메서드의 타입 역시 DTO 형태로 구현하면 스웨거에서 조회하기 편해진다.

 

3. 스웨거에 Response type 명시하기

@ApiOkResponse({ type: ProductDto })
@Get()
async getAllProducts(): Promise<ProductDto[]> {
  const products = await this.productService.getAll();
    // return products.map((el) => new ProductDto(el)); 서비스 단에서 DTO 형태로 생성하지 않았다면 반드시 이 로직이 필요하다.

  return products;
}

 

이렇게 하면 스웨거에서 요청 호출 없이도 클라이언트 개발자가 서버의 응답 객체를 조회할 수가 있다.

반응형