Woozy_DevLog
Published 2023. 3. 21. 21:42
Carryduo DTO Project/Carryduo

DTO 리팩토링의 시작 배경

dto를 객체의 타입용도로만 사용하였다. 이후 dto에 대해 알게 되었고 배운 내용을 토대로 현재 프로젝트에 적용해보기로 하였다.

  • dto는 각 계층 간 데이터 통신의 규격이 되는 객체이다.
  • controller → service → repository의 계층에서 데이터 통신을 하려면 각 계층의 dto로 변환해주어야 한다.
  • entity는 데이터베이스와 매핑되는 객체.
  • DB에 접근하는 repository 계층은 entity 객체만 이용되는 것이 바람직하다.

Service 계층

  • 프로젝트는 repository 패턴을 적용하고 있다.
  • repository에서 entity 객체를 전달 받아서 service에서 사용할 service 계층의 dto로 변환 후 데이터 연산을 통해 controller에 전달 하는 방식으로 리팩토링을 하였다.

객체의 필드값과 dto의 필드값이 다른 상황에서 dto 객체 변환

개요

  • 특정 챔피언의 승, 벤, 픽 비율을 가져올때 데이터 분석 서버를 통하여 해당 챔피언의 정보를 수집한 뒤 필요한 데이터들을 데이터베이스에 저장하고, 해당 데이터들을 가져와 연산을 통해 값을 산출한다.

문제점

현재 프로젝트에서는 최신 게임 버전의 데이터를 보여주기에 게임 패치가 된지 얼만 안된 시간엔 비주류 챔피언들의 데이터가 존재 하지 않기도 한다. 그래서 repository에서는 빈 배열을return 하기도 한다. 빈배열일때 객체의 필드가 존재하지 않아서 타입 객체(GetChampRate)로 변환자체가 불가하였다.

해결

export class GetChampRate {
  readonly winRate: string | number = 0;
  readonly pickRate: string | number = 0;
  readonly spell1: number = 0;
  readonly spell2: number = 0;
  readonly version: string | number = 0;
  constructor(partial: Partial<GetChampRate>) {
    if (partial) Object.assign(this, partial);
  }
}

//분기점을 나눠주고
if (champRate.length === 0) {
      rate = new GetChampRate(null);
    } else {
      rate = new GetChampRate(champRate[0]);
    }
데이터가 없을 경우 기본 값이 설정된 dto로, 있을 경우 해당 값을 각 필드에 복사하여 타입 객체를 생성하였다.

데이터가 없을 경우 기본 값이 설정된 dto로, 있을 경우 해당 값을 각 필드에 복사하여 타입 객체를 생성하였다.

문제점

응답해야하는 dto객체의 필드와 DataBase에서 가져온 타입 객체의 필드가 서로 달라 변환하는데 어려움을 겪었다.

해결

특정 필드를 제외한 나머지 필드와 특정필드의 값을 이용해 새로운 필드를 생성하여 각 필드를 합쳐 dto 객체로 생성하였다.

let spell1Img: string;
let spell2Img: string;
//spell1, spell2 필드를 이용해서 새로운 spell1Img, spell2Img 필드 생성
const { spell1, spell2, version, ...withoutSpell } = rate;
//기본 값일 경우
    if (spell1 === 0 && spell2 === 0) {
      spell1Img = `${process.env.S3_ORIGIN_URL}/default/default_0.png`;
      spell2Img = `${process.env.S3_ORIGIN_URL}/default/default_0.png`;
    } 
//데이터가 존재할 경우
		else {
      spell1Img = `${process.env.S3_ORIGIN_URL}/spell/${spellInfo[spell1]}.png`;
      spell2Img = `${process.env.S3_ORIGIN_URL}/spell/${spellInfo[spell2]}.png`;
    }
//각 필드를 합쳐 ChampRateDataDto 객체로 생성
return plainToInstance(ChampRateDataDto, {
      ...withoutSpell,
      spell1Img,
      spell2Img,
    });

Dto 객체를 Entity 객체로 변환

  • 새로운 사용자를 추가 할때 dto객체를 entity 객체로 생성 후 repository에 전달하게 구현하였다.
  • service 계층에서 repository 계층으로 데이터를 이동할때 entity 객체로 변환하여 전달하기 위해 dto 내 entity 객체 생성 메서드를 추가하여 코드 재사용성과 유지보수성을 높였다.
//dto
export class CreateSummonerDto extends OmitType(SummonerCommonDTO, [
  'id',
  'createdAt',
  'deletedAt',
  'updatedAt',
]) {
  toEntity() {
    const summoner = new SummonerEntity();
    summoner.summonerName = this.summonerName;
    summoner.summonerId = this.summonerId;
    summoner.summonerPuuId = this.summonerPuuId;
    summoner.summonerIcon = this.summonerIcon;
    summoner.summonerLevel = String(this.summonerLevel);
    summoner.tier = this.tier;
    summoner.tierImg = this.tierImg;
    summoner.lp = this.lp;
    summoner.win = this.win;
    summoner.lose = this.lose;
    summoner.winRate = this.winRate;
    summoner.mostChamp1 = this.mostChamp1;
    summoner.mostChamp2 = this.mostChamp2;
    summoner.mostChamp3 = this.mostChamp3;
    return summoner;
  }
}

//service
async summonerEntity(
    summonerDataDto: SummonerDataDto,
    soloRankDataDto: SoloRankDataDto,
    mostChampDataDto: MostChampDataDto,
  ): Promise<SummonerEntity> {
    const summonerResult = plainToInstance(CreateSummonerDto, {
      ...summonerDataDto,
      ...soloRankDataDto,
      ...mostChampDataDto,
    });
    return summonerResult.toEntity();
  }

Dto 상속을 통한 재사용성 증가

  • repository에서 전달받은 객체를 dto객체로 변환 후 데이터 연산을 한다.
  • 그 후 controller로 dto 객체를 전달할때 일일이 response 규격에 맞는 dto 객체를 만들기 보다는 @nestjs/swagger에서 제공하는 IntersectionType를 사용하여 기존 dto들을 결합하여 사용하였다.
  • plainToInstance를 사용해서 responseDto 객체로 변환 후 controller에 전달하였다.
import { ApiProperty, IntersectionType } from '@nestjs/swagger';
import { ChampRateDataDto } from '../champ-rate/champ.rate.dto';
import { ChampSkillCommonDTO } from '../champ-skill/champ.skill.common.dto';
import { ChampCommonDTO } from '../champ/champ.common.dto';
//dto
export class TargetChampionResDto extends IntersectionType(ChampCommonDTO, ChampRateDataDto) {
  @ApiProperty({
    description: '챔피언 스킬 정보',
    isArray: true,
    type: ChampSkillCommonDTO,
  })
  skill: ChampSkillCommonDTO[];
}

//service
const champData = await this.champRepository.getChampDefaultData(param.champId);
const champRate = await this.transfer.champRate(champRateInfo, banInfo?.banRate, champPosition);
const skill = await this.transfer.champSkill(skillInfo);

return plainToInstance(TargetChampionResDto, {
      ...champData,
      ...champRate,
      skill,
    });

Transform 을 이용한 필드값 변경

  • dto 객체로 데이터를 생성할때 필드 값의 타입 또는 필드 값의 변경이 필요한 상황이 있었다. ex) database 쿼리로 연산된 값이 string값으로 전달된 경우
  • class-transformer의 Transfom를 이용해 간단하게 변경이 가능하였다.
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';

export class RecentChampDto {
	...
  @Transform(({ value }) => Number(value))
  @ApiProperty({
    example: '5',
    description: '최근 플레이 챔피언 승 수',
  })
  readonly recentChampWin: number;

  @Transform(({ value }) => Number(value))
  @ApiProperty({
    example: '5',
    description: '최근 플레이 챔피언 패 수',
  })
  readonly recentChampLose: number;

  @Transform(({ value }) => Number(Number(value).toFixed(2)))
  @ApiProperty({
    example: '50',
    description: '최근 플레이 챔피언 승률',
  })
  readonly recentChampRate: number;

  @Transform(({ value }) => Number(value))
  @ApiProperty({
    example: '10',
    description: '챔피언으로 플레이한 게임 수',
  })
  readonly recentChampTotal: number;
}

//service
return recentChampInfoList.map((v) => plainToInstance(RecentChampDto, v));

클라이언트에서 parameter 검증하기

  • 특정 챔피언의 포지션별 데이터를 받기 위한 api가 있다.
  • 해당 포지션에는 해당 챔피언의 모스트 포지션에 대한 데이터를 받는 default 와 받을 포지션 파라미터 top,jungle,mid,ad,support 값이 전달된다.
  • 해당 파라미터로 DB에서 포지션별 데이터를 조회하므로 이 외에 파라미터값에 대한 예외처리가 필요했다.
@Get('/:champId/position/:position')
  async getTargetChampion(@Param() param: TargetChampionReqDTO) {
    return await this.champService.getTargetChampion(param);
  }

해결

class-validator에서 제공하는 IsIn을 활용하여 배열 안 값 외에 대한 예외처리를 하였다.

import { IsIn, IsNotEmpty, IsString } from 'class-validator';

export class TargetChampionReqDTO {
  @IsString()
  @IsNotEmpty()
  champId: string;
  @IsString()
  @IsNotEmpty()
  @IsIn(['default', 'top', 'jungle', 'mid', 'ad', 'support'])
  position: string;
}
profile

Woozy_DevLog

@Woozy_Dev

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!