백엔드 개발

#037. 리팩터링: one-size fits all API 만들기

iamjoy 2023. 6. 1. 11:50

개념

Restful API는 하나의 엔드포인트에 하나의 스키마를 내려주도록 한다. 그런데 해당 엔드 포인트를 쓰는 플랫폼이 많아지거나 요구사항이 늘어나면서 문제가 생긴다. 모두에게 맞는 엔드 포인트, 즉 모든 정보를 가진 엔드 포인트를 만들거나 (one-size fits all), 매번 비슷한 필드를 내려주면서 각 플랫폼, 디바이스, 수정된 요구사항에 따라 API 엔드포인트를 계속 만들어야하는 작업을 해야한다. 이러한 문제 의식에서부터 나온 것이 graphQL인데, 이를 도입하는 것은 큰 작업이고 새로운 기술을 도입하는 데에는 시간이 필요하다. 가능한 기존의 RESTful 한 엔드 포인트를 유지하면서 클라이언트가 서버에서 원하는 것을 다시 선택하도록 수정해보려고 한다.

적용

요구사항은 아래와 같다.
기존 API에서 3가지 정도의 필드를 내려주고 있는 데 클라이언트가 이 중에 원하는 필드를 선택해서 가져가도록 하려고 한다.
기존 API 에 ?include={원하는 필드} 에 쿼리문을 추가해 구현했다.

사실 그에 맞는 API를 생성하는 것도 한 가지 좋은 방법이다. 미리 배포해놓기도 좋다. 하지만 이렇게 비슷한 API가 많아지면 유지보수도 어려워지고 코드 중복도 높아지게 된다. 이런 장단점을 고려해봤을 때 이번에는 새로운 API 보다 호환성을 지키며 기존의 API를 수정해보려고 했다. 

선택할 수 있는 필드는 USER, MENTOR, META 그리고 모든 정보를 받는 ALL 이다.
받을 수 있는 필드를 enum 타입으로 만들면 들어올 수 있는 필드들을 관리하는 데 좋을 것 같아서 enum 타입으로 만들었다.
그리고 들어오는 필드가 all 또는 특정 필드인 경우 응답값에 포함시키는 매서드를 추가했다.

import { Enum, EnumType } from 'ts-jenum';

@Enum('code')
export class PartialType extends EnumType<PartialType>() {
static readonly USER = new PartialType('user');
static readonly MENTOR = new PartialType('mentor');
static readonly META = new PartialType('meta');
static readonly ALL = new PartialType('all');

constructor(private readonly _code: string) {
    super();
}

static isAll(value: PartialType) {
    return value.code === PartialType.ALL.code;
}

static isUser(value: PartialType) {
    return this.isAll(value) || value.code === PartialType.USER.code;
}

static isMeta(value: PartialType) {
     return this.isAll(value) || value.code === PartialType.META.code;
}

static isMentor(value: PartialType) {
     return this.isAll(value) || value.code === PartialType.MENTOR.code;
}

get code(): string {
    return this._code;
}
}

그리고 이런 값들을 쿼리로 받을 수 있도록 include 값을 받았다.

export class Request {
    @ApiProperty({ required: false, type: Boolean })
    @IsOptional()
    @ToEnum(PartialType)
    include: PartialType;
}

다양한 필드들이 들어올 수 있는데 모든 경우의 수를 어떻게 구현할 지 고민하다가 빌더 패턴을 이용하기로 했다.
우선 defulat로 아무 필드도 포함하지 않은 response 를 만들고

const response = exampleResponse.withEmpty();

여기에 들어오는 필드에 따라 값을 추가하도록 설정했다. 예를 들어 meta 정보를 알려달라는 쿼리가 들어오면 해당 정보를 추가하고 멘토 정보를 알려달라는 쿼리가 들어오면 해당 정보를 추가해서 내려줬다. 

if (PartialType.isMeta(include)) {
    response._reviewA = course.reviewA;
    response._reviewC = course.reviewC;
    response._studentC = course.studentC;
    response._courseC = course.courseC;
}

if (PartialType.isUser(include)) {
    response._user = UserDto.by(user, hasR);
}

if (PartialType.isMentor(include)) {
    response._mnformation = m ? MDto.by(mentor) : undefined;
}

 

다음과 같이 쿼리에 따라 원하는 값을 조회할 수 있다.

여러 값을 받도록 설정하면 다음과 같이 원하는 값을 array 형태로 넣어 전달할 수 있도록 할 수 있다.
아래는 user 와 mentor를 요청해서 해당 필드만 받도록 수정한 결과이다.
굉장히.. graphQL 스럽다.. 📈📉📊

참고
https://philcalcado.com/2019/07/12/some_thoughts_graphql_bff.html