본문 바로가기
백엔드 개발/백엔드 일기

#005. 백엔드 코드리뷰 : DTO에 엔티티 값을 그대로 사용하는 안티패턴

by iamjoy 2022. 4. 18.

 

 

 


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