본문 바로가기

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

어가며

사이드 프로젝트에서 푸시 알림을 활용한 서비스를 개발하고 있습니다. 개발하는 과정에서, 컨트롤러에서 응답 객체를 클라이언트에게 전달할 때, 어떻게 데이터를 전달하면 좋을까 생각했습니다. 개인적으로 ResponseEntity를 활용하여 응답 데이터를 전달하는 방식으로 코드를 작성했습니다. ResponseEntity를 활용하는 과정에서 했던 고민을 공유하고자 합니다. 정적 팩토리 메서드 글을 정리하면서 해당 을 참고했습니다.

 
 

 


응답 객체 전달 방식 고민

API에 대한 성공적인 응답을 어떻게 클라이언트에게 전달할까 고민했습니다. 

 

 

 

 

 

NestJS에서의 일반적인 요청/응답 생명주기는 위와 같습니다. 컨트롤러에서 응답하는 과정에서 인터셉터를 거치고, 예외 필터를 거친 후 응답합니다. 그럼 성공 응답은 인터셉터를 활용하고 실패 응답은 예외 필터를 활용한다면 중복되는 코드를 제거할 수 있겠다고 판단했습니다.

 

 

 

1. 인터셉터를 활용한다.

그렇게 성공 응답을 인터셉터를 활용하는 방식으로 코드를 구성하려 했습니다.

 

 

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { map, Observable } from 'rxjs';

@Injectable()
export class CommonResponseFormInterceptor implements NestInterceptor {
  intercept(
    context: ExecutionContext,
    next: CallHandler<any>,
  ): Observable<any> | Promise<Observable<any>> {
    return next.handle().pipe(map((data) => ({ success: true, data })));
  }
}

 

 

다음과 같이 인터셉터를 구성하여 활용하려 했습니다. 그럼 성공 응답의 형식이 success: true와 data 객체가 출력될 것입니다. 하지만 성공 응답을 인터셉터로 구현하면 문제점이 발생했습니다.

 

 

 

import { CommonResponseFormInterceptor } from './common/interceptors/common.response.form.interceptor';
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';

@UseInterceptors(CommonResponseFormInterceptor)
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}
  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

 

 

먼저 위와 같이 컨트롤러를 작성했을 때, getHello의 리턴 타입은 string 값입니다. 하지만 API의 최종 응답 값은 success: true와 data 객체입니다. 즉 getHello에서 전달하고 있는 리턴 타입과 API의 최종 응답의 타입이 맞지 않는 문제가 발생했습니다.

 

 

 

2. ResponseEntity를 활용한다.

리턴 타입과 컨트롤러에서 전달하는 객체의 타입을 맞추기 위해 ResponseEntity를 구성했습니다.

 

 

import { ResponseStatus } from '@app/common-config/src/response/ResponseStatus';
import { Exclude, Expose } from 'class-transformer';

export class ResponseEntity<T> {
  @Exclude() private readonly _statusCode: ResponseStatus;
  @Exclude() private readonly _message: string;
  @Exclude() private readonly _data: T;

  private constructor(status: ResponseStatus, message: string, data: T) {
    this._statusCode = status;
    this._message = message;
    this._data = data;
  }

  static OK(): ResponseEntity<string> {
    return new ResponseEntity<string>(ResponseStatus.OK, '', '');
  }

  static OK_WITH(message: string): ResponseEntity<string> {
    return new ResponseEntity<string>(ResponseStatus.OK, message, '');
  }

  static ERROR(): ResponseEntity<string> {
    return new ResponseEntity<string>(
      ResponseStatus.SERVER_ERROR,
      '서버 에러가 발생했습니다.',
      '',
    );
  }

  static ERROR_WITH_DATA<T>(
    message: string,
    code: ResponseStatus = ResponseStatus.SERVER_ERROR,
    data: T,
  ): ResponseEntity<T> {
    return new ResponseEntity<T>(code, message, data);
  }

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

  @Expose()
  get message(): string {
    return this._message;
  }

  @Expose()
  get data(): T {
    return this._data;
  }
}

 

 

위와 같이 ResponseEntity를 설계하여, 컨트롤러에서 리턴 값을 지정할 때 아래와 같이 설정했습니다.

 

 

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}
  @Get()
  getHello(): Promise<ResponseEntity<string>> {
    return ResponseEntity.OK_WITH('이벤트 처리가 완료 됐습니다.');
  }
}

 

 

그 후 ResponseEntity에 정적 팩토리 메서드를 활용하여 객체를 생성하도록 설정했습니다. 그럼 왜 정적 팩토리 메서드를 활용했으며, 어떻게 활용했는지 더 자세하게 알아보겠습니다.

 

 

 

 

 

 


정적 팩토리 메서드

정적 팩토리 메서드란 객체 생성의 역할을 하는 클래스 메서드라는 의미로 요약해볼 수 있습니다. 그럼 왜 정적 팩토리 메서드를 활용했을까요? 생성자를 활용하여 객체를 생성하면 될 텐데 왜 생성자를 private으로 설정했으며, 정적 팩토리 메서드를 활용함으로써 얻을 수 있는 이점은 무엇인지 정리해보려 합니다.  

 

 

1. 이름을 가질 수 있다.

정적 팩토리 메서드를 활용하면 가장 큰 특징은 이름을 가질 수 있다는 점입니다. 객체는 생성 목적과 과정에 따라 생성자를 구별해서 사용할 필요가 있습니다. new라는 키워드를 통해 객체를 생성하는 생성자는 내부 구조를 잘 알고 있어야 목적에 맞게 객체를 생성할 수 있습니다. 하지만 정적 팩토리 메서드를 사용하면 메서드 이름에 객체의 생성 목적을 담아낼 수 있습니다. 

 

위의 ResponseEntity 클래스에서 정적 팩토리 메서드를 활용하여 OK, Error를 구분했습니다. 메서드를 통해 성공적인 응답인지, 실패 응답인지를 구분할 수 있었습니다. 만약 ResponseEntity에서 생성자로만 객체를 생성했다면 명확하게 구분하기 힘들 수 있습니다. 정적 팩토리 메서드를 활용하여 가독성을 개선했습니다.  

 

 

 

2. 호출할 때마다 새로운 객체를 생성할 필요가 없다.

정적 팩토리 메서드는 Static을 활용하기 때문에, 자주 사용되는 요소의 개수가 정해져 있다면 해당 개수만큼 미리 생성해놓고 조회(캐싱)할 수 있는 구조로 만들 수 있습니다. 여기서, 생성자의 접근 제한자를 private로 설정함으로써 객체 생성을 정적 팩토리 메서드로만 가능하도록 제한할 수 있습니다. 이를 통해 정해진 범위를 벗어나는 ResponseEntity 객체의 생성을 막을 수 있다는 장점을 확보할 수 있습니다. 

 

 

3. 객체 생성을 캡슐화할 수 있다.

정적 팩토리 메서드는 객체 생성을 캡슐화하는 방법이기도 합니다. 아래 코드를 보겠습니다. 웹 애플리케이션을 개발하다 보면 계층 간에 데이터를 전송하기 위한 객체로 DTO를 정의해서 사용합니다. DTO와 Entity 간에는 자유롭게 형 변환이 가능해야 하는데, 정적 팩토리 메서드를 사용하면 내부 구현을 모르더라도 쉽게 변환할 수 있습니다.

 

 

public class CarDto {
  private String name;
  private int position;

  pulbic static CarDto from(Car car) {
    return new CarDto(car.getName(), car.getPosition());
  }
}


// Car -> CatDto 로 변환
CarDto carDto = CarDto.from(car);

 

 

만약 정적 팩토리 메서드를 쓰지 않고 DTO로 변환한다면 외부에서 생성자의 내부 구현을 모두 드러낸 채 해야 합니다.

 

 

Car carDto = CarDto.from(car); // 정적 팩토리 메서드를 쓴 경우
CarDto carDto = new CarDto(car.getName(), car.getPosition); // 생성자를 쓴 경우

 

이처럼 정적 팩토리 메서드는 단순히 생성자의 역할을 대신하는 것뿐만 아니라, 우리가 좀 더 가독성 좋은 코드를 작성하고 객체지향적으로 프로그래밍할 수 있도록 돕습니다. 다만 팩토리 메서드만 존재하는 클래스를 생성할 경우 상속이 불가능하다는 단점이 있으니 참고하여 사용해야 합니다.

 

 

 

 

 


정적 팩토리 메서드 네이밍 컨벤션

그럼 정적 팩토리 메서드를 활용할 때 객체 생성에 있어 메서드 네이밍은 어떻게 해야 할까요? 이는 아래와 같이 구성할 수 있습니다.

 

 

  • from : 하나의 매개 변수를 받아서 객체를 생성
  • of : 여러 개의 매개 변수를 받아서 객체를 생성
  • getInstance | instance : 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음.
  • newInstance | create : 새로운 인스턴스를 생성
  • get[OtherType] : 다른 타입의 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음.
  • new[OtherType] : 다른 타입의 새로운 인스턴스를 생성.

 

 

 

 

 


 

 

 

 

 

마치며

앞으로도 팀의 발전을 돕는 개발자가 되기 위해 노력하려 합니다. 팀에 필요한 부분이 무엇일지 고민하면서, 팀에 도움이 된다면, 열심히 공부해서 실무에 적용할 수 있는 개발자가 되기 위해 노력하고 싶습니다. 팀의 성장에 기여할 수 있는 개발자가 되겠습니다. 

 

 

 

 

 


 

 

 

 

 

 

참고 및 출처

 

정적 팩토리 메서드(Static Factory Method)는 왜 사용할까?

tecoble.techcourse.co.kr