NestJS 사용 소감

Ju Dohyun
NAVER Pay Dev Blog
Published in
9 min readNov 14, 2022

네이버파이낸셜에서 BE 개발을 하고있는 페이플랫폼 주도현입니다.

2009년 Node.js 가 발표되면서 이전에는 jQuery 정도로만 대표되던 Javascript 생태계가 급속도로 발전하기 시작했습니다. 빠른 개발 속도를 앞세운 높은 생산성을 강점으로 지금은 브라우저의 영역을 넘어서 Server-Side 환경에서도 활발히 사용되고 있습니다. 이제는 (CPU intensive 한 기능이 필요하지 않은) 어플리케이션 DB 정도와 연계한 일반적인 백엔드 API 기능을 제공하는 용도로는 다른 플랫폼과 비교해도 장점이 많은 수준까지 발전했습니다.

제가 속한 개발팀에서도 주로 Koa + Typescript 조합을 사용한 백엔드 서버를 구축하여 여러 사용자향 서비스 및 API 서버를 제공하고 있습니다.

Koa 의 높은 자유도와 간결함은 비교적 작은 기능 서버를 빠르게 개발하는데 강점이 있습니다. 하지만 팀에서 개발하는 어플리케이션의 기능이 점점 늘어나고 복잡해지기 시작하면서 높은 자유도는 점점 코드 스타일, 구조의 파편화를 발생시기도 하여서 코드 베이스의 컨벤션을 정하고 유지하기 위한 고민과 시간을 늘어나게 하는 단점이 두각을 나타내기 시작했습니다.

심플한 구조와 기능은 러닝커브가 적고 빠르게 적응할 수 있게 해주었지만 한편으로는 도메인에 좀 더 집중할 수 있도록 일반적으로 많이 쓰는 기능들에 대해서는 미리 제공되었으면 하는 요구도 점점 생겨났습니다.

이런 문제로 고민하면서, 대안으로 NestJS 프레임워크를 검토하였습니다. 그동안 필요성이 느껴지던 부분들에 대해 도움을 받을 수 있을 것으로 기대되어서 검토 끝에 Koa 를 사용하고 있었던 프로젝트들 몇 개를 NestJS 로 전환을 진행했습니다. 이 글에서는 실제 NestJS 로 프레임워크를 전환하고 사용해보면서 느꼈던 점들에 대해서 가볍게 이야기 해보고자 합니다. (참고로 작업시점에 @nestjs/core 8.x.x 버전에서 주로 개발을 진행했습니다.)

NestJS 로 전환하며

처음 NestJS 의 예제코드를 봤을때 자바 어노테이션과 비슷한 데코레이션들로 장식된 클래스와 DI 를 통해 의존성을 주입받아 사용하는 형태는 기존의 Node.js 웹 프레임워크들의 모습과는 많이 달랐습니다. 오히려 Java 스프링 프레임워크와 상당히 비슷하다는 생각이 들었습니다.

십여년전 두꺼운 토비의 스프링 책을 보면서 공부했던 기억이 있어서 NestJS 도 그정도의 러닝커브가 필요한 건가 싶었지만, 실제 기능을 차근차근 익혀가면서 기존 프로젝트를 완전히 전환해보니 개인적으로 NestJS 자체의 러닝커브는 우려했던 것보다 크진 않았습니다.

다만 선택해서 사용하는 기반 서버 플랫폼(Express 나 Fastify)에 대한 지식이 어느정도 필요하고, NestJS 에서 기본적으로 사용하는 의존 라이브러리들(rxjs, class-validator, jest 등)에 대한 지식도 가지고 있어야 사용이 수월합니다. 대부분에 대해 지식이 없는 상태라면 러닝커브가 생각보다 클 수 있겠습니다.

저같은 경우는 이미 Koa 를 사용해 개발되어 있는 프로젝트를 전환하는 것이라 Koa 의 Middleware 로 구현되어 있는 코드들을 NestJS 의 Middleware, Guard, Interceptor, Controller 등으로 옮기는 작업이 많았습니다. 프레임워크의 구조가 상당히 다르기 때문에 변경량이 꽤 클것으로 예상되었고, 구조적으로 많은 변경에 의한 사이드 이펙트가 생기지 않도록 고려를 해야 했습니다.

전환전 대상 프로젝트의 API 들에 대한 자동화된 e2e 테스트와 단위 테스트가 어느정도 구축되어 있는 상태라서 한번 더 테스트들을 보강한 후 전환을 진행했습니다. 자동화된 테스트 스위트의 존재는 전환 개발간 도움을 꽤 받을 수 있었고 결과적으로 사이드 이펙트 없이 전환 작업을 완료할 수 있었습니다. 세상에 실수하지 않는 사람은 없으니 사람의 꼼꼼함만을 믿으면 100% 확률로 실수가 발생합니다. 실제 저도 전환 개발간 실수를 했고, 테스트 스위트가 제 실수를 몇 번이나 알려줘서 인지할 수 있었습니다. :-)

Koa 는 모든걸 Middleware 하나로 구현합니다. 이런 단순함은 장점이자 단점이라고 할 수 있습니다. 프로젝트의 규모가 커지기 시작하면 파일의 네이밍 컨벤션을 정해서 역할을 구분하는 방법 정도밖에는 없습니다. NestJS 에서는 Controller, Pipe, Guard, Interceptor, Middleware, Exception Filter 등 목적에 따라 기능과 구조를 좀 더 세분화해 놓았습니다. Module 을 통해서는 어플리케이션을 기능 단위로 응집도있게 구조화 할 수 있도록 지원합니다.

NestJS 의 구조에 맞춰서 코드를 개발해보면 자연스럽게 소스 파일의 수나 코드량은 기존보단 조금 늘어나게 되는 것 같습니다. 제공해주는 CLI 기능을 활용하면 컴포넌트 생성, 모듈로의 자동 등록, 테스트 스켈레톤 코드 생성 등 자동화의 도움을 받을 수 있습니다. 생산성에 영향을 받을 정도로 코드량이 늘었다는 느낌은 크게 들지 않았습니다.

모듈을 이용해 기능 단위로 컴포넌트를 구분해 놓으면 Class 단위의 접근 제어자가 없는 JS 환경에서 코드 캡슐화나 무분별한 의존관계를 제어하는데 도움을 받을 수 있습니다. 또한 기능이 커져감에 따라서 복잡도가 높아지지 않도록 관리하는데도 도움이 됩니다.

DI 기능을 통해 의존성을 주입받게 작성된 Class 들은 테스트시 Mock 주입이 수월한 구조가 되어 의존성과 분리해서 격리된 테스트를 작성하기가 수월해 졌습니다.

이러한 특징들 모두 프로젝트의 유지보수성을 높히는데 도움을 주는 부분들이기 때문에 프로젝트가 커질수록 장기적으로 혜택이 더 커지게 됩니다.

한가지 확실히 해야할 건 NestJS 를 사용한다고 반드시 좋은 설계 구조의 어플리케이션이 만들어지는 것은 아닙니다. 예를 들면 Module 하나에 모든 Controller, Provider 를 다 등록해놓고 사용해도 Module 의 장점을 제대로 활용하지 못하는 것이긴 하겠지만 문제는 없습니다. 요청 인입시 전처리 로직을 Guard 로 구현할 수도 있고, Interceptor 로 구현할 수 도 있습니다.

고민없이 그냥 되는대로 구현하더라도 기능은 동작합니다. 전환 개발을 하면서 NestJS 를 새롭게 익히는 시간만큼 NestJS 를 활용해서 기존의 코드 구조를 어떻게 더 나은 구조로 변경할 수 있을까 고민하는 시간도 꽤 많이 들었습니다. NestJS가 제공하는 편의 기능들과 구조들을 활용해서 최선의 구조, 최선의 설계를 만들어 내는 것은 결국 개발자의 능력과 고민에 달려있는 영역입니다.

그래도 NestJS 의 구조는 어느정도 규모 이상의 어플리케이션을 개발할때 자잘한 부분들에 대한 고민을 없애고 편리함을 제공받을 수 있으므로 단순한 구조의 Koa 같은 프레임워크와는 차별되는 점이라고 할 수 있습니다.

기본적으로 제공해주는 내장 로거와 Jest, SuperTest 를 이용한 테스트 구조를 기본적으로 제공해주는 것도 편의성 측면에서 좋은 것 같은데 이번에 전환한 프로젝트에서는 둘 다 사용하지는 않았습니다.

로거는 기존에 사용하던 것을 커스텀 로거 세팅을 활용해 그대로 사용 가능했습니다.

테스트 프레임워크로는 이미 Mocha + Chai 조합을 사용하고 있었습니다. 이전에 테스트 프레임워크로 Jest 를 사용하다가 자주자주 실행하는 단위 테스트 특성상 조금 더 빠른 실행속도를 가지는 Mocha 로 전환을 했었기 때문에 다시 Jest 로 변경하지는 않았습니다. Jest 와 Mocha 의 테스트 코드 문법이 거의 비슷해서 CLI 의 코드 생성 기능으로 어느정도 도움은 받을 수 있었지만 테스트 프레임워크를 선택할 수 있도록 해줬으면 더 좋았을 것 같습니다.

NestJS 로의 전환 후 만족스러웠던 기능 중 하나는 Swagger 제공이 아주 쉽다는 것이였습니다. Bootstrap 파일에 몇 줄의 설정 추가만으로 바로 제공이 가능하고, CLI Plugin 기능을 활용하면 요청/응답 class 에 @ApiProperty() 같은 데코레이터 추가 없이도 dto, entity 클래스들을 인식해 스키마를 자동 생성해 줍니다. 개발한 서버에서 제공하는 API 명세를 간편하게 Swagger 로 제공이 가능해져서 개인적으로 만족도가 아주 높았습니다.

아직 완벽하지 않았던 부분

NestJS로 전환하면서 기반 웹 프레임워크를 Fastify 로 선택해 사용했을때 문제를 좀 겪었습니다. 아직은 좀 더 발전이 필요하겠단 생각이 들었던 부분입니다. Express 와 비교해서 상대적으로 Fastify 에 대한 지원은 아직까진 부족하게 느껴졌습니다.

  • Fastify 사용시 Express 를 사용할때와 다르게 설정해야 하는 부분들이 있습니다. 설정이 다른 부분들은 대부분 공식문서에서 설명이 나오기는 하지만 설정이 다른 그 부분만 간략하게 나와있습니다. 대부분은 Express 기준으로 문서가 쓰여져 있어 Fastify 를 사용할때는 불편한 부분이 있었습니다.
  • Fastify 사용시 Middleware 를 구현하면 use 메서드에 넘어오는 req, res 가 각각 FastifyRequest, FastifyReply 객체가 아닌 NodeJS의 IncomingMessage, ServerResponse 객체가 전달됩니다. Interceptor 에서는 FastifyRequest, FastifyReply 객체가 전달되어 동작에 일관성이 없어 헷갈립니다. Express 에서는 일관되게 Request, Response 객체가 전달됩니다.
  • 예외 발생시 ExceptionFilter 에서 에러 페이지를 렌더링해서 응답하도록 구현해야할 요건이 있었습니다. Fastify 사용시에는 res.render() 가 동작하지 않았습니다. Express 사용시에는 문제없이 동작했습니다.

특별한 기능이 필요하지 않은 REST API 만을 구현하는 경우에는 Fastify 기반으로도 큰 문제는 없었습니다. 하지만 웹 페이지를 렌더링 응답해야 한다거나 조금 특수한 기능이 필요한 경우에는 아직까지는 Express 기반으로 사용하는 것을 추천합니다.

그래도 환영할만한 프레임워크

개인적으로 새로운 프레임워크나 언어, 기술들을 접하게되면 항상 생각나는 격언이 있습니다.

은탄환은 없다

항상 들어맞는 기술이나 해결책은 없다는 것인데, 상황마다 다른 적절한 선택만이 존재하기 때문에 가장 중요한 것은 개발자로서 최선의 선택을 할 수 있는 안목과 실력을 기르는 것 같습니다.

물론 NestJS도 은탄환은 아닙니다. 실제 사용해보고나니 아직 완벽하다고는 할 수 없었지만 Node.js 플랫폼 상에서 어느정도 규모가 있는 백엔드 어플리케이션 개발을 고려하고 있다면 지금 시점에서도 좋은 선택지인 것은 분명한 것 같습니다.

앞으로의 발전이 더 기대되는 프레임워크입니다. Node.js 백엔드 개발 환경에 좋은 선택지가 하나 더 늘어난 것 같습니다. :-)

--

--