kimyu0218
  • [node] Swagger 데코레이터 어디까지 커스텀 해봤니?
    2023년 12월 23일 04시 17분 28초에 업로드 된 글입니다.
    작성자: @kimyu0218
    부제 : SWAG하게 Swagger 사용하는 법 🤟

     

    누구를 위한 Swagger인가

    Swagger는 API 문서를 간편하게 작성하도록 돕지만, 너무 많은 데코레이터들은 오히려 코드의 가독성을 해친다. FE분을 위해 작성한 Swagger 데코레이터들이 막상 BE 본인의 코드를 망치는 아이러니한 상황인 것이다.

    😑 : API 문서가 필요한 건 FE분들인데 왜 제 코드가 더러워져야 하죠?

    그렇다고 Swagger 데코레이터를 제거하면 별도의 문서화 작업이 필요하고, FE분들과 사이도 원만하지 못하게(?) 된다.

    내가 작성한 비즈니스 로직을 어지럽히지 않으면서 Swagger 데코레이터를 작성할 방법이 없을까 고민하다가 커스텀 데코레이터를 만들기로 결정했다.


    코드 중복을 최소화한 데코레이터

    커스텀 데코레이터는 nest 공식 문서를 참고하여 간단하게 만들 수 있다.

    하지만 데코레이터마다 applyDecorators()의 파라미터로 필요한 데코레이터들을 넘겨주면 컨트롤러만 깔끔해졌을 뿐, 데코레이터 파일의 코드가 복잡해진다. 즉, 하나의 데코레이터마다 아래와 같은 코드가 필요하다.

    return applyDecorators(
      ApiOperation(...),
      ApiParam(...),
      ApiBody(...),
      ApiBadRequestResponse(...),
      ApiUnauthorizedResponse(...),
      ApiForbiddenResponse(...),
      ApiInternalServerErrorResponse(...),
    );
    🙋‍♀️ : 일종의 돌려막기 아닌가요?

    데코레이터마다 중복되는 코드를 줄일 방법은 없을까?
    코드 중복을 줄이기 위해서는 기본적인 @ApiResponse()들을 자동으로 만들어주면 된다. 대부분의 요청은 200, 401, 403, 404, 500 응답을 보내기 때문이다.

    (프로젝트 경험이 거의 없다는 점을 고려부탁드립니다. 내 말이 틀렸을 수도,, 내가 무지했을 수도,,)

    ex. UPDATE /cats/:id

    - 200 : id에 해당하는 고양이 수정 완료
    - 401 : 인증받지 않은 사용자
    - 403 : 권한이 없는 사용자
    - 404 : id에 해당하는 고양이가 존재하지 않음
    - 500 : 서버 에러

    빌더 패턴을 사용한 이유

    정말 많은 디자인 패턴들이 있는데 나는 왜 하필 빌더 패턴을 사용했을까?

    앞서 말했듯이 코드 중복을 최소화하기 위해 기본적인 @ApiResponse()들을 자동으로 만들어줬다. 하지만 API별로 자동으로 만들어준 데코레이터가 필요하지 않을 수 있고, 다른 응답 데코레이터가 추가적으로 필요할 수도 있다.

    🙋‍♂️ : 해당 메서드는 POST라 200번이 아니라 201번이 필요한데요?
    🙋‍♀️ : 이 데코레이터엔 401번 응답이 필요없는데요?

    기본 데코레이터의 수정을 허용하기 위해 빌더 패턴을 적용했다.

    빌더 패턴
  • 객체 생성에 관한 디자인 패턴
  • 복잡한 객체를 단계적으로 생성
  • 객체 생성의 유연성을 높이고, 코드의 가독성을 향상시킴
  • // status에 해당하는 @ApiResponse() 추가
    add(response: SwaggerResponse): SwaggerDecoratorBuilder {
      this.response.set(response.status, this.makeApiResponse(response));
      return this;
    }
    
    // status에 해당하는 @ApiResponse() 제거
    remove(status: ResponseStatus): SwaggerDecoratorBuilder {
      this.response.delete(status);
      return this;
    }
    
    // 완성된 커스텀 데코레이터 반환
    build() {
      // 중략
      return applyDecorators(...decorators);
    }

    빌더 패턴을 적용함으로써 build()가 나오기 전까지 계속해서 데코레이터를 수정할 수 있다.


    데코레이터 파일을 작성해보자

    이제 SwaggerDecoratorBuilder로 Swagger를 위한 커스텀 데코레이터를 만들면 된다.

    커스텀 데코레이터에 파라미터를 넘기지 않는 것이 가장 깔끔하겠지만, 컨트롤러에서 param과 body를 넘겨주는 것이 유지보수에 좋을 것 같아 최소한의 파라미터를 허용하고 있다.

    🙋‍♂️ : param이나 body가 변경되면 데코레이터 파일을 찾아가서 수정해야 하나요? 너무 번거로운 것 같아요.
    // src/cats/cats.decorator.ts
    
    export const UpdateCatDecorator = (
      target: string,
      param: SwaggerParam,
      body: SwaggerBody,
    ) =>
      new SwaggerDecoratorBuilder(target, 'PATCH')
        .setParam(param)
        .setBody(body)
        .add({ status: 403, description: 'Forbidden - Unauthorized User' }) // 403 응답 오버라이드 가능
        .build(); // 완성된 데코레이터 반환

    target은 @ApiOperation()을 작성하는 데 사용되는 문자열로 리소스 대상을 의미한다. 내가 생성/수정/조회/삭제하려는 대상이 고양이인 경우, target은 고양이가 된다. target만 넘겨주면 내부 로직에 의해 자동으로 @ApiOperation()의 summary를 작성해준다.

     // src/cats/cats.controller.ts
     
      @Patch(':id')
      @UpdateCatDecorator(
        'Cat',
        { type: 'uuid', name: 'id', description: 'identifier of cat' },
        { type: UpdateCatDto, description: 'update dto of cat' },
      )
      update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {}

    Swagger 데코레이터를 바로 이용하는 것보다 훨씬 깔끔해진 것을 볼 수 있다.


    내가 만든 빌더, npm에 배포하기

    Swagger를 위한 빌더를 만든 것은 좋은 시도였지만, 매 프로젝트마다 해당 코드를 복사하여 사용하는 건 너무 비효율적이다.

    😑 : 프로젝트마다 swagger를 사용할 텐데 그때마다 빌더 코드를 작성해야 한다구요? 귀찮아요.

    위 문제를 해결하고자 npm에 패키지를 배포하기로 결심했다!

    npm에 패키지를 배포했을 때의 이점
  • 해당 코드를 다른 프로젝트에서 모듈화하여 재사용할 수 있다.
  • 코드의 유지보수성을 높인다.
  • npm이 의존성 관리를 자동화해주기 때문에 코드에 필요한 의존성들이 자동으로 해결된다.

  • 내가 작성한 코드와 패키지는 아래 링크에서 확인할 수 있다. 개인적으로 많이 사용하는 데코레이터에 대해서만 작성했기 때문에 아직 부족한 부분이 많다. 버그나 개선사항을 발견하신다면 깃헙의 메일이나 댓글 부탁드립니다. 많이 이용해주세요!!

    swagger-decorator-builder 소스코드
    swagger-decorator-builder 배포 사이트

    댓글