들어가기 전...
이 글은 스웨거 작성에 대한 튜토리얼이 아니다.
취준생 백엔드 개발자가 초기에 스웨거를 사용했을 때부터 최근에 사용하기까지 어떤 개선점이 있었는지 공유하기 위해 작성하였다.
이제 막 스웨거를 사용했다면 도움이 될 수도 있을 것이다!
첫 개선 - 반환값 명시 방식 변경
위 방식은 처음으로 백엔드를 공부하고 진행한 팀 프로젝트에서 사용하던 스웨거 작성 방식이었다. (약 1년 6개월 전, 사진 속 코드는 그다음 프로젝트 초기까지 적용되었던 코드다.)
첫 프로젝트에 스웨거 도입 파트도 아니다 보니 처음 적용 했을 때에는 이렇게 사용하는 게 당연한 줄 알았었다. (왜 그랬을까? 담당했던 친구도 최근에 해당 코드를 보고 내가 왜 그랬지?라고 했다는 사실)
그러나, 다음 프로젝트에서 위 스웨거를 통해 반환값을 명시 및 변경하는데에 드는 리소스가 커지며 관리의 어려움을 느끼기 시작했다.
얼른 해결해보자!
Custom SwaggerBuilder와 반환값에 사용하는 DTO 반환값에 대한 스키마를 DTO를 통해 제공
import { ApiProperty } from '@nestjs/swagger';
import { LecturePassTargetDto } from '@src/common/dtos/lecture-pass-target.dto';
import { LecturePassDto } from '@src/common/dtos/lecture-pass.dto';
import { Exclude, Expose, Type } from 'class-transformer';
@Exclude()
export class IssuedPassDto extends LecturePassDto {
@ApiProperty({
type: Number,
description: '판매 수량',
})
@Expose()
salesCount: number;
@ApiProperty({
type: [LecturePassTargetDto],
description: '패스권 적용 대상',
})
@Type(() => LecturePassTargetDto)
@Expose()
lecturePassTarget: LecturePassTargetDto[];
}
import { HttpStatus, Type, applyDecorators } from '@nestjs/common';
import { ErrorHttpStatusCode } from '@nestjs/common/utils/http-error-by-code.util';
import { ApiExtraModels, ApiProperty, ApiResponse } from '@nestjs/swagger';
export class PaginationResponseDto {
[key: string]: unknown;
static swaggerBuilder(
status: Exclude<HttpStatus, ErrorHttpStatusCode>,
key: string,
type: Type,
) {
class Data {
@ApiProperty({
type: Number,
})
private readonly totalItemCount: number;
@ApiProperty({
type,
name: key,
isArray: true,
})
private readonly temp: string;
}
class Temp extends this {
@ApiProperty({
name: 'status',
example: `${status}`,
enum: HttpStatus,
})
private readonly status: string;
@ApiProperty({
name: 'data',
})
private readonly data: Data = new Data();
}
Object.defineProperty(Temp, 'name', {
value: `${key[0].toUpperCase()}${key.slice(1)}ResponseDto`,
});
Object.defineProperty(Data, 'name', {
value: `${key[0].toUpperCase()}${key.slice(1)}DataDto`,
});
return applyDecorators(
ApiExtraModels(type),
ApiResponse({ status, type: Temp }),
);
}
constructor(res: { [key: string]: unknown }) {
Object.assign(this, res);
}
}
우리가 반환값이나 요청 Body에서 명시하는 DTO에 @ApiProperty()를 사용하면 따로 example 없이 반환값을 보여 줄 수 있다. ( ApiProperty 안에서 example를 사용하여 자세히 보여 줄 수도 있다.)
DTO를 전달받아 각 API에 맞는 스웨거를 만들 수 있으며, 우리 서비스에서 요구하는 반환 값 형식을 제공해 줄 수 있다.
해당 빌더는 페이지네이션 반환값을 수월하게 만들어 주는 스웨거 빌더다.
기본적으로 totalItemCount와 data의 타입을 배열로 지정하여 휴먼 에러를 최소화해 준다.
key를 받아 DTO를 만드는 이유?
스웨거는 폴더의 위치에 따라 이름이 같다고 해서 다른 파일로 인식하지 않는다. 만약 각각 다른 위치의 구조도 스웨거 이름이 UserDto라면, 가장 늦게 읽힌 UserDTO의 구조가 모든 UserDto에 적용되기 때문에 Key를 받아 중복을 없앤다.
또한 이 프로젝트는 반환값을 response.body에 data가 있는 것이 아닌 한 꺼풀 더 추가를 원해서 key를 통해 한 꺼풀 만드는 과정이 포함되어 있다.
아래 버전으로 만들면 response.body를 통해 data에 접근할 수 있다.
import { HttpStatus, Type, applyDecorators } from '@nestjs/common';
import { ErrorHttpStatusCode } from '@nestjs/common/utils/http-error-by-code.util';
import {
ApiExtraModels,
ApiProperty,
ApiPropertyOptions,
ApiResponse,
} from '@nestjs/swagger';
export class CommonResponseDto {
static swaggerBuilder(
status: Exclude<HttpStatus, ErrorHttpStatusCode>,
key: string,
type: Type,
options: Omit<ApiPropertyOptions, 'name' | 'type'> = {},
) {
class Temp extends this {
@ApiProperty({
name: 'statusCode',
example: `${status}`,
enum: HttpStatus,
})
private readonly statusCode: number;
@ApiProperty({
name: 'data',
type,
...options,
})
private readonly data: string;
}
Object.defineProperty(Temp, 'name', {
value: `${key}ResponseDto`,
});
return applyDecorators(
ApiExtraModels(type),
ApiResponse({ status, type: Temp }),
);
}
}
두 번째 개선 - 컨트롤러 가독성 향상
두 코드를 봤을 때 어떤 코드가 조금 더 파악하기 쉬워 보일까?
나는 오른쪽이 더 보기 쉬워 보인다.
왼쪽 같은 코드가 많아질수록 인간이기에 헷갈리거나 서로 다르게 이해할 수도 있을 것이다.
이때마다 스웨거 코드에서 summary, description을 확인 또는 서비스 코드에서 파악해야 한다. (서비스 코드를 보지 말자는 말은 아니다. 이 코드가 어떤 코드인지 알고 보는 것과 모르고 보는 건 큰 차이가 있다고 생각한다. 또한 메서드 명 만으론 모든 의도를 전달하기 힘들다.)
오른쪽 방식은 스웨거에서 사용하는 summary를 통해서 해당코드가 어떤 코드인지 주석 같은 역할을 제공해 준다. 물론 description도 사용 가능하다.
개선 내용
컨트롤러에 정의되어 있는 메서드의 key와 Swagger의 OperationObject 중 summary를 필수 인수로 사용하게 하는 ApiOperator을 생성해준다.
import { ApiOperationOptions } from '@nestjs/swagger';
export type ApiOperator<T extends string> = {
[key in Capitalize<T>]: (
apiOperationOptions: Required<Pick<ApiOperationOptions, 'summary'>> &
ApiOperationOptions,
) => PropertyDecorator;
};
export const ApiPass: ApiOperator<keyof PassController> = {}
이후 생성할 Api의 객체를 만들면 해당 컨트롤러의 메서드를 Key로 사용하여 Api의 메서드를 갖게 된다.
만약 PassController에 tistory()를 추가한다면
ApiPass는 PassController의 메서드들을 Key로 사용하기 때문에 tistory가 없다고 울부짖을 것이다.
이를 통해 스웨거를 까먹고 추가하지 않는 상황을 막을 수 있다. 족쇄 +1
이제 원하는 기능을 구현해 준다.
ApiPass에 GetMyIssuedPassList를 추가해 주고 우리가 원래 사용했던 방식대로 작성해 주면 세팅이 끝이 났다! 이러면 사용이…. 안된다! 객체의 메서드를 선언한 것뿐이다..!
선언한 ApiPass객체의 GetMyIssuedPassList를 호출한다. applyDecorators을 통해 반환값을 return 하기 때문에 데코레이터로 호출이 가능하다.
인수로는 summary를 받는다. 상단에서 summary를 필수로 선언했기 때문에 summary사용을 강제시켜 해당 Api가 어떤 기능을 하는지 명시해 줄 수 있다.
앞 구르기를 하면서 봐도 뒷구르기를 하면서 봐도 변경 후 코드가 더욱 기능을 명확하게 알 수 있다.
또한 Api마다 스웨거 데코레이터가 추가되었는데 이를 한 파일에서 관리할 수 있게 되었다.