코딩딩/NestJS

왜 NestJS에서는 Builder와 같은 역할을 하는 데코레이터가 없을까?

전낙타 2024. 11. 24. 17:36

요즘 NestJS로 사이드 프로젝트를 진행하면서 문득 아래 코드를 보고 이런 생각이 들었다.

// 여기!
static toDto(userAfterAuth: UserAfterAuth, user: User) {
  return {
    email: userAfterAuth.email,
    nickname: user.nickname,
    roles: userAfterAuth.roles,
    profileImage: user.profileImage,
  };
}

왜 NestJS에서는 Builder와 같은 역할을 하는 데코레이터가 없을까?

만약 이 코드를 Spring에서 Lombok의 @Builder를 사용하면 이렇게 작성할 수 있다.

public static UserDto toDto(UserAfterAuth userAfterAuth, User user) {
    return UserDto.builder()
            .email(userAfterAuth.getEmail())
            .nickname(user.getNickname())
            .roles(userAfterAuth.getRoles())
            .profileImage(user.getProfileImage())
            .build();
}

뭔가 Builder 패턴만의 딱딱 맞아 떨어지는 형태가 나는 좋은데 말이지.

지금부터 왜 NestJS에서는 Lombok 같은 라이브러리가 없는지 알아봐야겠다.


왜 NestJS에서는 Builder 패턴을 사용하지 않을까?

 

1. TypeScript의 언어적 특성 및 객체 생성 방식

간편한 객체 리터럴 사용

TypeScript에서는 객체를 생성할 때 객체 리터럴을 많이 사용한다. 프로퍼티를 직접 지정하여 객체를 생성하는 것이 일반적이며, 이는 빌더 패턴의 필요성을 감소시킨다.

const user = {
  id: 1,
  name: 'Byeongjun',
  email: 'test@example.com',
};

선택적 프로퍼티 및 기본값 지원

인터페이스나 클래스에서 선택적 프로퍼티(?)와 기본값을 활용할 수 있어 객체 생성 시 모든 필드를 지정할 필요가 없다.

class User {
  id: number;
  name?: string;
  email?: string;

  constructor(id: number, name: string = 'Unknown') {
    this.id = id;
    this.name = name;
  }
}

const user = new User(1);

 

2. 클래스 생성자 및 프로퍼티 초기화의 유연성

생성자 파라미터의 간결성

TypeScript에서는 생성자 파라미터에 public, private, readonly 등을 직접 지정하여 프로퍼티를 선언하고 초기화할 수 있다.

class User {
  constructor(
    public id: number,
    public name: string,
    public email: string,
  ) {}
}

const user = new User(1, 'Byeongjun', 'test@example.com');

프로퍼티 초기화의 유연성

프로퍼티를 클래스 내부에서 초기화하거나, 선택적 프로퍼티로 선언하여 객체 생성 시 유연하게 처리할 수 있다.

 

3. 함수형 프로그래밍 스타일과 불변성

함수형 프로그래밍 선호

JavaScript 및 TypeScript 커뮤니티는 객체 지향 프로그래밍보다 함수형 프로그래밍 스타일을 선호하는 경향이 있다. 이는 객체의 상태를 변경하기보다는 새로운 객체를 생성하는 방식을 선호한다는 것을 의미한다.

 

스프레드 연산자 활용

기존 객체를 기반으로 새로운 객체를 생성할 때 스프레드 연산자를 사용하여 쉽게 구현할 수 있다.

const user = { id: 1, name: 'Byeongjun' };
const updatedUser = { ...user, email: 'test@example.com' };

 

4. 라이브러리 및 프레임워크 지원

class-transformer 및 class-validator의 활용

NestJS에서는 DTO(Data Transfer Object)와 함께 class-transformer, class-validator 라이브러리를 사용하여 객체를 변환하고 검증한다. 이를 통해 객체 생성 및 데이터 검증을 효율적으로 처리할 수 있어 빌더 패턴의 필요성이 줄어든다.

import { IsEmail, IsNotEmpty } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty()
  id: number;

  @IsNotEmpty()
  name: string;

  @IsEmail()
  email: string;
}

데코레이터의 제한 사항

TypeScript의 데코레이터는 런타임에 동작하며, Lombok처럼 컴파일 타임에 코드를 생성하지 않는다. 따라서 Lombok과 동일한 수준의 코드 생성을 위해서는 추가적인 구현이 필요하며, 이는 복잡성을 증가시킬 수 있다.

 

5. 보일러플레이트 코드의 상대적 감소

간결한 코드 작성

TypeScript는 Java에 비해 보일러플레이트 코드가 적다. 게터와 세터를 자동으로 생성하지 않아도 프로퍼티에 직접 접근할 수 있으며, 필요에 따라 접근 제어자를 사용할 수 있다.

class User {
  constructor(
    private id: number,
    private name: string,
    private email: string,
  ) {}

  getId(): number {
    return this.id;
  }
}

Lombok의 필요성 감소

Lombok은 Java의 장황한 코드 구조를 간결하게 만들기 위해 사용되지만, TypeScript에서는 언어 자체의 특성으로 인해 Lombok과 같은 도구의 필요성이 상대적으로 낮다.

 

6. 커뮤니티의 관행 및 학습 곡선

관행적인 스타일

NestJS 및 TypeScript 커뮤니티에서는 빌더 패턴보다 다른 디자인 패턴이나 코딩 스타일을 더 선호한다. 이는 새로운 개발자들이 코드베이스를 이해하고 유지보수하는 데 도움을 준다.

 

복잡성 증가 우려

빌더 패턴을 사용하면 객체 생성이 명시적이고 유연해지지만, 작은 프로젝트나 간단한 객체 생성의 경우 오히려 복잡성을 증가시킬 수 있다.


결론

NestJS에서는 TypeScript의 언어적 특성과 프레임워크의 구조로 인해 빌더 패턴이 일반적으로 사용되지 않는다. 대신 객체 리터럴, 클래스 생성자, DTO, 유효성 검증 라이브러리 등을 활용하여 객체를 효율적으로 생성하고 관리한다.

개인적으로는 Builder 패턴의 명시적이고 유연한 객체 생성 방식이 마음에 들지만, TypeScript의 특성과 NestJS의 철학을 이해하니 왜 빌더 패턴이 일반적이지 않은지 알 것 같다.


사실상 가장 큰 이유는 선택적 프로퍼티 및 기본값 지원이 아닐까.

nest 외길 인생을 걸을 건 아니지만 그래도 내가 익숙해지는게 빠를것같다.