DTO는 데이터를 전달하거나 받을 때 데이터를 담는 요소로 클라이언트와 서버 간, 서버 내에서 레이어 간에 데이터를 주고 받을 때 사용할 수 있다. 그런데 DTO를 이렇게 표면적인 정의로 접근하면 엔티티를 그대로 DTO에 사용하는 안티패턴을 만들 수 있다.
아래는 유저의 전화번호 뒷자를 확인해서 일치하는 것이 있으면 아이디와 비밀번호를 알려주는 시스템을 아주 간단하게 만든 예시이다.
아래와 같은 테스트를 통과시켜보려고 한다.
// test.ts
import request from "supertest";
import app from './app';
describe("005 sample test", function () {
it('요청의 결과 200이 리턴된다.', async() => {
await request(app)
.get('/?phonenumber=2058')
.expect(200)
.expect({"_id":2,"_password":"def456"});
})
})
먼저 client 와 통신할 controller를 만들고, 로직을 담을 service 레이어를 만들었다. 아직 제대로 된 DB를 붙이지 않았으므로 간단히 db 객체를 만들었다. 받은 정보와 일치하는 값이 있는 지만 filter 로 확인해서 돌려주는 로직을 답고 있다. 유저 정보 (id 와 password)를 가지고 오므로 getAuthInfo라고 이름을 붙였다.
// controller.ts
getAuthInfo(query: FindUserInformationRequest): FindUserInformationResponse {
const serviceInstance = Container.get(ExampleService);
const authInfo: AuthInfo = serviceInstance.getAuthInfo(query);
return new FindUserInformationResponse(authInfo);
}
// service.ts
getAuthInfo (dto: FindUserInformationRequest): AuthInfo{
return exampleDB.filter(cur => cur.phone === Number(dto.phone))[0];
}
controller를 보면 client에서 넘어온 query 값을 FindUserInformationReques라는 DTO 객체 타입으로 받고 있다. 이 DTO에는 다음과 같이 유저 이름과 전화 번호를 받는다. 여기에서는 간단하게 전화번호만 확인해서 비교하려고 한다. (이후 발전 시킬 예정)
import { IsNumber, IsString } from "class-validator"
export class FindUserInformationRequest{
@IsString()
name: string;
@IsNumber()
phone: number;
constructor(){}
}
DTO는 값을 강제할 수 없다. 위의 request DTO는 name 과 phone을 넣고 있는데 만약 다음과 같이 phonenumber로 값을 넣어주면 dto.phone을 사용하고 있는 service 레이어에서 서버 에러가 난다. 코드는 DTO를 interface처럼 사용하고 있기 때문에 DTO에 phone이라고 되어있으면, 그 이상의 정보를 담지 않는다.

이 글에서 중심적으로 말하고자 하는 안티패턴은 다음과 같다.
엔티티와 response DTO가 있을 때 엔티티의 컬럼 값을 그대로 DTO로 사용하는 것이다.
클라이언트와 맞춘 스펙을 제대로 사용할 수 없고 백엔드의 API가 플랫폼으로써의 역할을 제대로 할 수 없다.
클라이언트와 소통하기 위해 매번 그에 맞는 API를 만들어야 할 수도 있다.

// entity.ts
import { Column } from "typeorm";
export class AuthInfo{
@Column()
id: number;
@Column({length: 50})
identification: string;
@Column({length:100})
pw: string;
@Column({length: 100})
name: string;
@Column({nullable: false})
phone: number;
}
// response DTO
import { AuthInfo } from "entity/AuthInfo";
export class FindUserInformationResponse{
private readonly _id: number;
private readonly _pw: string;
constructor(authInfo : AuthInfo){
this._id = authInfo.id;
this._pw = authInfo.pw;
}
}
이렇게 response를 만들어 클라이언트에게 넘겨주면 클라이언트는 id 와 pw를 넘겨 받게 되는데, 이렇게 하면 response DTO를 만드는 의미가 줄어든다. 상황에 따라 맞게 만들어진 DTO라면 그 상황에 맞는 이름들이 생긴다. 예를 들어서 비밀번호를 업데이트 한다면 예전 비밀번호(passwordFrom) 과 변경된 비밀번호 (passwordTo)와 같이 똑같은 pw라도 다른 이름으로 부를 필요가 있다.
이 예시에서는 클라이언트와 만든 스펙이 identification, password라면 다음과 같이 DTO를 수정할 수 있다.
import { AuthInfo } from "entity/AuthInfo";
export class FindUserInformationResponse{
private readonly identification: number;
private readonly password: string;
constructor(authInfo : AuthInfo){
this.identification = authInfo.id;
this.password = authInfo.pw;
}
}
이러면 다음 테스트를 통과할 수 있다.

직접 돌려볼 수 있는 코드는 아래 제 깃헙에서 확인할 수 있습니다!
https://github.com/erie0210/blog-example-code/tree/main/005
GitHub - erie0210/blog-example-code: 블로그에 작성하는 글의 예시 코드를 모아놓는 레포입니다.
블로그에 작성하는 글의 예시 코드를 모아놓는 레포입니다. Contribute to erie0210/blog-example-code development by creating an account on GitHub.
github.com
'백엔드 개발 > 백엔드 일기' 카테고리의 다른 글
#007. 백엔드 성장일기: 주간 프리뷰(작은 동물 의자, 이별 휴가 등) (2) | 2022.04.20 |
---|---|
#006. 백엔드 성장일기: 안티패턴 return await (0) | 2022.04.19 |
#004. 백엔드 성장 일기 : source tree (0) | 2022.04.15 |
#003. 코드리뷰: 디미터의 법칙 (0) | 2022.04.14 |
#002: 글을 쓰기 시작하는 목적 두 가지 (2) | 2022.04.13 |