본문 바로가기

개발/typescript

[NestJs] 직관적으로 VO 생성하기

반응형

안녕하세요 도깨비 개발자입니다.

VO(Value Obejct)는 식별자가 없어 값만으로 비교가 가능합니다.

Entity는 추적 비용이 발생해 VO도 적절히 사용해줘야하는데요.

NestJs에서 조금 더 효과적으로 VO를 사용하는 법을 전달드릴까 합니다.


 

DTO는 데이터 전달 객체입니다.

행위보단 상태를 전달받을때 주요 사용됩니다.

DTO를 Service에도 사용할수 있지만, Controller에서 End Point별로 생성하고 사용되는 점을 고려하면 사용하기 주저됩니다.

그렇다고 Entity를 사용하기 애매할땐 VO를 사용하면 됩니다.

 

 

물건을 구매하는 상황을 가정해보겠습니다.

장바구니에 상품 정보, 가격 정보, 구매 수량등을 구성한 BasketItem 개념을 고안합니다.

class BasketItem {
  constructor(
    public readonly productId: string,
    public readonly price: number,
    public readonly discountPrice: number,
    public readonly quantity: number,
  ) {
    if (quantity <= 0) {
      throw new Error('set purchase quantity');
    }
  }
}

상품 정보와 수량만으로는 객체화하기에 상태가 너무 부족하고, VO를 생성하는 별도 로직이 필요해 요청 데이터에 가격 정보를 추가했습니다.

 

기본적으로 VO를 map으로 생성할수 있습니다.

export class Service {
  constructor(
      ....
  ) {}

  async purchase(dto: PurchaseReqDto): Promise<ResDto> {
    const basketItem = dto.basket.map(v => new BasketItem(....))

    ....
  }
}

 

책임에만 집중하지 못해 깔끔하지 못합니다.

그래서 NestJs에서 자랑하는 pipe를 이용해 DTO Validation후 VO를 생성시킬수 있습니다.

 

## pipe ##
...

export class TransformBasketPipe implements PipeTransform {
  transform(value: PurchaseReqDto, metadata: ArgumentMetadata) {
    value.basket = value.basket.map(
      (item) =>
        new BasketItem(...),
    );
    return value;
  }
}

 

Controller와 Service는 아래처럼 사용할수 있습니다.

## Controller ##
  @Controller('')
  @Post('/')
  public async purchase(
    @Body(TransformBasketPipe) body: PurchaseReqDto,
  ): Promise<PurchaseRes> {
  	service.purchase(userId, body.basekt)
  }
  
  
## Service
export class Service {
  constructor(...) {}

  async purcahse(userId: string, basket: BasketItem[]): Promise<PurchaseResult> {
	.... 도메인 로직
  }
}

 

 

확실히 도메인 로직에만 집중할수 있어 깔끔합니다.

좋네요!

 

이상입니다.