Template Library라는 이름으로 새로운 시리즈의 글들을 작성하고자 합니다. 우선 Template Library가 무엇인지 말씀드리고자 합니다.
개인적 또는 업무적으로 크고 작은 프로젝트를 진행하다 보니 일부 기술 세트에 대해서는 기본이 되는 부분을 공통화해서 정리해두면 좋을 것 같다는 생각이 들었습니다. 이렇게 잘 정리해두면 이를 참고하여 새로운 프로젝트를 시작할 때 기준으로 사용할 수도 있겠다 싶었습니다. 이를 위한 방법으로 막연하게 시간이 나면 Todo List를 구현해서 정리하자고 생각만 하고 있을 때쯤 Todo list 만들기는 이제 그만이라는 글에서 RealWorld라는 것을 알게 되었습니다. 간단하게 CRUD만 검증하는 것이 아닌 회원, 인증, CRUD 등 실제 프로젝트를 진행하는 중에 필요한 부분들에 대해서 포괄적으로 검증할 수 있는 부분이 Todo List보다 매력적으로 다가왔습니다. 게다가 명확한 API 스펙까지 제공하고 있으니 이거다 싶었습니다.
이런 생각을 하고 이런저런 이유로 실행에 옮기지 못하고 있을 때 Nest.js에 대한 검증이 필요했고 이에 다시 필요성을 느끼게 되어 이를 동기로 삼아 Template Library라는 새로 생성한 저장소를 만들고 진행하였습니다.
Template Library의 용도를 Nest.js 검증뿐 아니라 다른 복수의 클라이언트 및 서버를 검증할 수 있는 환경으로 만들고 싶었습니다. Go 혹은 Kotlin 등 새로운 언어를 학습하게 되면 RealWorld의 스펙에 맞추어 구현하고 이를 띄워서 확인하는 공간으로 생각하였습니다. 이를 위해서 각 언어에 따른 폴더를 생성하고 그 하위에 구현하고 공통으로 사용하는 부분을 루트에 모아두었습니다.
.
├── README.md
├── db
├── docker-compose.yml
├── node
└── schema
이번 검증을 진행하면서 하나 더 구성해보고 싶었던 부분은 클라이언트와 서버 간의 공통 데이터 모델에 대한 스키마를 공유하고 싶었습니다. Protocol Buffers는 클라이언트에서 사용하기 불편했던 경험이 있었고 egaoneko/stocker와 egaoneko/unimark에서 구성한 방식인 Interface를 만들고 이에 대한 일련의 Use Case와 Repository를 만들어 공유하는 방식은 너무 장황하기도 하고 Node에 한정되는 부분이 있어서 다른 방법에 대해 고민해보았습니다.
.
├── client
├── core
└── server
이번에는 Json Schema를 작성해서 각 언어에 맞추어 클라이언트와 서버에서 공유할 인터페이스 혹은 클래스를 생성하는 방식으로 생각하고 진행하였습니다. 이번에 Node를 가장 먼저 진행하게 되어 Lerna로 구성하고 core 라이브러리에서 루트의 Schema를 보고 Interface를 생성하고 빌드하면 다른 package들에서는 core를 사용하는 방식으로 구현하였습니다.
Json Schema로 TypeScript를 생성하기 위하여 사용한 json-schema-to-typescript에서는 다른 파일에서 참조한 파일을 import
를 통해 참조하는 방식이 아닌 인라인으로 작성되어 Json Schema를 한 파일로 구성하지 않으면 불필요한 중복 코드가 발생하여 declareExternallyReferenced
설정을 끄고 직접 import
를 파일에 주입해주는 방식으로 처리하였습니다. 관련된 내용은 schema.js에서 확인하실 수 있습니다.
이전에도 Nest.js에 대해서는 들어봤지만, Angular와 유사한 아키텍처를 보고는 선입견을 품고 의도적으로 배제하고 있었습니다. Angular의 전반적인 구성이나 잘 구성된 아키텍처는 만족하며 사용했었으나 Monkey Patch로 인한 무거웠던 기억으로 인해 Nest.js도 무겁고 느릴 것이라는 생각을 하고 있었습니다. 하지만 이번에 직접 사용해보면서 느낀 것은 express에 비해 크게 무거운 부분도 없었고 이를 지원하는 전반적인 라이브러리 생태계도 생각 이상으로 좋아서 이런 생각들을 깰 수 있었습니다.
특히 express를 사용하여 여러 사람과 협업하면서 어려웠던 부분은 시간이 지남에 따라 부족한 기능을 보충하기 위해 필요에 따라 사람들이 생각하는 다양한 아키텍처가 녹아들어 유지 보수하기 어렵다는 것이었습니다. 이를 위해 계속 논의하며 공통 아키텍처를 잡고 이를 위한 기능을 만들다 보면 이를 유지보수 하는 것 또한 상당한 노력이 필요했고 그렇다고 만족할 만큼 충족이 되는 것도 아니었습니다. Nest.js를 사용하면 이런 부분은 Nest.js에 맡겨두고 좀 더 본질적인 부분에 집중할 수 있었습니다.
Nest.js나 Angular를 사용할 때 좋았던 부분은 기본적으로 가이드되는 구조가 있어 이를 따라가며 추가적으로 필요한 부분만 정하면 된다는 것입니다. CLI를 통해 생성되는 구조에서 필요한 모듈을 생성하고 공통 모듈인 shared
모듈을 두는 방식으로 구현하였습니다. Angular와 동일하게 CLI에서 폴더 구조를 반영해주기 때문에 모듈을 modules
와 같이 묶어서 구성해도 문제가 없어서 필요에 따라 구성하면 될 것 같습니다.
.
├── all-exceptions.filter.ts
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── article
├── auth
├── config
├── health
├── integration.spec.ts
├── main.ts
├── profile
├── shared
├── test
└── user
이전에 프로젝트를 진행해보면서 shared
나 auth
와 같이 공용 모듈과 다른 모듈간의 상호 참조로 순환 참조가 일어나는 부분이 발생하고 이런 부분이 유지 보수에 불편함을 준다고 느껴왔습니다. 얼마전에 읽었던 ArchUnit - UnitTest로 아키텍처 검사를라는 글을 보고 이에 해당하는 TypeScript 라이브러리인 ts-arch를 사용하여 integration.spec.ts
에서 테스트를 수행할 때 상호 의존 관계에 대해서 확인하여 수정할 수 있도록 추가하였습니다.
개별 모듈은 CLI를 통해 생성하고 대부분 Nest.js에서 많이 사용하는 폴더 구조와 파일 이름을 따라가도록 작성하여 쉽게 작성할 수 있었습니다. dto
의 response
객체들은 core에서 정의한 interface
구현하도록 작업허여 타입 체크를 받도록 하였습니다. 전반적으로 Nest.js 가이드에 추가적으로 스프링에서의 경험을 바탕으로 작성했습니다.
.
├── decorators
├── dto
├── entities
├── interfaces
├── repositories
├── user.controller.spec.ts
├── user.controller.ts
├── user.module.ts
├── user.service.spec.ts
└── user.service.ts
테스트 또한 Nest.js에서 기본적으로 CLI를 통해 생성되는 파일을 바탕으로 작성해 나가면 되서 편하게 작성할 수 있었습니다. 테스트는 Jest를 사용한 단위 테스트와 SuperTest를 통한 E2E 테스트를 작성하였습니다. 이런저런 시도 끝에 필자는 다음과 같은 기준을 가지고 작성하였습니다.
초기에는 1, 2번애 해당하는 테스트에서도 개별로 더미 데이터를 주입하여 진행하였는데 4번에서 유사한 검증을 하게되어 최대한 테스트 부담을 줄이는 방향으로 진행하였습니다.
지난 3주간 시간이 날때마다 Nest.js를 사용하여 RealWorld 스펙을 구현하면서 느낀점은 “어떻게 구조를 잡아야 할지에 대한 고민은 많이 덜어내고 최대한 스펙에 집중할 수 있었던 것 같다”는 점입니다. 기존에 사용하던 Memory 설정에서 OOM이 발생한 것을 보면 Express보다 조금 더 무거운 것 같았지만, 이를 상쇄할 정도로 생산성 측면에서 좋은 느낌을 주어서 당분간은 만족하며 주력으로 사용할 수 있을 것 같다는 생각이 들었습니다.