본문 바로가기
백엔드 개발

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

by iamjoy 2023. 6. 1.

개념

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