리팩토링 포스트모템

M4r()·2022년 12월 8일
0
post-thumbnail

들어가며

제가 현재 참여 중인 서비스는 iOS용 어플리케이션을 위한 서버로 Typescript 기반의 Nest.js 프레임워크를 이용 중이고, API는 GraphQL을 사용 중입니다. Apollo GraphQL을 이용 중이죠.

리팩토링은 원래 시간을 들여서 하는 게 아니라 중간 중간 진행해야 한다고 들었습니다만, 이번에는 Schema first로 되어있는 GQL 코드를 code first로 바꾸는 것을 제안받았는데 이 부분은 점진적으로 바꿀 수 있는 부분이 아니라서 시간을 들여서 리팩토링을 진행하게 되었습니다. 덤으로 이전에 만들어진 코드들을 훑어 보면서 레이어가 분리되어 있지 않던 몇몇 서비스의 레이어를 정리하는 시간을 가졌습니다.

레이어 정리

레이어 정리는 한꺼번에 할 필요는 없지만, 가끔 레이어가 전혀 나뉘어져 있지 않은 모듈들이 있어 보이는 대로 조금씩 정리해나갔습니다.

저희 서버 아키텍쳐는 기본적으로 레이어드 아키텍쳐입니다.

GraqhQL로 요청을 받고 응답을 보내는 Resolver 객체들이 Presentation layer를 구성하고,
요청에 따라서 데이터를 처리하는 Service 객체들이 Business layer를 구성하고,
typeORM을 이용해 데이터를 Postgres DB에 저장하는 Repository 객체들이 Persistance layer를 구성하는 비교적 단순한 구조로 되어있습니다.

레이어 정리는 크게 3 단계로 나누어서 진행했습니다.

code first로 resolver 정리하기

GQL 문단에 정리한 대로 resolver 객체를 생성하면 GQL을 위한 schema가 자동으로 생성되게 변경하였습니다.

GQL에 대해 이러저러한 정보를 찾아봤지만 제가 느끼기에 서버 측에서 가장 중요한 것은 Type 시스템을 이용해서 클라이언트에게 보낼 정보를 정의한다는 점이라고 생각 되었습니다.

이러한 목표를 달성을 위해 schema first로 접근해서 어떤 타입을 주고 받을지 GraphQL SDL을 이용해 스키마부터 작성하건, code first로 type들을 작성하건 근본적인 차이는 없다고 생각하지만, 제 경우엔 code first를 사용하는 것이 클라이언트와 서버 사이에 객체를 주고 받는 다는 것으로 이해하기 편했던 것 같습니다.

각 서비스의 Resolver를 정리하면서 정리를 하면 좋겠다 생각한 부분 중에 제가 예전에 만든 Community를 관리하는 Resolver제 listCommunityStatus라는 쿼리가 눈에 띄었습니다.

특정 커뮤니티에 특정 연도, 주차에 몇 개의 글이 올라왔나를 담아서 보여주는 CommunityStat같은 객체를 반환하게 되어있었습니다만, 지금이라면 Community를 관리하는 객체라면 Community 객체를 반환하게 해주는 방향으로 정리를 하고 싶어서 지금이라면 이런 식으로 만들 것 같습니다.

type Query{
	listCommunity(communityID:number, searchStart?: Date, searchEnd?: Date): Community;
}

type Community {
	communityID: number,
	communityName?: string,
    weeklyStatus?: WeeklyStat[],
    ...
};

type WeeklyStat {
	year: number,
	weekNumber: number,
	numberOfPost: number
};

쿼리가 바뀌면 클라이언트 측에서도 변경사항이 생겨나는데, 클라이언트 개발을 담당하는 분들은 다른 작업으로 바빴기 때문에 이번에는 이런 식의 정리는 하지 못 했지만, 앞으로 디자인 하는 쿼리들부터 조금씩 바꿔나가려고 합니다.

dto 작성하기

presentation layer에서 사용할 DTO를 작성하였습니다. Nest.js와 관련된 글을 찾다보면 DTO에 데이터 검증을 추가하는 경우가 많아 저도 Class-validator를 이용한 검증을 추가하고 whitelist를 설정해, 의도하지 않은 값이 들어오는 경우를 막았습니다.

import { Field, ID, Int, ObjectType } from '@nestjs/graphql';
import { IsBoolean, IsDateString, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';

@ObjectType()
export class Community {
  @Field(() => ID, { description: 'Community의 Id, uuid v4' })
  @IsNotEmpty()
  @IsUUID()
  id: string;

  @Field({ description: 'Community의 이름' })
  @IsNotEmpty()
  @IsString()
  name: string;

  @Field({ description: 'Community의 설명', nullable: true })
  @IsOptional()
  @IsString()
  description?: string;

  @Field({ description: 'Community의 대표 icon', nullable: true })
  @IsOptional()
  @IsString()
  icon?: string;

  @Field(() => Boolean, { description: 'Community 비공개 설정' })
  @IsNotEmpty()
  @IsBoolean()
  isPrivate: boolean;

  @Field(() => Int, { description: 'Community의 회원 수' })
  @IsNotEmpty()
  @IsInt()
  memberCount: number;

  @Field({ description: 'Community를 생성한 사용자 ' })
  @IsString()
  @IsNotEmpty()
  createdBy: string;

  @Field(() => Date)
  @IsDateString()
  @IsNotEmpty()
  createdAt: Date;

  @Field(() => Date)
  @IsDateString()
  @IsNotEmpty()
  updatedAt: Date;
}

Business layer에서 사용할 타입 새로 만들기

모듈중에는 가끔 레이어분리가 전혀 안 되어있는 모듈들이 존재했는데, 심한 경우에는 persistance layer에 해당하는 entity가 presentation layer에 해당하는 Gql schema에 선언 된 type을 가져다 쓰는 경우도 존재했습니다. 💩

이런 코드들은 경험상 무언가를 수정하면 그 수정사항이 굉장히 광범위하게 퍼지고, 오류 또한 걷잡을 수 없이 커지기 때문에 수정이 어려웠기 때문에 보이는대로 분리를 해주었는데요, 분리를 하면서 DTO의 사용 범위를 어디까지로 한정해야 하는가에 대해 한참 고민도 하고, 자료도 찾아봤지만 무언가 시원한 답은 나오질 않아, Resolver에서 사용하는 DTO와 Service에서 사용할 DTO는 또 새로운 객체를 만들어 관리하는 방향으로 했습니다.

제가 이렇게 나눈 걸 보고 한 친구가 Resolver에서 사용하는 DTO는 검증기능이라도 있지, service에서 쓰는 객체는 아무런 기능이 없던데요 라는 질문을 받았는데, JAVA 개발자인 친구는 원래 DTO는 data transfer object니까 데이터를 담는 것 말고 다른 기능이 있는게 더 이상한 거 아니냐라고 해서 고민은 많이 되었습니다만... 🧐

DTO의 정의와 사용범위에 대해서는 시원하게 결론을 내리지 못했지만 사용해보고 유지보수에 유리한 방향으로 고쳐나가기로 했습니다.

마치며

방금 리팩토링을 진행한 코드를 커밋하고, 그 커밋이 gitaction을 통해 배포된 것을 확인 한 참입니다. 커밋한 코드가 자동으로 배포될 수 있는 시스템을 준비해 두었으니 작성한 코드를 커밋하고 확인하는 사이클이 더 짧게 진행되었으면 훨씬 좋았겠지만, 저 혼자서 서비스 간에 얽혀있는 의존성까지 분리하기에는 시간도 경험도 실력도 부족했던 것 같아 아쉬움이 남습니다.

리졸버들을 정리하려고 파일을 열어봤더니 레이어 분리가 안 되어있는 부분도 많아 서비스들을 정리 했는데 이게 가장 고민도 많이 되고 손도 많이 갔던 것 같습니다. 또한 RESTful 시절에 사용하던 엔드포인트들이 남아있어 이걸 정리하고 싶었는데 쿼리를 변경하면 클라이언트 측의 변경도 필요하고 워낙 변경사항이 많은 범위에서 이루어져야 하기 때문에 깔끔하게 정리할 수는 없었던 점은 아쉽기도 합니다만, 지금 고친 부분도 나중에 보면 또 이상하다고 느끼는 부분이 생길테니 최대한 다른 팀에 영향이 가지 않는 방향으로 해보려고 했습니다. 이래서 deprecated 시켜놓고 남겨놓은 코드들이 많구나 하는 생각도 했고요.

또한 글을 쓰면서 제가 한 고민들을 조금 더 다른 사람들이 알기 쉽게 남기고, 보기 좋게 꾸미는 방법도 고민해봐야겠다고 생각했습니다. 텍스트 분량에 비해 그닥 도움이 되지 않는 글이 된 것 같아 이 글 자체에도 아쉬움이 많이 남습니다.

참고

https://graphql.org/learn/
https://docs.nestjs.com/techniques/validation
https://betterprogramming.pub/how-to-use-data-transfer-objects-dto-for-validation-in-nest-js-7ff95309f650
https://velog.io/@pixelstudio/nestjsgraphql-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0-%EC%A0%95%EB%A6%90
https://velog.io/@jujube0/nestjs-graphql-typeorm-%EC%8B%A4%EC%A0%9C-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0-resolver-%EB%A7%8C%EB%93%A4%EA%B8%B0

profile
달리려고 해야 걸을 수 있다.

0개의 댓글