핵심 3줄 요약
1. Swagger를 통해 API 문서화를 쉽게 할 수 있다.
2. NestJS에서 사용할 경우 데코레이터(@로 시작하는 녀석)가 너무 많아 가독성이 떨어진다.
3. NestJS에서 제공하는 플러그인으로 데코레이터를 줄일 수 있다.
웹 프로젝트를 하다보면 프론트엔드와 백엔드 간에 의사소통이 정말 잦습니다.
그 중 대부분은 API에 대한 것이라고 확신합니다.
프론트엔드는 API를 빠르게 개발하지 않는 백엔드가 밉고, 백엔드는 기능 개발하느라 바쁜데 재촉하는 프론트엔드가 밉습니다. 바쁜 와중에 문서화까지 해야하니 정신이 나가버릴 것만 같습니다.
하나의 기능을 개발하면 해당 기능에 대한 모든 응답을 문서화해야 나중에 양측 API 양식이 맞지 않는 에러를 방지할 수 있지만, 이게 생각보다 굉장히 귀찮은 작업입니다.
그리고 백엔드도 사람인지라 문서화 도중 실수를 할 수 있고, 실수는 곧 에러가 되어 프론트엔드의 화를 돋굴 겁니다.
저도 이번 프로젝트를 진행하면서 처음 API 문서를 만든지라 실수가 많았고, 다행히 팀원들이 착해 별 탈 없이 넘어갔지만 반복되는 실수에 미안함이 컸습니다.
상황마다 응답 예시를 만들 수 있지만 모든 기능에 대해 만들어야 하기 때문에 그 양이 결코 적지 않습니다.
저의 경우 이번 프로젝트를 애자일 방식으로 한 주마다 스프린트를 진행했는데, 매주 첫날에 그 주 개발할 기능을 정하고 문서화를 한 뒤 개발을 시작하는 루틴을 잡았습니다.
프로젝트 절반쯤 되니 문서화를 하다 지쳐 2~3개씩 만들던 응답 예시들을 만들지 않기 시작했고, 자세한 응답 양식이 없으니 답답한 프론트엔드 쪽에서는 하나하나 물어보며 작업해야 하니까 프로젝트 진행이 더뎌지는 상황이 생겼습니다.
문제 의식을 느끼던 중, 코드 레벨에서 API 문서화를 할 수 있는 라이브러리가 있다는 것을 알게 되었고 곧바로 적용을 해보았습니다.
Swagger
Swagger 공식 문서는 다음과 같이 소개하고 있습니다.
Swagger offers the most powerful and easiest to use tools to take full advantage of the OpenAPI Specification.
Swagger는 OpenAPI 사양을 최대한 활용할 수 있는 도구라고 하는데, OpenAPI란 무엇일까요?
OpenAPI
OpenAPI Specification is an API description format for REST APIs.
위 소개에 따르면 OpenAPI 사양은 REST API에 대한 설명서라고 이해할 수 있습니다. YAML 또는 JSON으로 작성할 수 있으며, 다음과 같은 요소들을 정의할 수 있습니다.
- 사용 가능한 끝점(/users) 및 각 끝점의 기능(GET /users, POST /users)
- 기능 각각에 대한 인풋, 아웃풋
- 인증
- 추가적인 정보
또 Swagger 공식 문서에는 OpenAPI의 다양한 장점들을 말하고 있는데, 제가 느끼는 가장 큰 장점은 API 문서화의 간편함이라고 생각합니다.
Swagger 외에도 많은 API 문서화 도구들이 존재하지만 NestJS는 공식 문서에 OPEN API 도구로 Swagger를 소개하고 있기에 NestJS를 사용하는 입장에서 Swagger를 선택하게 되었습니다.
NestJS에서 Swagger 설정
우선 라이브러리를 설치해야 합니다.
npm install --save @nestjs/swagger
그리고 Swagger를 다음처럼 적용합니다.
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('Cats example')
.setDescription('The cats API description')
.setVersion('1.0')
.addTag('cats')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();
Swagger 적용 흐름은 크게 3가지 단계로 이루어집니다.
- 문서 설정
- 문서 생성
- 문서 적용
정말 간단합니다. 위 코드의 config, document와 setup()이 각 단계를 의미합니다.
title, version, description, tag가 각각 어떤 것을 나타내는지 간단하게 표시해봤습니다.
그리고 setup()의 첫 번째 인자로 '/api'를 입력했기 때문에 해당 문서의 경로는 /api가 됩니다. 따라서 localhost:3000/api로 접속하면 위 문서를 마주할 수 있습니다.
그리고 이 문서를 JSON이나 YAML 파일로 내려받을 수 있는데 JSON의 경우 localhost:3000/api-json에 접속하면 내려받을 수 있습니다.
문서 config을 설정할 때 다양한 옵션들을 줄 수 있지만 문서화를 하기 위한 용도로는 위 기본 설정만 알아도 충분하다고 판단하고 생략하겠습니다. 혹여 궁금하신 분들을 위해 링크를 첨부하겠습니다.
https://docs.nestjs.com/openapi/introduction#document-options
Swagger 적용은 끝났습니다. 이제 정교한 문서화를 위해 각 엔드포인트를 담당하는 컨트롤러에 Swagger를 설정해줄 일만 남았습니다.
DTO에 Swagger 매핑
NestJS는 고맙게도 데코레이터를 통해 간편하게 Swagger를 적용할 수 있도록 도와줍니다.
우선 Swagger 모듈이 모든 라우트 핸들러로부터 Body, Query, Param 데코레이터를 찾아 API 명세로 만듭니다. 다음 코드를 보겠습니다.
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
위 코드가 컨트롤러에 존재할 경우, 문서 하단에 다음과 같은 Swagger UI가 생성됩니다.
하지만 CreateCatDto에 속성이 존재해도 내부가 빈 것처럼 보이는데, 이를 표시하기 위해서는 해당 클래스에 @ApiProperty() 데코레이터를 붙여야 합니다. 혹은 CLI 플러그인을 적용할 수도 있는데, 이 방식은 다음 글에서 설명하겠습니다.
데코레이터는 다음처럼 붙입니다.
import { ApiProperty } from '@nestjs/swagger';
export class CreateCatDto {
@ApiProperty()
name: string;
@ApiProperty()
age: number;
@ApiProperty()
breed: string;
}
위처럼 데코레이터를 붙여주면 문서에 적용되어 속성이 보이게 됩니다.
위 데코레이터에는 다양한 옵션들을 설정함으로써 문서화를 더 세세하게 할 수 있는데, 아래 링크를 참고하여 적용하시면 되겠습니다.
https://docs.nestjs.com/openapi/types-and-parameters
컨트롤러에 Swagger 매핑
DTO에 매핑하는 법을 알아봤으니, 이제는 컨트롤러에 매핑하는 방법을 알아보겠습니다.
태그 매핑
우선 태그를 매핑할 수 있습니다.
@ApiTags('인증 API')
@Controller('auth')
export class AuthController {}
위처럼 @ApiTags 데코레이터를 컨트롤러에 붙여주면 다음처럼 그룹화하여 API들을 나타낼 수 있습니다.
응답
또한 응답에 대한 정보를 데코레이터로 나타낼 수 있습니다.
@Post('reviews')
@ApiResponse({
status: 201,
description: '리뷰 작성에 성공했습니다',
})
@ApiResponse({
status: 404,
description: '게시물 or 사용자가 존재하지 않습니다',
})
async write(@Req() req: Request, @Body() requestDto: ReviewWriteRequestDto) {
await this.reviewService.write(userId, requestDto);
}
위처럼 @ApiResponse 데코레이터에 인자로 상태 코드와 상세 설명을 넣어 응답 정보를 표현할 수 있습니다. 하지만 상태 코드가 숫자로 되어 있어 가독성이 좋지 않습니다.
위 코드는 다음처럼 개선할 수 있습니다.
@Post('reviews')
@ApiCreatedResponse({
description: '리뷰 작성에 성공했습니다',
})
@ApiNotFoundResponse({
description: '게시물 or 사용자가 존재하지 않습니다',
})
async write(@Req() req: Request, @Body() requestDto: ReviewWriteRequestDto) {
await this.reviewService.write(userId, requestDto);
}
위처럼 표현하면 상태 코드를 문자열로 나타낼 수 있어 훨씬 가독성이 좋아집니다. 위 코드는 문서에 아래처럼 표시됩니다.
위 코드에는 없는 400과 401에 대한 응답이 있는데, 이는 컨트롤러에 데코레이터를 적용하여 해당 컨트롤러 내의 모든 메서드에 적용했기 때문입니다. 다음처럼 쓸 수 있습니다.
@Controller()
@ApiTags('리뷰 API')
@ApiBadRequestResponse({ description: '잘못된 요청입니다' })
@ApiUnauthorizedResponse()
export class ReviewController {
constructor(private readonly reviewService: ReviewService) {}
// 생략...
}
그 외 파일 업로드 등의 기능을 적용할 수 있는데, 저는 사용해보지 않아서 자세한 사항은 공식 문서를 참고해주시기 바랍니다.
https://docs.nestjs.com/openapi/operations
인증
어떤 기능은 헤더의 Authorization 속에 액세스 토큰이 있어야만 동작하기도 하고, 쿠키에 토큰이 있어야만 동작하기도 합니다. Swagger는 이런 기능 또한 @ApiSecurity() 데코레이터를 통해 지원합니다.
@ApiSecurity('basic')
@Controller('cats')
export class CatsController {}
위 기능을 사용하기 전에 문서에 보안 정의를 추가해야 합니다.
const options = new DocumentBuilder().addSecurity('basic', {
type: 'http',
scheme: 'basic',
});
하지만 basic이나 Bearer같은 자주 사용되는 인증 기술은 내장되어 있기 때문에 @ApiSecurity로 매커니즘을 정의해주지 않아도 됩니다.
기본 인증
기본 인증은 다음처럼 쓸 수 있습니다.
@ApiBearerAuth('accessToken')
export class ReviewController {}
위 기능을 사용하려면 문서에 정의해줘야 하는데, 아래처럼 상세하게 정의해줄 수도 있습니다.
const config = new DocumentBuilder()
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
name: 'JWT',
description: 'Enter JWT Token',
in: 'header',
},
'accessToken',
).build();
인증 기능을 적용하면 문서에 인증을 할 수 있는 버튼이 생깁니다.
누르면 다음과 같은 화면이 나오고, 인증 정보를 입력할 수 있습니다.
쿠키 인증
쿠키 인증은 @ApiCookieAuth()로 활성화할 수 있습니다.
@Get('refresh')
@ApiCookieAuth('refreshToken')
refreshTokens(): RefreshTokensDto {
const id = req.user['id'];
// 생략...
}
위 코드는 저희 프로젝트에서 액세스 토큰을 갱신하는 기능인데 쿠키 속 리프레쉬 토큰 정보가 필요하기 때문에 쿠키 인증이 필요합니다. 쿠키 인증 또한 문서에 작성해야 합니다.
const config = new DocumentBuilder()
.addCookieAuth('refreshToken')
.build();
위는 단순하게 쿠키의 키값만 적어줬지만, 다른 값들도 위에서 봤던 BearerAuth처럼 적을 수 있습니다. 적용하면 다음처럼 쿠키 인증 칸이 추가됩니다.
위 기능을 간단히나마 보여드리면, 아무 인증 정보 없이 갱신을 요청하면 다음처럼 401 에러를 반환합니다.
이제 아까 봤던 자물쇠 버튼을 누르고 쿠키에 리프레쉬 토큰을 입력해줍니다.
그 후 Authorize 버튼을 누르면 다음처럼 쿠키에 리프레쉬 토큰이 저장됩니다.
그 후 요청을 하면 다음처럼 성공합니다.
문제점
대강 기본 기능들에 대한 설명은 끝났습니다. 보신 것처럼 Swagger는 몇 줄의 데코레이터로 간단하게 API를 문서화할 수 있는 아주 강력한 도구지만, 가장 큰 문제점이 적용을 망설이게 합니다.
데코레이터가 너무 많다.
어떻게 보면 Swagger의 데코레이터는 문서화를 위한 코드입니다. 이 코드들이 다른 NestJS 데코레이터들과 섞이면 다음처럼 우리를 굉장히 혼란스럽게 만듭니다.
@Post()
@UseGuards(AccessTokenGuard)
@HttpCode(HttpStatus.CREATED)
@ApiCreatedResponse({
description: '게시물 작성에 성공했습니다',
})
@ApiNotFoundResponse({
description: '해당 유저는 존재하지 않습니다',
})
@ApiUnauthorizedResponse()
async write(@Req() req: Request, @Body() writeDto: WriteDto) {}
모든 응답에 대한 정보를 표현해야 하다보니 데코레이터 3~4개쯤은 우습게 추가됩니다. 이러한 단점때문에 같은 기간 프로젝트를 하던 다른 캠퍼분들은 도입을 포기하기도 했습니다.
저희 팀도 처음에는 API 문서를 타입스크립트 코드로 관리할 수 있다는 간편함에 혹해 도입을 했지만, 막상 도입하고 보니 증식하는 데코레이터에 당황하여 도입을 포기할까 고민하기도 했습니다.
그래서 분명 방법이 있을 것이라 생각하고 구글링을 통해 정보를 모은 결과, 플러그인을 통해 데코레이터를 줄일 수 있다는 것을 알게 되었습니다.
플러그인
NestJS도 이런 Swagger의 문제를 인지하고 있는지, 플러그인을 제공하고 있습니다.
공식 문서의 DTO 코드를 예로 들어보겠습니다.
export class CreateUserDto {
@ApiProperty()
email: string;
@ApiProperty()
password: string;
@ApiProperty({ enum: RoleEnum, default: [], isArray: true })
roles: RoleEnum[] = [];
@ApiProperty({ required: false, default: true })
isEnabled?: boolean = true;
}
속성마다 데코레이터가 달려있는 것이 썩 보기 좋지 않습니다. 여기에 class validator의 데코레이터까지 달리면 코드보다 데코레이터가 많아지는 상황이 발생합니다.
위 코드에 플러그인을 적용하면 깔끔한 코드가 됩니다.
export class CreateUserDto {
email: string;
password: string;
roles: RoleEnum[] = [];
isEnabled?: boolean = true;
}
아무 데코레이터도 적용하고 있지 않습니다. 어떻게 이게 가능할까요?
플러그인의 동작 방식
우선 플러그인을 활성화해야 합니다. 활성화는 nest-cli.json 파일에서 설정할 수 있습니다.
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": [
{
"name": "@nestjs/swagger",
"options": {
"classValidatorShim": true,
"introspectComments": true,
"controllerKeyOfComment": "summary"
}
}
]
}
}
options는 명시하지 않으면 기본값으로 적용됩니다. 설정할 수 있는 옵션들은 다음과 같습니다.
옵션 | 기본값 | 설명 |
dtoFileNameSuffix | ['.dto.ts', '.entity.ts'] | DTO 파일 접미사 |
controllerFileNameSuffix | .controller.ts | 컨트롤러 파일 접미사 |
classValidatorShim | true | class-validator 데코레이터가 적용됩니다. |
dtoKeyOfComment | 'description' | 주석으로 ApiProperty 데코레이터의 특정 키를 대신 나타낼 수 있습니다. |
controllerKeyOfComment | 'description' | 주석으로 ApiOperation 데코레이터의 특정 키를 대신 나타낼 수 있습니다. |
introspectComments | false | 주석 기능의 사용 여부 |
저는 주석 기능을 활성화하고, class-validator 데코레이터가 적용되도록 했으며, 컨트롤러의 주석은 summary를 나타내도록 설정했습니다.
플러그인을 활성화하면 플러그인이 등록된 파일 접미사로 컨트롤러나 DTO, 엔티티를 식별하여 Abstract Syntax Tree를 기반으로 적절한 데코레이터를 추가합니다. 플러그인은 다음과 같은 문서화 정보들을 자동으로 추가합니다.
- 프로퍼티 타입에 따라 타입 설정
- 프로퍼티에 할당된 기본값 설정
- 프로퍼티에 ?가 있으면 required를 false로 설정
- 주석을 기반으로 @ApiProperty 또는 @ApiOperation의 특정 키값 설정
몇 가지 정보가 더 있지만, 제가 적는 것은 단순한 번역에 지나지 않기 때문에 자세한 사항은 공식 문서를 참고하시면 좋습니다.
https://docs.nestjs.com/openapi/cli-plugin
플러그인을 실제 적용한 코드
주석을 읽는다는 사실에 이 플러그인이 제대로 동작하는지 믿기 어려운 사람도 있을 거라고 생각합니다. 저도 실제로 주석을 적용하느라 약간 헤맸습니다.
실제 코드와 함께 설명하겠습니다. 우선 컨트롤러 코드입니다.
/**
* 검색조건에 맞는 최신 데이터를 가져옵니다
*/
@Get('posts/:postId/reviews')
@ApiOkResponse({ description: '요청에 성공했습니다' })
async getReviewsOfPost(
@Param('postId') postId: number,
@Query() requestDto: ReviewGetAllRequestDto,
) {}
주석이 적혀있는 것을 확인할 수 있습니다. 다음은 DTO 코드입니다.
export class ReviewGetAllRequestDto {
/**
* lastId를 기준으로 더 최신의 데이터를 반환합니다. lastId를 입력하지 않으면 가장 최신의 데이터를 반환합니다
*/
@IsOptional()
@Type(() => Number)
@IsInt({ message: '글 번호가 형식에 맞지 않습니다.' })
lastId?: number = -1;
}
저희 프로젝트의 리뷰 정보에 대한 DTO입니다. 주석이 있고, ?가 있으며, 기본값이 설정되어 있습니다. 이 코드가 문서화되면 다음처럼 나타납니다.
컨트롤러에 주석으로 적어줬던 문자열이 summary로 적용된 것을 확인할 수 있습니다. 앞서 설명드린 플러그인 옵션에서 해당 값을 변경할 수 있습니다.
또한 lastId를 보시면 설정한대로 잘 문서화된 것을 볼 수 있고, 주석이 description으로 적용된 것도 확인할 수 있습니다.
한 가지 명심해야 할 것은 주석은 반드시 다른 데코레이터들보다 위에 위치해야 합니다. 공식문서에는 적혀있지 않아서 저도 직접 몇 번의 테스트를 해보고나서야 알게 되었습니다.
확실히 데코레이터가 사라져서 가독성과 간편한 문서화, 두 마리 토끼를 모두 잡을 수 있게 되었습니다. 하지만 이렇게 편리한 플러그인도 약간의 문제가 있습니다.
플러그인의 문제점
결론부터 말하면 지원하는 문서화 옵션이 적습니다.
무슨 말이냐면 주석으로 데코레이터를 대신할 수 있지만 description이나 summary 중 하나의 키값만 대신할 수 있습니다. 그리고 이 정보도 공식 문서에 나와있지 않습니다. 직접 깃허브를 검색해서 찾아낸 정보입니다.
https://github.com/search?q=controllerKeyOfComment&type=code
따라서 다양한 설명이 포함된 문서를 작성하려면 어쩔 수 없이 데코레이터를 다시 사용해야 합니다. 이 기능을 지원해달라는 이슈가 NestJS Swagger 공식 레포지토리에도 존재하지만 2022년 12월 8일 기준 아직 적용되지 않은 것 같습니다.
https://github.com/nestjs/swagger/pull/1207
하지만 데코레이터가 줄어든다는 것만으로 코드의 가독성이 훨씬 좋아지기 때문에 NestJS에 Swagger를 사용할 예정이라면 무조건 플러그인을 활성화하기를 추천드립니다.
결론
NestJS에서 Swagger를 사용할 때 플러그인을 활성화해서 데코레이터를 없앨 수 있다.
'프로젝트 > 웹' 카테고리의 다른 글
(CICD) 어디 가서 내가 깃허브 액션 만들었다고 말하지 마라~ (1) | 2022.12.15 |
---|---|
(CICD) CICD 개선 일대기 - 3. 깃허브 액션 캐시로 node 빌드 속도를 높여보자 (0) | 2022.12.12 |
(CICD) CICD 개선 일대기 - 2. 깃허브 액션의 중복 코드를 없애보자 (0) | 2022.12.11 |
(CICD) CICD 개선 일대기 - 1. 깃허브 액션과 슬랙을 연동시켜보자 (0) | 2022.12.11 |
(NestJS) TDD를 하며 배운 것들 (1) | 2022.11.24 |