컨트롤러와 라우팅 — @Controller, DTO, Pipe로 요청 처리하기
API를 만들 때 요청 데이터를 검증하지 않으면, 잘못된 데이터가 서비스 레이어까지 흘러들어갑니다. NestJS는 DTO + Pipe 조합으로 컨트롤러 진입 시점에서 데이터를 걸러냅니다. Spring의
@Valid+@RequestBody와 같은 역할인데, 어떻게 다른지 살펴보겠습니다.
컨트롤러 기본 구조
컨트롤러는 HTTP 요청을 받아서 적절한 서비스 메서드를 호출 하는 역할입니다. 비즈니스 로직은 넣지 않습니다.
@Controller('users') // 기본 경로: /users
export class UserController {
constructor(private readonly userService: UserService) {}
@Get() // GET /users
findAll() {
return this.userService.findAll();
}
@Get(':id') // GET /users/:id
findOne(@Param('id', ParseIntPipe) id: number) {
return this.userService.findOne(id);
}
@Post() // POST /users
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
@Put(':id') // PUT /users/:id
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.userService.update(id, updateUserDto);
}
@Delete(':id') // DELETE /users/:id
@HttpCode(204) // 응답 코드 지정
remove(@Param('id') id: string) {
return this.userService.remove(id);
}
}
Spring과의 대응을 정리하면 이렇습니다.
| NestJS | Spring | 역할 |
|---|---|---|
@Controller('users') | @RequestMapping("/users") | 기본 경로 |
@Get(), @Post() | @GetMapping, @PostMapping | HTTP 메서드 |
@Param('id') | @PathVariable("id") | 경로 파라미터 |
@Body() | @RequestBody | 요청 본문 |
@Query('page') | @RequestParam("page") | 쿼리 파라미터 |
@Headers('authorization') | @RequestHeader | 헤더 값 |
DTO — 요청 데이터의 타입 정의
DTO(Data Transfer Object) 는 요청 데이터의 형태를 정의하는 클래스입니다. TypeScript의 인터페이스로도 가능하지만, 클래스 를 써야 class-validator의 데코레이터를 붙일 수 있습니다.
import { IsString, IsEmail, IsOptional, MinLength } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(2)
name: string;
@IsEmail()
email: string;
@IsOptional()
@IsString()
phone?: string;
}
Spring의 @NotNull, @Size, @Email 어노테이션과 같은 역할입니다.
ValidationPipe — 요청 검증 자동화
DTO에 데코레이터를 붙여도 ValidationPipe를 설정하지 않으면 검증이 동작하지 않습니다.
// main.ts — 전역 설정
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // DTO에 없는 속성 자동 제거
forbidNonWhitelisted: true, // DTO에 없는 속성이 오면 400 에러
transform: true, // 문자열 → 타입 자동 변환
}));
await app.listen(3000);
}
각 옵션이 하는 일을 정리하면 이렇습니다.
| 옵션 | 동작 | 왜 필요한가 |
|---|---|---|
whitelist | DTO에 정의되지 않은 속성 제거 | 악의적인 추가 필드 차단 |
forbidNonWhitelisted | 정의되지 않은 속성이 있으면 에러 | 클라이언트 실수를 조기 발견 |
transform | @Param('id') 문자열을 숫자로 변환 | 수동 타입 변환 불필요 |
whitelist를 켜지 않으면 생기는 일
// POST /users 요청 본문
{
"name": "홍길동",
"email": "hong@example.com",
"isAdmin": true // ← DTO에 없는 필드!
}
whitelist: true가 없으면 isAdmin이 그대로 서비스까지 전달됩니다. 이걸로 권한 상승 공격이 가능합니다. ** 반드시 켜야 합니다.**
내장 Pipe — 타입 변환과 검증
NestJS에는 자주 쓰는 변환/검증용 Pipe가 내장되어 있습니다.
// ParseIntPipe — 문자열을 정수로 변환, 실패 시 400
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {}
// ParseUUIDPipe — UUID 형식 검증
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {}
// DefaultValuePipe — 기본값 설정
@Get()
findAll(@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number) {}
Spring에서 @PathVariable에 타입을 지정하면 자동 변환되는 것과 비슷하지만, NestJS는 Pipe라는 명시적 단계 를 거칩니다. 변환 실패 시 자동으로 400 Bad Request를 반환합니다.
커스텀 Pipe 만들기
특수한 변환이나 검증이 필요하면 PipeTransform 인터페이스를 구현합니다.
@Injectable()
export class ParseDatePipe implements PipeTransform<string, Date> {
transform(value: string, metadata: ArgumentMetadata): Date {
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new BadRequestException(`"${value}"는 유효한 날짜가 아닙니다`);
}
return date;
}
}
// 사용
@Get()
findByDate(@Query('date', ParseDatePipe) date: Date) {}
주의할 점
DTO에 인터페이스를 쓰면 검증이 안 된다
TypeScript 인터페이스는 **컴파일 후 사라집니다 **. 런타임에 존재하지 않기 때문에 class-validator가 메타데이터를 읽을 수 없습니다.
// 이렇게 하면 검증 안 됨
interface CreateUserDto {
name: string;
email: string;
}
// 반드시 class로 정의
class CreateUserDto {
@IsString()
name: string;
}
이것은 TypeScript를 처음 쓰는 개발자가 가장 많이 하는 실수입니다.
class-transformer의 @Exclude로 응답 필드 제어
응답에 비밀번호 같은 민감한 필드가 포함되지 않도록 해야 합니다.
import { Exclude } from 'class-transformer';
export class User {
id: number;
name: string;
@Exclude()
password: string; // 응답에서 제외
}
ClassSerializerInterceptor를 전역으로 등록해야 @Exclude()가 동작합니다.
정리
| 포인트 | 내용 |
|---|---|
| 컨트롤러 | HTTP 요청 수신만 담당, 비즈니스 로직은 서비스로 |
| DTO | class로 정의해야 검증 가능 (인터페이스 불가) |
| ValidationPipe | whitelist: true 필수 — 없으면 보안 취약점 |
| 내장 Pipe | ParseIntPipe, ParseUUIDPipe 등으로 타입 변환 자동화 |
| 핵심 기억 | Pipe가 컨트롤러 진입 전 데이터를 걸러주는 관문 역할 |