(NestJs) DTO 폴더 리팩토링

최건·2025년 8월 17일

기존 DTO

  • 기존 프로젝트에서는 각 도메인별로 하나의 DTO만 두며 사용하고 있었다.

문제점

단일 책임 원칙(SRP) 위반

하나의 파일에 Query, Path, Body, Response, 내부용 DTO까지 모두 몰아 넣다 보니 변경 사유가 여러 개가 된다. 그 결과 수정 범위가 불필요하게 커지고, 코드 리뷰도 비효율적으로 진행된다.

이름 충돌/네이밍 난맥상

CreateXDto, CreateXRequest, CreateXResponse가 한 파일에서 중복/혼용되며 import 시 오타·충돌 위험 증가.

변경 파급(캡슐화 붕괴)

특정 엔드포인트의 스펙만 바꿔도 같은 파일을 건드리면서 다른 DTO까지 린터/빌드/테스트에 영향 → 작은 변경도 큰 PR로 부풀어짐.

바뀐 DTO

리팩토링 결과 설명

1) Params는 DTO에서 분리 (파이프 사용)

  • 의도: 경로 변수는 대부분 스칼라 타입(숫자/문자열)이라 DTO까지 만들 필요가 없음.
  • 사용: Nest의 @Param() + 파이프(예: ParseIntPipe)로 즉시 변환·검증.
  • 효과: DTO 파일에서 불필요한 타입을 제거 → DTO는 “메시지 바디/쿼리/응답”에만 집중.
// controller
@Get('/:shopId')
getShop(@Param('shopId', ParseIntPipe) shopId: number) {
  return this.shopService.findOne(shopId);
}

2) dto/query/Query 전용 DTO

  • 의도: 페이지네이션, 정렬, 필터 등 URL 쿼리스트링을 명확히 분리.

  • 특징:

    • class-transformer로 숫자/불리언 변환 (@Type(() => Number), @Transform(...)).
    • class-validator로 유효성 보장 (@IsInt, @Min, @IsOptional 등).
    • Swagger 문서화(@ApiPropertyOptional)로 클라이언트와 스펙 공유.
  • 예: pagination.dto.ts, user.dto.ts(사용자 조회 조건 등)

// shop/dto/query/pagination.dto.ts
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsInt, IsOptional, Min } from 'class-validator';
import { Type } from 'class-transformer';

export class GetShopWithin1KmDTO {
  @ApiProperty({ description: '위도', example: 37.5665 })
  @IsNumber()
  @IsNotEmpty()
  @Type(() => Number)
  lat: number;

  @ApiProperty({ description: '경도', example: 127 })
  @IsNumber()
  @IsNotEmpty()
  @Type(() => Number)
  lng: number;

사용처:

async getShopWithin1Km(
  @Query() getShopWithin1KmDTO: GetShopWithin1KmDTO, 	   	   @GetUUID() uuid: string
) {...}

3) dto/requests/RequestBody 전용 DTO

  • 의도: 클라이언트가 서버로 보내는 데이터(Body) 만을 명확히 규정.

  • 특징:

    • 입력 검증 중심(class-validator 필수).
    • 서버가 관리하는 필드(id, createdAt 등)는 절대 포함하지 않음.
    • 엔드포인트별로 구분: user_requests.dto.ts, wishlist_requests.dto.ts, review_request.dto.ts 등.
  • 효과: 입력 스키마가 명확해지고 Under/Over-posting 위험 감소.

// shop/dto/requests/review_request.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsInt, IsNotEmpty, IsString, MaxLength, Min, Type } from 'class-transformer';

export class SubmitNewShopDto {
  @ApiProperty({ description: '소품샵 정보' })
  @IsNotEmpty()
  shop: SubmitShop;

  @ApiPropertyOptional({ description: '운영 시간 정보' })
  operatingHours?: OperatingHoursDto;

  @ApiPropertyOptional({ description: '판매 제품 리스트', type: [Products] })
  products?: Products[];
}

사용처:

  async submitNewShop(
    @Body() newShopData: SubmitNewShopDto, 
    @GetUUID() uuid: string
  ) {...}

4) dto/responses/Response 전용 DTO(ViewModel)

  • 의도: 서버가 클라이언트로 반환하는 형태를 명확히 분리(엔티티와 분리).

  • 특징:

    • 문서화 중심(@ApiProperty 적극 사용).
    • 가공/집계 필드, 관계형 데이터의 표현용 구조를 포함 가능.
    • 검증 데코레이터는 보통 생략하고, 필요 시 class-transformer@Expose/@Transform으로 직렬화 제어.
    • 페이지네이션 래퍼(pagination_response.dto.ts) 등 공용 뷰모델 제공.
  • 효과: 엔티티 스키마 변경이 외부 계약을 곧바로 흔들지 않음. 응답 일관성↑.

// shop/dto/responses/shop_responses.dto.ts
import { ApiProperty } from '@nestjs/swagger';

export class ShopWithin1KmResponseItemDTO {
  @ApiProperty({ example: 1 })
  id: number;

  @ApiProperty({ example: 'Green Valley Market' })
  name: string;

  ...
}

서비스 → 응답 매핑은 어셈블러/프레젠터에서:

  return new ShopSearchPageNationResultDTO(mappedResults, pageInfoDTO);

5) 이 구조의 핵심 장점

  • 계약 명확화: 입력(Query/Body)과 출력(Response)을 분리해 API 계약이 선명.
  • SRP 준수: 목적별 DTO로 파일·리뷰 범위 축소.
  • 테스트 용이: 각 DTO를 독립적으로 검증 테스트 가능.
  • 문서 품질↑: Swagger 스키마가 역할별로 정돈.
  • 버저닝 용이: v1/v2 응답 DTO를 폴더로 공존시키기 쉬움.
profile
개발이 즐거운 백엔드 개발자

0개의 댓글