해당 내용은 NestJs와 typescript 기반의 내용으로 작성되었습니다.
NestJS, TypeScript 기반 프로젝트에서 흔히 고민하게 되는 주제 중 하나가 바로 DTO와 Interface의 분리입니다.
저 역시 초기에는 DTO만 사용했고, 실제로도 큰 문제 없이 서비스를 운영해왔습니다.
그런데 최근 기술과제에서 다음과 같은 요구를 받았습니다:
"Interface를 정의해주세요. 클라이언트에서 같은 타입을 사용할 수 있다고 가정해요."
이 경험을 통해 저는 처음으로 "DTO와 Interface를 나눠야 할까?"라는 질문을 깊게 하게 되었고,
이번 글을 통해 그 고민의 여정과 정리한 인사이트를 나눠보려 합니다.
DTO (Data Transfer Object): 주로 입력/출력 데이터의 구조를 정의.
class로 구현하며, 유효성 검증(class-validator) 등을 걸 수 있음.
Interface: 객체의 타입 정의를 위한 구조.
런타임에 존재하지 않으며, 타입 시스템 상에서만 의미를 가짐. interface 또는 type으로 선언.
기존에는 이렇게 사용했습니다:
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
}
이 DTO 하나로 controller, service, client 응답, swagger까지 모두 커버했죠.
간단한 CRUD에서는 매우 효율적입니다.
하지만 기술과제에서는 이런 요구사항이 붙었죠:
그래서 다음과 같이 코드를 나눠 사용했습니다:
// user.interface.ts
export interface User {
name: string;
email: string;
}
// create-user.dto.ts
export class CreateUserDto implements User {
@IsString()
name: string;
@IsEmail()
email: string;
}
interface는 런타임 코드가 없어, 클라이언트와 API 문서에 공유하기 적합| 상황 | Interface만 | DTO만 | 둘 다 사용 |
|---|---|---|---|
| 클라이언트 타입 공유 | ✅ | ❌ | ✅ |
| NestJS 유효성 검증 | ❌ | ✅ | ✅ |
| 내부 로직에서 단순 타입 체크 | ✅ | ✅ | ✅ |
| API 문서 자동화 (Swagger) | ❌ | ✅ | ✅ |
| 테스트/모킹 유연성 | ✅ | ❌ | ✅ |
implements Interface를 통해 중복을 줄이는 방향으로 관리 가능type, Pick<T, K>, Omit<T, K> 등을 활용하면 중복 선언도 줄일 수 있음"인터페이스와 DTO를 분리한다고 진짜 효과가 있을까?"
그에 대한 제 결론은 이렇습니다:
단순한 CRUD에서는 굳이 분리할 필요는 없지만,
확장성, 타입 공유, 테스트 환경까지 고려한다면 분리해서 관리하는 것이 장기적으로 더 유리하다.
당장은 귀찮을 수 있어도, 프로젝트 규모가 커질수록 그 진가가 드러납니다.
혹시 여러분도 같은 고민을 해보셨나요?
다른 팀에서는 어떤 기준으로 interface와 DTO를 관리하고 있는지도 궁금합니다!