개요

API DTO를 다룰 때 class-transformer의 @Expose, @Exclude, @Type와 plainToInstance, excludeExtraneousValues 옵션을 정확히 이해해야 데이터 노출 제어와 변환 일관성을 확보할 수 있음 아래는 개념 정의와 동작 방향, 자주 생기는 오해 정리 및 최소 예시


@Expose와 @Exclude

핵심 개념

  • @Expose: 변환 대상으로 명시적 노출 표시
  • @Exclude: 변환에서 제외 표시
  • 기본 전략은 include-all에 가까움. 즉 아무 옵션 없이 변환하면 대부분의 필드가 그대로 따라옴. 진짜 필드 필터링을 원하면 @Exclude 사용 또는 excludeExtraneousValues 옵션과 함께 @Expose 사용 필요

동작 원리

  • plain → instance 변환 시
    • 기본값으로는 평문에 있는 키가 인스턴스에 거의 그대로 주입됨
    • 특정 필드를 제거하려면 해당 필드에 @Exclude 적용 또는 옵션 excludeExtraneousValues: true와 @Expose 병행 사용 필요
  • instance → plain 변환 시
    • @Exclude가 있으면 직렬화 결과에서 빠짐
    • @Expose({ name })로 키 이름 매핑 가능. 이때 이름 변경은 instance → plain 방향에서 보이는 것임

주의

  • @Expose만 달아놓고 옵션 없이 plainToInstance를 호출해도 비노출 필드가 자동으로 사라지지 않음
  • 명시적 제외가 필요하면 @Exclude 또는 excludeExtraneousValues와 @Expose 조합 사용 필요

최소 스니펫

class UserDto {
  @Expose() name: string
  @Exclude() password: string
}

키 이름 매핑: @Expose({ name })의 방향성

정의

  • @Expose({ name: ‘full_name’ })는 양방향 매핑 설정
    • plain → instance: plain의 full_name → 클래스의 name으로 주입
    • instance → plain: 클래스의 name → plain의 full_name으로 직렬화

자주 하는 실수

  • plainToInstance만 호출하고 이름이 바뀐 plain을 기대함. 키 변경은 instance → plain에서 보이는 것이므로 instanceToPlain 사용 필요

최소 스니펫

class UserDto {
  @Expose({ name: 'full_name' }) name: string
}
// plain → instance 시 full_name이 name으로 들어옴
// instance → plain 시 name이 full_name으로 나감

excludeExtraneousValues

목적

  • DTO에 명시되지 않았거나 @Expose가 붙지 않은 필드를 변환 결과에서 제외해 데이터 무결성과 보안 강화

동작

  • plain → instance에서 옵션 excludeExtraneousValues: true 사용 시 @Expose가 붙은 필드만 주입
  • instance → plain에서도 동일 옵션 사용 시 @Expose가 붙은 필드만 직렬화

최소 스니펫

const dto = plainToInstance(UserDto, plain, { excludeExtraneousValues: true })

주의

  • 이 옵션을 쓰면 @Expose가 실질적으로 필터 역할을 함. 클래스에 정의만 되어 있고 @Expose 없는 필드는 제외됨
  • 필드가 많을수록 필터링 비용이 있으므로 DTO 범위를 최소화해 사용 권장

plainToInstance 기본기

목적

  • 평문 객체를 지정한 클래스 인스턴스로 변환해 타입 기반 접근과 메서드 활용 가능하게 함

핵심 포인트

  • 배열 입력도 동일하게 처리. 결과는 인스턴스 배열
  • 중첩 객체 변환은 @Type으로 타입 힌트 제공 필요
  • plainToClass는 구버전 명칭. plainToInstance 사용 권장

최소 스니펫

const instance = plainToInstance(ClassCtor, plain)

@Type과 Swagger @ApiProperty(type)의 차이

역할 구분

  • @Type(() => ChildDto)
    • 런타임 변환 목적. 중첩 객체나 배열 요소 타입 힌트 제공
    • 예: tracks: ChildDto[] 변환 시 각 요소를 ChildDto로 인스턴스화
  • @ApiProperty({ type: ChildDto, isArray: true })
    • 문서 목적. OpenAPI 스키마를 명시해 Swagger UI에 타입 정보 노출

왜 둘 다 필요한가

  • 문서화는 빌드 타임 타입 정보만으로 자동 완전 추론 불가. @ApiProperty로 명시 필요
  • 런타임 데이터 변환은 TypeScript 타입이 사라짐. @Type으로 힌트 제공 필요

최소 스니펫

class ParentDto {
  @Expose()
  @Type(() => ChildDto)
  items: ChildDto[]
}

방향별 변환 요약

  • plain → instance
    • excludeExtraneousValues: true일 때 @Expose로 화이트리스트 제어
    • @Exclude는 지정 필드 차단
    • @Expose({ name })로 입력 키 매핑 지원
    • @Type으로 중첩 타입 힌트 제공
  • instance → plain
    • @Exclude 적용 필드 제외
    • @Expose({ name }) 키 이름 변경 반영
    • excludeExtraneousValues: true면 @Expose 없는 필드 제거

자주 하는 오해와 디버깅 포인트

  • 오해: @Expose만 붙이면 비노출 필드가 자동 제외됨
    • 사실: 제외를 원하면 @Exclude 또는 excludeExtraneousValues와 함께 사용 필요
  • 오해: @Expose({ name })를 쓰면 plainToInstance 결과가 바뀐 키로 보임
    • 사실: 키 변경은 instance → plain에서 보이며, plain → instance에서는 매핑으로만 사용됨
  • 오해: @Type 없이도 중첩 배열이 알아서 변환됨
    • 사실: 런타임 타입 정보가 없으므로 @Type 필요
  • 콘솔 출력 혼동
    • instance를 그대로 console.log 하면 인스턴스 구조가 보일 뿐 직렬화 결과가 아님. 키 변경이나 제외 결과를 확인하려면 instanceToPlain 사용 필요

최소 스니펫

const plain = instanceToPlain(instance, { excludeExtraneousValues: true })

베스트 프랙티스

  • 외부 입력을 받는 DTO에는 excludeExtraneousValues와 @Expose 조합으로 화이트리스트 기반 변환 적용
  • 출력 DTO는 @Expose({ name })로 API 스키마와 명확히 매핑. 내부 도메인 모델과 API 응답 모델 분리 권장
  • 민감 정보는 @Exclude로 이중 차단. 변환 옵션 누락 시에도 노출 방지
  • 중첩 구조에는 @Type을 일관되게 지정. 배열 요소 타입 명시 필수
  • 변환 경계 최소화. 필요할 때만 변환 수행해 성능 부담 완화

간단 예시 모음

  1. 비노출 필드 제외
class SecureDto {
  @Expose() username: string
  @Expose() role: string
  @Exclude() password: string
}
  1. 이름 매핑과 직렬화 확인
class UserDto { @Expose({ name: 'full_name' }) name: string }
const plain = instanceToPlain(plainToInstance(UserDto, { full_name: 'Alice' }))
// plain은 { full_name: 'Alice' }
  1. 중첩 배열 변환
class ItemDto { @Expose() id: number }
class ListDto {
  @Expose()
  @Type(() => ItemDto)
  items: ItemDto[]
}

마무리

@Expose와 @Exclude는 필드 노출 제어의 선언적 기준, excludeExtraneousValues는 이를 강제 적용하는 실행 옵션, @Type은 런타임 변환의 타입 힌트, @ApiProperty(type)은 문서화 표준화 수단임 방향성과 옵션의 결합을 정확히 이해하면 DTO 경계를 안전하게 유지하면서 직렬화와 역직렬화에서 일관된 결과를 얻을 수 있음

참고자료