얼마 전, 회사 팀원에게 DTO를 왜 굳이 사용해야하는지?에 대한 질문을 받았다.
그래서 이번 포스팅에서는 DTO를 왜 사용해야하는지에 대해 알아보고자 한다.
먼저, DTO는 Data Transfer Object의 약자로, 데이터 전송 객체라고 한다.
DTO는 데이터베이스에서 데이터를 얻어 서비스나 비즈니스 로직으로 전달할 때 사용하는 객체이다.
아마, 앞서 받은 질문은, 엔터티를 그대로 사용하면 되지 왜 DTO를 사용해야하나? 라는 의문이었을 것이다.
아래 예시는, User 에 관한 엔터티 클래스이다.
// user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
loginType: string;
@Column()
email: string;
@Column()
password: string;
@Column()
socialId: string;
@Column()
socialAccessToken: string;
@Column()
name: string;
@Column()
nickname: string;
@Column()
profileImage: string;
}
위와 같은 엔터티를 그대로 사용하면, 모든 데이터가 노출되기 때문에 보안상의 이유로 DTO를 사용한다.
아래 예시는, 특정 User 를 조회하는 API 이다.
// user.controller.ts
import { Controller, Get, Param } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('user')
class UserController {
constructor(private readonly userService: UserService) {}
@Get(':id')
async getUser(@Param('id') id: number) {
return await this.userService.getUser(id);
}
}
// user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
class UserService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
async getUser(id: number) {
return await this.userRepository.findOne(id);
}
}
이 API 를 호출하면, 아래와 같은 응답이 온다.
{
"id": 1,
"loginType": "email",
"email": "test@test.com",
"password": "test",
"socialId": null,
"socialAccessToken": null,
"name": "test",
"nickname": "test",
"profileImage": null
}
모든 데이터가 노출되기 때문에 보안상의 이유로 DTO를 사용한다.
엔터티를 그대로 사용하면, 엔터티의 데이터가 변경되면 API 응답 데이터도 변경된다.
기존 User 테이블에서, lastLoginAt 컬럼을 추가하였다고 가정하자.
@Column()
lastLoginAt: Date;
이 경우, API 응답 데이터도 변경되어야 한다.
백엔드에서는, API 응답 데이터를 변경하는 것은 큰 문제가 되지 않지만, 프론트엔드에서는 큰 문제가 된다.
프론트엔드에서는, API 응답 데이터가 변경되면, API 응답 데이터를 사용하는 모든 곳에서 변경된 데이터를 사용해야 한다.
이러한 문제를 해결하기 위해 DTO를 사용한다.
아래 예시는, 본인이 아닌, 다른 User 를 조회하는 API 이다.
// 엔터티를 그대로 사용하는 경우
{
"id": 1,
"loginType": "email",
"email": "test@test.com",
"password": "test",
"socialId": null,
"socialAccessToken": null,
"name": "test",
"nickname": "test",
"profileImage": null,
"lastLoginAt": "2021-10-24T12:00:00.000Z" // 추가된 컬럼
}
// DTO를 사용하는 경우
{
"id": 1,
"email": "test@test.com",
"name": "test",
"nickname": "test",
"profileImage": null // 필요한 데이터만 응답받아서, 추가된 컬럼이 응답되지 않는다.
}
위와 같이, 필요한 데이터만 응답받아서, 추가된 컬럼이 응답되지 않는다.
이러한 이유로, 엔터티를 그대로 사용하지 않고, DTO를 사용한다.
엔터티를 그대로 사용하면, 불필요한 데이터가 포함되어, API 응답 데이터의 크기가 커진다.
예를 들어, User 테이블과 연관된 Post 테이블이 있다고 가정하자.
이 경우, 엔터티를 그대로 사용하면, User 테이블과 연관된 Post 테이블의 데이터도 응답된다.
하지만, User 테이블과 연관된 Post 테이블의 데이터는 필요하지 않다.