iamkanguk.dev

[NestJS] ResponseEntity와 정적 팩토리 메서드 본문

Framework/NestJS

[NestJS] ResponseEntity와 정적 팩토리 메서드

iamkanguk 2023. 11. 14. 19:28

필자는 Nest를 개발할 때 Service와 Repository를 거쳐온 결괏값을 Client로 반환할 때 Response DTO 객체로 만들어서 Controller 계층으로 넘기고 Controller에서 클라이언트로 결괏값을 반환할 때 ResponseEntity 객체에 넣어서 반환한다.

 

ResponseEntity를 사용한 이유는 무엇인가요?

ResponseEntity는 뭐 특별한 라이브러리나 그런것이 아니다. 원래 초기에는 개발할 때 그냥 return { age: 10, name: '' } 이런 식으로 했었는데 가시성 측면으로도 좋지 않아 보였고, 추후 유지보수적으로도 좋지 않다고 생각했다. Response DTO를 도입한 것도 그 이유다.

 

그래서 뭔가 객체를 하나 만들어서 반환해준다면 코드적으로 더욱 깔끔할 것 같다고 판단되었다. 그래서 블로그 글들을 많이 찾아봤는데 ResponseEntity를 직접 커스텀해서 만드신 개발자 분이 있었다.

 

하지만 필자가 의도한 이유로 ResponseEntity를 사용한 것이 아니었다. 블로그 개발자님은 원래는 성공 Response를 interceptor를 활용해서 코드를 구성했었는데 Controller 계층에서 전달하고 있는 데이터 타입과 API의 응답 타입이 맞지 않는 문제로 인해 도입하셨다고 했다.

 

그래서 필자는 interceptor를 활용할 생각은 못했지만, 위와 같은 이유로 인해 ResponseEntity를 커스텀했다.

 

ResponseEntity Code

import { Exclude, Expose } from 'class-transformer';

export class ResponseEntity<T> {
  @Exclude() private readonly _isSuccess: boolean;
  @Exclude() private readonly _statusCode: number;
  @Exclude() private readonly _result: T;

  private constructor(
    isSuccess: boolean,
    statusCode: number,
    result?: T | undefined,
  ) {
    this._isSuccess = isSuccess;
    this._statusCode = statusCode;
    this._result = result;
  }

  @Expose()
  get isSuccess() {
    return this._isSuccess;
  }

  @Expose()
  get statusCode() {
    return this._statusCode;
  }

  @Expose()
  get result() {
    return this._result;
  }

  static OK(statusCode: number): ResponseEntity<undefined> {
    return new ResponseEntity(true, statusCode);
  }

  static OK_WITH<T>(statusCode: number, result: T): ResponseEntity<T> {
    return new ResponseEntity<T>(true, statusCode, result);
  }
}

 

필자는 평소에 API Response에서는 공통적으로 isSuccess, statusCode 그리고 result를 반환한다. 그리고 실패 Response인 경우에는 여기에 timestamp, url, error message 등을 추가한다.

 

위의 코드는 성공 Response 객체를 생성할 수 있는 코드만 작성했다. Error Response는 ExceptionFilter가 마무리되면 비슷한 구조로 추가할 예정이다.

 

그리고 글을 작성하면서 생각났지만 위에서 statusCode parameter는 없어도 될 것 같다. 최근에 클린코드 책을 읽고있는데 메서드의 파라미터 개수는 없으면 너무 좋고 1개까지가 좋다고 써져 있어서 최대한 지켜보려고 노력하고 있기 때문에 statusCode 매개변수는 없어도? 충분히 구현이 가능할 것 같다.

 

그래서 위 ResponseEntity를 가지고 Controller에서는 다음과 같이 코드를 작성한다.

 

  @Post()
  @HttpCode(HttpStatus.CREATED)
  async postMarkCategory(
    @UserId() userId: number,
    @Body() body: PostMarkCategoryRequestDto,
  ): Promise<ResponseEntity<PostMarkCategoryResponseDto>> {
    const result = await this.markCategoryService.createMarkCategory(
      userId,
      body.title,
    );
    return ResponseEntity.OK_WITH(HttpStatus.CREATED, result);
  }

 

위와 같이 작성하면 Service에서 어떤 Type의 결과값을 반환했는지도 확인할 수 있고 깔끔하게 코드를 작성할 수 있다.

여기서 눈여겨 볼 수 있는 점은 new 키워드를 사용하지 않았다는 것이다. 이유가 무엇인지 보자!

 

정적 팩토리 메서드

필자는 생각없이 다른 개발자님의 블로그를 따라한 것 같아서 지금이라도 이유를 한번 곰곰이 생각해보려고 한다. 나처럼 하지 마시길 ㅠ

 

정적 팩토리 메서드는 객체 생성의 역할을 하는 클래스 메서드라고 할 수 있다. 그러면 여기서 질문을 던질 수 있는 것이 생성자를 써도 충분히 구현이 가능할텐데 왜 생성자를 private으로 선언하면서 정적 팩토리 메서드를 사용했느냐이다.

 

(1) 이름을 가질 수 있다.

이름을 가질 수 있다는 점이 가장 큰 장점이다. 우리는 new 라는 키워드를 알고 있다. new를 통해 객체를 생성하려고 한다면 Class의 내부 구조를 알고 있어야 객체를 올바르게 생성할 수 있다. 하지만 정적 팩토리 메서드를 사용하게 되면 메서드 이름에 객체의 생성 목적을 부여할 수 있다.

 

위의 코드를 보면 나는 OK와 OK_WITH 이라고 네이밍을 정했다. 물론 여기에 ERROR라는 키워드를 추가할 수 있을 것 같다. 이렇게 정적 팩토리 메서드를 적용하면서 Controller 계층이나 외의 다른 계층에서 성공 응답인지 실패 응답인지 쉽게 구분할 수 있다.

 

만약에 생성자를 통해 객체를 생성했다면 일일이 Depth를 들어가면서 확인을 했어야 했기 때문에 개발적인 측면에서 불편했을 수 있을 것 같다.

 

(2) 객체 생성을 캡슐화할 수 있다.

정적 팩터리 메서드는 객체 생성을 캡슐화하는 방법일 수 있다. 아래 예시 코드를 보자.

 

export class CarDto {
    private readonly name: string;
    private readonly position: number;
    
    constructor(name: string, position: number) {
    	this.name = name;
        this.position = position;
    }
    
    static from(Car car): CarDto {
    	return new CarDto(car.getName(), car.getPosition());
    }
}

// 정적 팩토리 메서드를 사용하는 경우에는?
const carDto = CarDto.from(car);

// 생성자를 사용하는 경우에는?
const carDto = new CarDto(car.getName(), car.getPosition());

 

코드는 이런 느낌이다~라는 정도만 봐주시면 좋을 것 같다. 정적 팩토리 메서드를 사용하게 되면 내부 구조를 모르더라도 쉽게 Entity에서 DTO로 변환할 수 있다. 하지만 생성자를 사용하게 된다면 내부 구조를 모두 드러내게 된다.

 

따라서 가독성도 좋고 객체지향적으로도 코드를 작성할 수 있다. 하지만 팩토리 메서드만 존재하는 클래스를 생성할 경우 상속이 불가능하다는 점은 참고해야 할 것 같다!

 

급 후기..

앗.. 정적 팩토리 메서드에서 from과 of가 있는데 from은 하나의 parameter, of는 여러 개의 parameter를 받아올 때 키워드를 사용하는데 난 반대로.. 했었네? ㅋㅋㅋㅋㅋㅋ ㅠㅠㅠ


참고 자료

- https://overcome-the-limits.tistory.com/714

 

[Project] 프로젝트 삽질기33 (feat 정적 팩토리 메서드)

들어가며 사이드 프로젝트에서 푸시 알림을 활용한 서비스를 개발하고 있습니다. 개발하는 과정에서, 컨트롤러에서 응답 객체를 클라이언트에게 전달할 때, 어떻게 데이터를 전달하면 좋을까

overcome-the-limits.tistory.com