NestJS를 사용한 서버 개발을 하는 회사가 점점 늘어나면서 Spring과 NestJS의 차이점에 대해서 궁금해하실 분들이 계실 것 같습니다. 오늘은 NestJS로 커리어를 시작해서 현재는 Kotlin & Spring 프로젝트까지 개발 중인 저의 주관적인 의견을 소개해 드리려고 합니다.


강력한 모듈 구조

NestJS는 Angular 프레임워크의 영향을 받아서 Module-Controller-Service 아키텍처 패턴을 따릅니다. Root Module 내부에서 거의 위의 그림처럼 정형화된 구조의 각 모듈로 구성되게 됩니다. 이는 비교적 자유롭게 아키텍쳐를 구성할 수 있는 Spring에 비해서 엄격하다고 느낄 수 있습니다. 이런 강제성이 마음에 안 드는 사람도 있을 것 입니다. 하지만 NestJS의 탄생 배경 자체가 NodeJS 진영에 체계화된 아키텍쳐를 제공하기 위함이기 때문에 엄격한 그 맛에 사용한다고 볼 수 있습니다.

그러한 이유로 NestJS는 모듈을 생성 시 커맨드 명령어를 제공합니다. 만약 Spring을 먼저 접한 개발자라면 NestJS 실습 중 Nest CLI 가 신선하게 느껴질 수 있습니다. 제가 위의 users 모듈을 생성할 때 마우스는 일절 사용하지 않고 아래의 코드를 이용해서 생성했습니다.

nest g module users
nest g controller users --no-spec
nest g service users --no-spec


애플리케이션 초기화 방식과 의존성

애플리케이션이 부팅되고 구성되는 방식에서도 두 프레임워크는 차이를 보입니다.

Spring Boot는 하나의 거대한 ApplicationContext를 중심으로 모든 Bean을 관리합니다. 시작과 동시에 Classpath 및 컴포넌트를 스캔하여 필요한 빈을 등록하고 Auto Configuration를 통해 개발자가 명시적으로 설정하지 않아도 트랜잭션, 보안, DB 연결 등의 기능을 자동으로 구성해 줍니다. 즉, Spring은 프레임워크가 주도적으로 거대한 환경을 한 번에 구성합니다. 초기화 제어 역시 @PostConstructApplicationReadyEvent를 통해 빈 생성 직후나 컨텍스트 초기화 완료 시점에 유연하게 개입할 수 있습니다.

NestJS는 Angular의 영향을 받은 철저한 모듈 기반 구조를 따릅니다. 초기화 과정은 루트 모듈(AppModule)에서 시작하여 의존성 그래프(Dependency Graph)를 따라 하위 모듈들을 탐색하고 조립하는 방식으로 진행됩니다. 개발자는 모듈 간의 imports 관계를 명시적으로 정의해야 하며 NestJS는 이 설계도를 바탕으로 순차적으로 인스턴스를 생성합니다. OnModuleInit, OnApplicationBootstrap과 같은 라이프사이클 훅을 통해 모듈별로 초기화 로직을 제어할 수 있는 점은 Spring과 유사합니다.

요약하자면 Spring은 거대한 컨텍스트를 프레워크가 한 번에 구성하는 방식이고, NestJS는 명시된 설계도에 따라 블록을 쌓아 올리듯 단계적으로 애플리케이션을 완성해 나가는 방식이라 할 수 있습니다. 이때 개발자가 느끼는 차이점은 설계도를 직접 만드느냐에 있습니다. NestJS는 개발자가 의존성 관계를 Module에 명시해야 합니다. Spring을 사용하다가 넘어오신 분들은 번거롭다고 느끼실 수 있는 포인트입니다. Node에는 ClassLoader가 없기 때문에 개발자의 공수가 조금 더 필요합니다.


프레임워크와 생태계의 성숙도

Spring은 역사가 오래된만큼 생태계의 완성도를 그대로 누릴 수 있다는 장점이 있습니다. Spring Batch, Spring Security, Spring Cloud Gateway 등 공식으로 지원하는 다양한 모듈을 사용할 수 있습니다. 러닝 커브는 다소 있지만 직접 구현하는 것보다 더 안정적이고 쉽게 복잡한 기능을 도입할 수 있습니다.

지원하는 기능이나 레퍼런스는 Spring이 NestJS보다 더 많습니다. 하지만 NestJS도 빠르게 성장하고 있습니다. 그리고 노드 진영의 오픈소스 생태계가 활발하기 때문에 필요한 기능을 npm, yarn 같은 패키지 매니저 기반으로 유연하게 조합하여 사용할 수 있었습니다.


개발 언어의 차이

두 프레임워크를 사용할 때 가장 와닿는 차이는 바로 Kotlin과 TypeScript라는 언어의 차이였습니다. 가장 크게 느꼈던 차이점 3가지만 정리하겠습니다.

1. 멀티스레드 + Blocking I/O vs 단일스레드 + Non-Blocking I/O

Kotlin과 Typescript(또는 JS)는 각각 JVM과 Node(주로 V8)를 기반으로 작동합니다. JVM 은 블로킹I/O, 멀티스레드로 동작하고 Node는 논블로킹I/O, 싱글스레드로 동작한다는 차이점이 있습니다. 이 때문에 처음 아키텍처를 구상할 때 고려하게 되는 포인트에서 차이점이 존재합니다.

Kotlin으로 작업한 프로젝트는 멀티 스레드를 이용한 병렬 처리가 가능하기 때문에 하나의 인스턴스에서 단일 프로세스를 사용합니다. 프로세스가 늘어나면 메모리 효율이 줄어들기 때문입니다. 또한 멀티 스레드를 이용하기 때문에 메모리 가시성이나 스레드 동기화를 고려해야하고 컨텍스트 스위칭 비용 등을 고려해서 적절한 스레드 개수를 지정해줘야 합니다. 또한 블로킹I/O이기 때문에 별도의 비동기 함수없이 직관적으로 코딩을 할 수 있습니다. 비동기 프로그래밍이 필요한 경우에만 스레드나 코루틴을 사용해주면 됩니다.

반면에 Typescript 프로젝트의 경우에는 단일 스레드 기반이기 때문에 대부분 멀티 프로세스를 사용하게 됩니다. 그래서 Node 진영에서는 PM2라는 툴을 거의 필수적으로 사용하게 됩니다. Typescript는 논블로킹I/O라는 특징처럼 비동기 프로그래밍을 용이하게 할 수 있습니다. 대신 비동기 함수들이 존재하기 때문에 해당 함수들에 대한 숙지가 필요하고 코딩을 하면서 비동기 흐름을 계속 고려하며 작업하게 됩니다.


2. 타입 지정의 유연성

두 언어 모두 정적 타이핑 언어인 것은 맞습니다. 하지만 사용하면서 그 정도에는 차이점이 있습니다. (물론 TS를 사용한다는 가정입니다. JS는 동적 타이핑이죠.)

먼저, Kotlin은 Java와 마찬가지로 강력한 정적 타이핑 언어입니다. 타입 검사 시에도 명목적 타이핑 (Nominal Type System)을 사용하기 때문에 두 클래스가 동일한 필드를 가지고 있어도 서로 다른 타입으로 간주됩니다. 그리고 대부분 사람들은 직관적으로 명목적 타이핑이 자연스럽다고 느낍니다.

하지만 Typescript는 구조적 타이핑 (Structual type system)을 사용하기 때문에 두 객체가 동일한 구조를 가지면 동일한 타입으로 간주됩니다. 타입의 이름은 Alias처럼 취급되는 것이죠.

즉, Typescript는 타입간 호환성이 좋고 Kotlin은 엄격한 타입 검사가 가능합니다. 무엇이 더 좋은지는 프로젝트의 요구사항마다 다르겠지만, 개인적으로 TS는 몸이 편하고 Kotlin은 마음이 편합니다…

Null 안전성 역시 Kotlin이 더 강력하게 보장하고 Typescript는 선택적으로 안전성을 보장합니다. 때문에 Typescript의 자유도가 더 높게 느껴집니다.


3. 비동기 함수 사용법

비동기 프로그래밍을 할 때에도 두 언어는 문법이 다릅니다. 키워드의 호칭만 다르고 동작 흐름이 같다면 편하겠지만 아쉽게도 그렇지 않습니다.

Typescript는 async, await 키워드를 사용하여 비동기 함수를 지정하고 호출 시 비동기로 호출할지 동기적으로 호출할지가 결정됩니다. 비교적 단순한 문법으로 직관적으로 받아들이기 쉽습니다. 단, await을 사용하지 않으면 Promise 객체를 반환하기 때문에 실수하는 경우가 생길 수 있습니다.

Kotlin은 러닝커브가 비교적 더 높습니다. 비동기 프로그래밍 시 코루틴을 사용하며 suspend, launch, async, runBlocking 등의 키워드를 사용합니다. suspend로 비동기 함수를 생성하여 코루틴 스코프에서 사용하게 됩니다. 코루틴 스코프는 suspend 함수를 동기적으로 실행하며 비동기 실행을 위해서는 launch, async 내부에서 실행시켜야 합니다.


다른 차이점은?

요청 흐름에 따른 라이프 사이클이 다르다는 점이 있습니다. NestJS가 역할에 따른 Layer를 Guard, Pipe, Interceptor 등으로 더 세부화한 느낌이 있습니다. 또한 패키지 관리에 있어서 노드 진영은 npm, yarn 등 CLI 기반의 매니저가 존재하고 JVM 진영은 Gradle, Maven을 사용하기 때문에 직접 명시해주는 차이점이 있습니다.

지금까지 여러가지 차이점을 이야기했지만 사실 두 프레임워크는 큰 틀에서는 굉장히 유사한 점이 많아서 언어만 숙지된다면 프레임워크를 익히기에는 어렵지 않다고 느끼실겁니다. 제 동료분들도 생각보다 비슷한 점이 많아서 전환하기 쉬웠고 큰 차이를 체감하지 못했다고 했습니다.



그동안 서로 다른 언어와 프레임워크를 동시에 다루면서 새로운 동기부여와 소소한 재미가 있었습니다. 개발자에게 프레임워크나 언어처럼 도구의 사용은 비교적 마이너한 포인트이고 결국 중요한 것은 설계하는 능력과 새로운 것을 습득하는 능력이라는 것도 다시금 깨달았던 것 같습니다.

글을 마치며 팁(?) 하나 드리자면, 동시에 다수의 언어를 사용하다보면 헷갈릴 때가 있습니다. 그럴 때는 사용하는 언어마다 다른 개발툴을 사용하시길 권해드립니다. 저는 VSCode와 IntelliJ를 사용 중인데 개발툴을 나눠두면 덜 헷갈리더라구요.

제 글이 새로운 프레임워크를 익히는데 주저하시는 분들께 도움이 되었으면 좋겠습니다 🙂

댓글남기기