개요
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을 일관되게 지정. 배열 요소 타입 명시 필수
- 변환 경계 최소화. 필요할 때만 변환 수행해 성능 부담 완화
간단 예시 모음
- 비노출 필드 제외
class SecureDto {
@Expose() username: string
@Expose() role: string
@Exclude() password: string
}- 이름 매핑과 직렬화 확인
class UserDto { @Expose({ name: 'full_name' }) name: string }
const plain = instanceToPlain(plainToInstance(UserDto, { full_name: 'Alice' }))
// plain은 { full_name: 'Alice' }
- 중첩 배열 변환
class ItemDto { @Expose() id: number }
class ListDto {
@Expose()
@Type(() => ItemDto)
items: ItemDto[]
}마무리
@Expose와 @Exclude는 필드 노출 제어의 선언적 기준, excludeExtraneousValues는 이를 강제 적용하는 실행 옵션, @Type은 런타임 변환의 타입 힌트, @ApiProperty(type)은 문서화 표준화 수단임 방향성과 옵션의 결합을 정확히 이해하면 DTO 경계를 안전하게 유지하면서 직렬화와 역직렬화에서 일관된 결과를 얻을 수 있음