들어가기 전에
새로 들어가는 프로젝트에서 생산성과 코드 품질을 중요한 기준으로 삼았다. 또한 프론트와 백엔드 상호간의 원할한 피드백이 필요한 상황이기에 하나의 통일된 JS기반의 언어로 프로젝트를 시작하는 것이 좋겠다는 의견이 있었다.
https://github.com/Fingoo-org/Fingoo
여태까지 JAVA 기반의 Spring 프레임워크만 사용했던 나는 꽤나 고민됐다. 하지만 위와 같은 이유와 함께 문득 새로운 기술이 필요한 시점이라면 왜 이 기술을 사용해야하는지, 어떤 장점이 있는지, 또한 익숙한 언어를 포기한 만큼의 생산성을 보장할 수 있을지 알아보는 것이 필요하다고 생각했다.
그러던 중 배달의 민족에서 발표한 '우아콘 - 새로운 백엔드 개발 표준' 을 보게 됐고, 이를 기반으로 한번 생각을 정리해보려 한다.
https://youtu.be/Z0d7ZrxY-i0?si=1IzGFEmcNnwntR2Z
우아한 형제들에서 NestJS를 새로운 백엔드 개발 표준으로 삼은 이유
배민 역시 새로운 개발환경에 대한 목표가 안정성과 생산성에 있었다.
자바 스프링 환경의 단점을 보완하면서도 자바 스프링 만큼의 단단한 코드를 유지하고 빠르게 서비스를 개발하고 운영할 수 있는 환경이 필요했다.
나는 이 대목에서 NestJS로 빠르게 서비스를 개발하고, 운영할 수 있다면 충분히 도전해볼 가치가 있다고 생각했다.
추가로 express나 여러 다른 기술이 존재하는데, NestJS의 어떤 점 때문에 새로운 표준으로 삼기로 했는지 알아보자
TypeScript
약타입 언어인 JS에 정적 타입 시스템을 도입한 것이 타입스크립트(TS)이다.
TS는 코드 작성 단계에서 정적 타입으로 관리할 수 있도록 하여, 코드 레벨에서의 문제를 조기에 발견할 수 있다는 장점이 있다.
하지만, 런타임 레벨에서는 여전히 JS로 동작하기 때문에 타입이 엄격하게 관리되는 것은 아니다.
그러므로, 사용하는 프레임워크에서 TS를 완벽하게 제공하지 않거나 외부에서 전달받은 request나 DB 같은 서드파티 저장소로부터 전달받는 데이터의 타입이 명확하게 관리되지 않는다면 런타임에서는 코드와 다르게 다른 타입으로 동작할 수 있다는 문제가 있다.
이는 반대로, 외부에서 전달받은 데이터의 타입만 정확하다면 자바와 같은 강타입 언어처럼 단단한 코드를 유지할 수 있게 된다는 말이기도 하다.
NodeJS
NodeJS는 이벤트 루프를 통해 시스템 커널에 작업을 넘겨서 싱글 스레드 임에도 불구하고 non-blocking IO 작업을 손쉽게 수행할 수 있도록 디자인되어있다.
현대 kernel 환경은 멀티 스레드이기 때문에 백그라운드에서 다수의 작업을 실행할 수가 있고, 이러한 작업 중 하나가 완료되면 kernel이 NodeJS에게 알려주어 적절한 callback을 큐에 추가함으로써 실행되는 구조를 가진다.
이러한 방식은 단일 스레드 기반으로 동작하기 때문에 CPU 집약적인 작업에는 적합하지 않지만, 많은 I/O 작업을 처리하는데에는 멀티 스레드에 비해 작은 자원을 사용하여 많은 양을 처리할 수 있도록 합니다.
배민의 경우는 NodeJS는 멀티스레드의 CPU 집약적인 처리에 능숙한 자바 스프링 환경과는 정반대의 장단점을 가졌기 때문에 서로 가진 장점과 단점을 보완해 줄 수 있었고, 앞에서의 명확한 특성을 통해 프로덕트를 만드는데 선택할 수 있었다고 합니다.
이런 특징은 요청이 많고 가벼운 RestAPI 환경에서는 굉장히 높은 효율을 달성할 수 있도록 된다.
이번 프로젝트의 특성상 많은 API 요청이 존재할 것이고, 추가적으로 파이썬 모델 서버와 요청을 주고 받아야하기 때문에 우리 프로젝트에 더욱 적합하다는 판단이었다.
추가로 NodeJs는 많은 I/O 작업을 처리하는데 능숙하기 때문에 웹 환경에 적합하고, 웹 환경에서 사용할 수 있는 프레임워크 중 가장 사람들에게 많이 알려진 프레임워크는 express일 것이다.
express가 아닌 NestJS를 선택한 이유를 간단히 정리해보겠습니다.
1. 높은 자유도가 주어지지만, 협업과 시스템의 확장 가능성을 생각할 때 충돌과 컨벤션 통합의 비용이 발생한다.
2. TS를 부분적으로 적용할 수 있지만, 공식적으로 지원하고 있지는 않다.
3. 보완, DB 로깅, 캐싱, 메세징, DI, validation를 선택해서 사용할 수 있지만, 버전 관리, 라이브러리 만료의 문제가 남아있다.
NestJS
Nest는 Spring 프레임워크와 마찬가지로 Opinionated 프레임워크이다.
Opinionated 프레임워크는 엔지니어의 작성 패턴을 강제하는 대신 다양한 기능을 제공하는 완전 관리형 프레임워크이다.
이미 아키텍쳐 결정에 의해 잘 다져진 경로를 제공하기 때문에, 예측 가능하고 일관되며, 대규모 인원에 관리되고 개발되더라도 이해하기 쉬운 코드를 가진다는 특징이 있다.
그러나 Spring과 마찬가지로 프레임워크의 단계의 목적과 요구사항이 일치하지 않을 경우, 프레임워크 레벨에서의 수정이 필요하게 되고 Opinionated 프레임워크의 경우 강제된 규칙을 벗어나기 어렵다는 규칙이 있다.
express와 같은 Un-Opinionated 프레임워크의 경우 목적에 맞게 새로운 기능을 추가할 수 있다는 장점이 있지만, 이때마다 수많은 시작코드와 테스트, 최적화와 검증이 필요하다는 단점이 있다.
Spring과 NestJS의 비슷한 점
Spring에 익숙한 제가 본격적으로 NestJS를 사용하게 된다면 알아둘 내용들입니다.
NestJS의 경우 OOP, FP, FRP라는 부분에서 철학적이나, 기능적인 부분이 많이 닮아있는 것을 알 수 있다.
또한 이 글 끝의 reference를 보면 알겠지만 spring과 유사한 구조를 가지고 있다는 것을 알 수 있다. 때문에 spring에 익숙했던 엔지니어가 nestjs에 큰 러닝커브 없이 적응할 수 있는 것이다.
또한 객체를 생성하고 관리하는 IoC Container나 Layered 아키텍쳐, orm을 사용한 repository 패턴과 같은 내용들이 모두 NestJS에 적용되어있다.
NestJS만의 특징
Middleware로 시작해서 필터로 끝나는 NestJS 만의 라이프사이클은 하나의 요청에 대해서 각 단계별로 관리할 수 있게 되어있다.
복잡한 비지니스 로직을 단계별로 나누어 관리할 수 있게 되는 것이다. 이렇게 단계별로 로직을 분리하면 코드의 가독성과 유지보수성을 높여줄 수 있다.
Validation
추가적으로, TypeScript의 단점 중 외부 데이터 타입에 대한 검증이 필요하다는 것을 NestJS에서는 Validation을 제공하면서 해결했다.
아래의 사진은 클래스 validation과 클래스 트랜스포머를 통해 제공되는 validation은 요청하는 request에 대해서 검증한 내용이고 추가적인 내용은 아래의 링크를 참고하면 된다.
Modular Layered Architecture
https://docs.nestjs.com/modules#feature-modules
NestJS는 Modular Layered Architecture를 기본으로 사용하고 있어서, 어플리케이션 내에서 모듈 단위로 도메인과 서비스를 분리하고, 모듈 내에서 레이어를 분리함으로써 확장에 유리하면서 복잡한 비지니스를 쉽게 관리할 수 있도록 합니다.
이러한 모듈 단위의 관리는 라이브러러리 또한 모듈 단위로 관리할 수 있어서 다양한 형태의 모듈들이 준비만 되어있다면 별도의 개발 없이 쉽게 추가함으로써 안정적이고 빠르게 요구 사항을 구현할 수 있게 됩니다.
Transport Layer (Microservice)
https://docs.nestjs.com/microservices/basics
단순히 HTTP만 통신하는 어플리케이션이 아닌 메시지 큐나 gRPC 소켓 I/O 뿐만 아니라 자신이 구성하는 다양한 형태의 통신 프로토콜을 쉽게 적용할 수 있도록 되어있다.
즉, Transport Layer가 변경될 수 있다는 것은 request나 response가 비지니스 로직까지 전달되지 않고 명확하게 통신 레이어에서 관리된다는 것을 의미한다. 이 덕분에 비지니스 로직이 트랜스포트 레이어의 영향을 받지 않게 되고 결과적으로 하나의 비지니스 로직을 다양한 통신 프로토콜을 통해 처리할 수 있다는 것이 된다.
트랜스포트 레이어의 변경이 아니더라도 서버가 부팅할 때 또는 명령을 내려 데몬을 작동시키며 데몬은 백그라운드에서 요청을 기다렸다가 요청이 들어오면 혼자서 처리하는 방식인 stand alone 방식으로 동작하는 람다 형태의 또한 제공할 수 있게 되어 있습니다.
우아한 형제들의 백엔드 개발환경
저의 고민과는 먼 얘기지만, 개인적으로 해당 영상을 정리하면서 새로운 내용을 공부하는 느낌으로 정리해봤습니다.
최소품질 체크 리스트
서비스하고 운영하면서 배운 규칙이나 설정, 누군가 겪었던 문제를 사전에 방지하고자 최소 품질을 지키기 위한 체크리스트이다.
이런 최소품질 체크 리스트 역시 대부분 자바 Spring으로 구성되어있었기 때문에 NestJS로 새로운 개발환경을 구성하면서 새로운 최소품질 체크 리스트와 함께 엔지니어가 전사 개발 환경과 설정들을 쉽게 지키고 적용하는 부분을 고민했다고 한다.
즉, 가이드 문서를 확인하고 체크리스트를 통해 점검할 뿐만 아니라 별도로 찾아보고 확인하지 않아도 최소 품질을 지킬 수 있는 방법을 고민하게 되었으며, 하나의 도메인을 공유하는 상태에서 여러 형태의 서비스를 제공하기 위한 구조 또한 고민 중에 하나였다고 한다.
아래의 링크는 해당 내용에 대한 고민을 기존에 정리해둔 글이다.
https://techblog.woowahan.com/2637/
프레임워크 레벨에서 다양한 동작 형태가 하나의 코드 기반으로 운영할 수 있다면 하나의 코드 베이스이기 때문에 이러한 의존 관계가 사라지게 될 수 있다.
NestJS 기반으로 공통 템플릿을 제공하게 된다면 엔지니어가 전사 표준 가이드를 하나씩 확인하지 않아도 전사 표준 환경을 지키면서 개발을 할 수 있지 않을까? 라는 생각을 하셨다고 한다.
비지니스 영역과 템플릿 영역을 분리하고, 우형만의 NestJS Boilerplate를 완성하게 되었다.
템플릿에서는 HTTP나 gRPC로 동작하는 어플리케이션 서버외에 Kafka와 같은 메시지 Queue 기반으로 동작하는 워커 형태와 핸들러로 불리는 람다 형태를 기본으로 제공한다.
모듈과 라이브러리를 관리할 디렉토리 구조부터 kafka 설정, 로깅, 등 비지니스 로직을 제외한 설정과 규칙들을 템플릿에 포함함으로써 최소 품질 체크리스트에서 가이드하는 항목과 NestJS 개발 환경에서 추가로 지켜야할 규칙들을 엔지니어가 자연스럽게 지킬 수 있도록 한 것이다.
즉, 다양한 사람들이 함께 개발할 때 코드 품질을 최소한 보장하는 역할을 수행할 수 있는 것이다.
여기까지 왜 NestJS를 사용하는지 알아봤다. 결론적으로 보자면 서비스에 안정성과 생산성이 필요했고, 이를 위해서는 안정적인 환경이 필요하다. 그리고 안정적인 개발 환경을 위해서는 전체 개발 생명주기에서 예측 가능하고, 가이드 되어있다는 것을 의미한다. 이러한 안정성과 생산성의 고민의 결과들로 우아한 형제들에서는 NestJS를 새로운 백엔드 개발 표준으로 삼게 되었다.
참고자료들
NestJS 초기 설정
https://velog.io/@fcfargo/Nest.js-초기-세팅
https://velog.io/@sujeong7474/01.-Nest.js-프로젝트-초기-세팅하기
TypeScript 핸드북
https://typescript-kr.github.io/pages/unions-and-intersections.html
nestjs TypeORM
https://velog.io/@___pepper/Nest.js-TypeORM-사용하기
https://any-ting.tistory.com/113
https://velog.io/@fcfargo/Nest.js-typeORM-설정
nestjs+serverless
https://jeounpar.tistory.com/19
nestjs TypeORM
https://velog.io/@dev_leewoooo/TypeORM에서-Transaction을-이용해보기
nestjs API 작성
https://www.daleseo.com/nestjs-rest-api/
NestJS 테스트 코드 - Jest 사용