JS 공부/NestJS

[NestJS] Spring을 공부하다 왔을 때 처음 낯설던 것(DTO)

장아장 2023. 11. 3. 11:20

SpringBoot에서 @RequestBody를 통해 dto 객체를 받을 수 있었다. 

똑같은 로직을 생각하고, RegisterRequest객체를 js에서 아래와 같이 만들었다. 

const EMAIL_REGEX = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;

export class RegisterRequest{
	username:string;
    nickname:string;
    email:string;
    password:string;
    passwordCheck:string;
    
    constructor(username:string, nickname:string, email:string, password:string, passwordCheck:string) {
    	this.username = username;
        this.nickname = nickname;
        this.email = email;
        this.password = password;
        this.passwordCheck = passwordCheck;
    }
    
    public isValid():boolean {
    	return this.password === this.passwordCheck;
    }
    
    public isEmailFormat():boolean {
    	return EMAIL_REGEX.test(this.email);
    }
}

 

이런 로직을 @Body()로 바로 받으면 어떻게 될까? 

 

터진다(...)

왜 터졌을까...?

왜 이럴까 너...?

 

사실 이에 대해서 찾아보면서, 부스트캠프에서 리뷰어분이 해주셨던 이야기가 있었다. 

 

JS에서 Body의 DTO를 어떻게 받는지 찾아보는게 좋을 것 같습니다!

 

네??? 달라요?? 싶었다. 

여기서 해결책을 찾아나가기 시작했다. 

 

자바와 자바 스크립트는 DTO를 어떻게 초기화시킬까?

먼저 자바의 상황부터 해석해보자. 

자바는 직렬화, 역직렬화를 통해 body의 json을 DTO로 초기화시킨다. 

직렬화를 통해 자바의 객체에서, 인스턴스와 인스턴스의 값만을 json으로 만든 후, 바이트스트림으로 내보낸다. 

 

받을 때에는 이진 데이터 스트림을 받아 json으로 파싱한다. 

이를 원하는 객체의 인스턴스에 맞게 매핑해 초기화시킨다. 

 

그렇다면 자바스크립트는?

아무것도 안한다고 한다(...)

 

왜 안할까????

 

자바의 직렬화와 역직렬화의 과정 중 일부를 꺼내와보자.

 

객체에서, 인스턴스와 인스턴스의 값만을 json으로 만든 후

받을 때에는 이진 데이터 스트림을 받아 json으로 파싱

 

여기에서 우리가 생각할 부분이 있다. 

JS는 json을 단순 object로 받을 수 있다.

그렇기에 직렬화, 역직렬화의 과정의 부분에서, 이진 데이터스트림을 json으로 만들면 모든 과정이 끝난다. 

 

그래서, 위의 로직을 수정해주고 컨트롤러에 넣어주었다. 

const EMAIL_REGEX = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;

export interface RegisterRequest{
	username:string;
    nickname:string;
    email:string;
    password:string;
    passwordCheck:string;
}

const validatePassword = (registerRequest: RegisterRequest) => {
	if(registerRequest.password === registerRequest.passwordCheck) return;
    throw new PasswordNotEqualException();
}
export const registerValidator = (registerRequest: RegisterRequest) => {
	if(!EMAIL_REGEX.test(registerRequest.email))
    	throw new EmailNotFormatException();
    validatePassword(registerRequest);
}

// Controller로 이동

  @Post("register")
  public async register(@Body() registerRequest: RegisterRequest) {
    try {
      await this.authService.register(registerRequest);
      return { message: "회원가입이 성공했습니다." };
    } catch (e) {
      return { error: e.message };
    }
  }

 

이렇게 했을 때, 아무 문제가 없이 동작하게 된다. 

여기에서 유의할 점은, 아까 말했듯이 js환경에서는 @Body()를 통해 들어온 객체는 단순 object일 뿐이라는 것이다. 

타입스크립트의 interface는 json의 타입의 형태를 명시해줄 뿐, json의 타입 자체를 바꾸는 능력은 없다. 

 

실제로 이렇게 했을 때의 아쉬운 점도 존재했다. 

  • interface로 존재하기 때문에, DTO내부에 메서드를 등록할 수 없다. 
  • 해당 인스턴스는 전부 public이다. 

 

캡슐화가 되지 않고, 정확하게 어떤 타입으로 body를 받는지를, 타입으로 검증하는게 복잡해지게 된다

(실제로 모든 key, value를 가져와 expected, actual을 검증해야 하는 방식이다)

 

그래서 class-transformer, class-validator 라이브러리를 이용해 DTO의 직렬화와 역직렬화, 그리고 spring validator의 어노테이션과 같은 역할을 하는 데코레이터들을 쓸 수 있게 해준다. 

이를 통해 데이터를 검증하고 객체의 변환을 단순화시킬 수 있다. 

또한, 타입에 대한 안전성을 가질 수 있게 된다. 

 

class-transformer는 자바스크립트 리플렉션을 통해 동작하는데, 물론 단순 json을 받는 것 보단 느릴 수 있지만, 그마저도 성능상의 속도저하가 미미할 정도라고 한다. 

 

해당 공부를 하면서 직렬화, 역직렬화를 어떻게 하는지, ts에서는 어떤 방식이 기본적으로 채택되는지 알게되었다.