자바 스프링의 한계와 새 표준의 필요성
우아한형제들의 박지호 발표자가 자바 스프링 외에 타입스크립트 기반 NestJS를 새 백엔드 표준으로 추가한 배경을 연다. 우아한형제들은 자바 스프링으로 서비스를 운영해 왔는데, 스프링은 공개된 지 20년이 넘었고 엔터프라이즈 환경에서 풍부한 기능을 제공하며 특히 한국에서 사랑받는다. 가장 큰 장점은 많은 레퍼런스와 모범 사례 덕에 안정적이고 단단한 코드를 유지할 수 있다는 점으로, 엔지니어가 팀을 옮기고 새 사람이 맡아도 코드 품질을 균일하게 유지할 수 있다.
그러나 자바 스프링은 멀티스레드 기반이라 하나의 세션을 높은 신뢰성으로 처리하는 대신 온디맨드 트래픽이 많을 때 시스템 자원 낭비가 클 수 있고, 서버리스나 빠른 스케일 아웃이 필요한 경우 느린 콜드스타트 타임도 대표적 단점이다. 그래서 스프링의 단점을 보완하면서도 그만큼 단단한 코드를 유지하고 빠르게 개발·운영할 환경이 필요했고, 그 답으로 타입스크립트와 NestJS를 새 표준으로 추가했다. 발표는 왜 타입스크립트·NestJS를 골랐고 우아한형제들이 이를 어떻게 쓰는지를 다룬다.
새 환경의 목표는 안정성과 생산성이다. 많은 엔지니어가 함께 개발·운영하므로 대중적이고 일정 수준 이상의 생태계가 필요했다. 타입스크립트는 최근 5년간 약 세 배 성장하며, 약타입이라 런타임 안정성이 떨어지는 자바스크립트에 정적 타입 시스템을 더해 문제를 작성 단계에서 조기에 잡게 해 준다. 다만 런타임에서는 여전히 자바스크립트로 동작하므로, 외부 요청이나 데이터베이스에서 들어오는 데이터의 타입을 엄격히 관리하지 못하면 코드와 다른 타입으로 동작할 수 있다. 반대로 외부 데이터 타입만 엄격히 관리하면 강타입 언어만큼 단단한 코드를 유지할 수 있다. 런타임으로는 Node.js를 골랐는데, 이벤트 루프로 작업을 커널에 넘겨 싱글스레드임에도 블로킹 IO를 손쉽게 처리한다. CPU 집약 작업엔 약하지만 많은 IO를 적은 자원으로 처리해 가볍고 요청이 많은 REST API에 효율적이다. 우아한형제들은 서비스가 조직적으로 작은 단위로 나뉘어 있어 IO에 강한 Node.js가 잘 맞았고, CPU 처리에 능한 자바 스프링과 정반대 장단점이라 서로 보완할 수 있다고 봤다.
Node.js 웹 프레임워크 중 가장 유명한 익스프레스는 강력한 커뮤니티와 높은 자유도를 가져 단순한 HTTP 서버라면 놀랍도록 빠르고 간편하다. 하지만 규칙이 없어 협업이 늘수록 컨벤션 통합 비용이 개발보다 커지고, 일정 규모 이상에서 복잡도가 치솟아 결국 자바 스프링으로 재구축하기도 한다. 타입스크립트도 공식 지원하지 않고 필요한 기능을 직접 조합해야 해 버전·품질 이슈가 생긴다. 그래서 택한 NestJS는 앵귤러에서 영감을 받아 Node.js 기반임에도 스프링에 더 가깝다. 작성 패턴을 강제하는 대신 다양한 기능을 제공하는 오피니어네이티드·완전 관리형 프레임워크로, 잘 다져진 경로를 제공해 대규모 인원이 관리해도 예측 가능하고 일관된 코드 베이스를 유지하기 쉽다. IoC 컨테이너, 컨트롤러·서비스 레이어드 아키텍처, ORM 리포지토리 패턴 같은 오랜 패턴을 그대로 쓰고 어노테이션·데코레이터의 코드 모습도 닮아, 오히려 스프링 엔지니어가 기존 Node.js 엔지니어보다 더 빠르게 적응한다.
스프링과 다른 점도 강점이다. 미들웨어로 시작해 필터로 끝나는 NestJS만의 라이프사이클은 하나의 요청을 단계별로 관리해, 인증·밸리데이션·에러 처리를 단계로 나눠 가독성과 유지보수성을 높인다. class-validator·class-transformer 기반 밸리데이션은 외부에서 들어오는 데이터의 타입을 안전하게 관리해, 런타임에 타입이 사라지는 타입스크립트라도 강타입 언어만큼 안전하게 만들며, 우아한형제들은 이를 요청만이 아니라 코드 전반에 동일하게 적용한다. 또 모듈러 레이어드 아키텍처로 도메인·서비스를 모듈 단위로 나누고 라이브러리도 레고처럼 모듈로 추가·제거하며, 트랜스포트 레이어를 분리해 HTTP뿐 아니라 메시지 큐·gRPC·소켓아이오 등 여러 프로토콜과 람다 형태로도 같은 비즈니스 로직을 구동할 수 있다. 이번 발표는 이처럼 예측 가능하고 가이드된 개발 환경을 만드는 고민에 맞춰져 있으며, 운영·유지 관리 방안은 다음 세션에서 다룬다.
우아한형제들에는 '최소 품질 체크리스트'가 있다. 서비스·운영하며 배운 규칙이나 누군가 겪은 문제를 사전에 막기 위한 최소 품질 기준으로, 백엔드는 이를 통과해야만 서비스할 수 있다. 대부분 자바 스프링 기반이었는데, NestJS로 새 환경을 만들며 엔지니어가 전사 설정을 따로 찾지 않아도 자연스럽게 지키게 할 방법을 고민했다. 동시에 하나의 도메인을 여러 형태의 서비스로 제공하는 구조도 과제였다. 복잡한 현대 아키텍처에선 한 도메인을 한 방식으로만 제공하면 요구를 만족시키기 어려워, REST API 환경에서도 배치가 필요하고 비동기·안정성이나 서버리스가 필요할 수 있다. 이를 환경별로 따로 구성하면 같은 도메인을 공유하는 탓에 비즈니스 로직·인터페이스가 중복되고 서비스 간 의존이 높아진다. 의존을 느슨하게 두면 변경을 놓치거나 비용이 중복되고, 너무 높으면 변경의 영향 범위가 지나치게 커진다.
결국 하나의 도메인이 구동 환경마다 다른 코드 베이스로 관리되는 것이 문제라 보고, 하나의 코드 베이스로 다양한 환경을 제공하기로 했다. 그래서 비즈니스 영역과 템플릿 영역을 분리한 우아한형제들만의 NestJS 보일러플레이트를 만들었다. 템플릿은 디렉토리 구조부터 익스프레스·Kafka 설정, 로깅·메트릭까지 비즈니스 로직을 제외한 설정과 규칙을 담아, 최소 품질 체크리스트와 NestJS 추가 규칙을 엔지니어가 자연스럽게 지키게 한다. HTTP·gRPC로 동작하는 애플리케이션 서버 외에 Kafka 같은 메시지 큐 기반 워커와 핸들러(람다) 형태를 기본 제공하고, 이벤트 프로토콜 같은 전사 표준을 템플릿에서 규정해 실수나 가이드 변경을 따로 신경 쓸 필요가 없게 했다. 하나의 비즈니스 로직을 바꾸면 모든 환경에 적용되고 별도 스캐폴딩 없이 그대로 쓸 수 있으며, 누구나 제안할 수 있는 컨벤션으로 함께 만들어 간다. NestJS가 프레임워크 레벨에서 제공하는 테스트 환경으로 유닛·E2E 테스트를 쉽게 작성하고, CI/CD까지 템플릿에 포함해 새 프로젝트를 스키매틱으로 1분 안에 생성할 수 있다.
더 높은 생산성을 위해 몇 가지 모듈과 라이브러리를 만들었고 그중 두 가지를 오픈소스로 관리한다. 첫 번째는 CRUD API를 자동 생성해 주는 라이브러리다. 엔티티 정의, 밸리데이션이 포함된 DTO 작성, 컨트롤러의 스웨거 데코레이터 작성은 단순·반복적이면서 엔티티와 API가 늘수록 코드가 중복돼 비용이 커진다. nestjsx/crud처럼 완성도 높은 오픈소스가 있었지만 메인테이너가 우크라이나 전장 영향으로 피난하며 관리가 중단됐고 마지막 릴리스에 버그와 낡은 의존이 남아, 결국 직접 만들기로 했다. 자체 라이브러리는 NestJS 믹스인 형태로 개발해 기능을 잔뜩 떠안기보다 가드·인터셉터·데코레이터·필터 같은 라이프사이클을 활용해 사용자가 기본 기능 외엔 스스로 커스터마이징하도록 가이드한다. 사용법은 엔티티에서 class-validator·class-transformer의 그룹 기능으로 메서드별 밸리데이션을 정의하고, 라이브러리가 제공하는 CRUD 서비스·컨트롤러를 상속·구현한 뒤 CRUD 데코레이터로 옵션을 바꾸는 것이다. 기본 8개 API를 스웨거 스펙과 함께 제공하되 필요한 것만 활성화하고, 데코레이터 오버라이드로 문구를 바꾸거나 인터셉터로 호출 전후 로직을 끼울 수 있다. TypeORM 기반이라 RDBMS·MongoDB와 복잡한 릴레이션 엔티티, 단순 조회부터 POST 기반 서치까지 지원하며, 사용법은 별도 문서 대신 케이스별 테스트 코드에 녹여 두었다.
두 번째는 환경 변수를 관리하는 Config 라이브러리다. NestJS 공식 Config는 관리가 엄격하지 않고 값의 타입이 추상화되며 설정값이 모듈을 벗어나 전역에서 관리되는데, 이는 모듈 단위 의존을 최소화하는 NestJS 지향과 어긋나 하나의 설정값이 여기저기서 다른 의미로 쓰이며 문제가 생긴다. 또 데이터베이스·AWS·구글 설정값을 해당 서비스 포맷 그대로 쓰고 싶은데 값을 하나하나 다시 매칭해야 하는 불편이 있었다. 그래서 모듈 단위로 관리하고 정의된 클래스를 그대로 쓰도록, 추상 Config 서비스를 상속받아 class-validator·class-transformer로 Expose·Transform 데코레이터로 노출·기본값·타입 변환을 하고 IsString·IsNumber로 검증한다. 환경 변수가 잘못되면 애플리케이션 시작 시 빠르게 잡아 잘못된 설정으로 인한 장애를 막고, 라이브러리가 제공하는 인터페이스를 implements하면 버전이 바뀌어도 컴파일 단계에서 변경을 잡는다. 등록된 값은 읽기 전용이라 실수로 바뀌지 않으며 의도적으로 바꿀 땐 밸리데이션을 유지하는 changeValue 함수를 쓴다. 파편화된 설정은 모아서 README로 만들어 주는 기능으로 보완한다. 이렇게 만든 NestJS 환경은 배달의민족 서비스 일부를 담당하며 한국과 베트남 개발센터에서 함께 쓰이고 있고, 백엔드 200명이 넘는 우아한형제들은 채용을 이어 간다는 말로 발표를 맺는다.