Middy & Esbuild를 이용한 프로젝트 구조 개선하기
팀원과 함께 현재 프로젝트의 구조에 대해서 개선할 방향들에 대해서 많은 얘기를 나누었을 때 당장의 가장 큰 문제점으로 신규 서비스를 개발하거나 혹은 기존 서비스를 유지보수할 때에 발생하는 비용에 대한 얘기가 많이 있었다.
이러한 문제를 개선하고 앞으로 유연한 방향으로 개발을 하기위해 적용한 Middy(Middleware Framework) 그리고 Esuild(Build Tool)에 대해서 정리해보았다.
Middy
Middy란?
Middy는 AWS Lambda 함수를 위한 Node.js 미들웨어 프레임워크이며, Express.js와 같은 전통적인 웹 프레임워크에서 사용되는 미들웨어(Middleware) 패턴을 Lambda 실행 환경에 도입할 수 있게 한다.
미들웨어는 요청 이벤트가 메인 핸들러 함수에 도달하기 이전 (before)과 핸들러가 응답을 반환한 이후 (after), 그리고 오류가 발생했을 때 (onError) 실행되도록 구성된다.
이 패턴을 통해 개발자는 인증, 데이터베이스 연결 관리, 로깅 등의 코드를 메인 핸들러에서 분리하여 재사용 가능한 컴포넌트(미들웨어)로 만들 수 있다.
주요 특징
- 핸들러 코드: 오직 핵심 비즈니스 로직만 남게 되어, 코드가 매우 간결해지고 가독성이 높아진다.
- 재사용성: 미들웨어는 독립적인 모듈이기 때문에 여러 Lambda 함수에서 쉽게 재사용할 수 있다.
- 유지보수: 비기능적 요구사항이 변경되어도, 해당 미들웨어만 수정하면 되므로 메인 비즈니스 로직에 영향을 주지 않는다.
Esbuild
Esbuild는 JavaScript, TypeScript 코드를 빠르고 효율적으로 번들링(Bundling)하고 축소(Minifying)하는데 사용되는 빌드 도구이자 번들러(Bundler)이다.
esbuild는 Go 언어로 작성되었으며, 이는 JavaScript 기반의 다른 번들러와 달리 멀티 코어를 적극적으로 활용하고 병렬 처리에 매우 유리합니다. 이로 인해 수백 밀리초 만에 대규모 애플리케이션을 번들링할 수 있다.
주요 특징
- 번들링 (Bundling): 여러 모듈(파일)을 하나 또는 소수의 파일로 합칩니다. 이는 브라우저나 Node.js 환경에서 효율적으로 코드를 로드하고 실행할 수 있다.
- 축소 (Minification): 코드에서 불필요한 공백, 주석, 긴 변수 이름 등을 제거하여 최종 파일 크기를 줄일 수 있다.
- 트랜스파일링 (Transpiling): 최신 JavaScript/TypeScript 문법을 구형 브라우저나 Node.js 환경에서도 동작할 수 있는 버전으로 변환가능하다. (예: JSX, TypeScript를 순수 JavaScript로 변환)
- 트리 쉐이킹 (Tree Shaking): 코드를 분석하여 실제로 사용되지 않는 모듈이나 함수를 최종 번들에서 제거하여 파일 크기를 더욱 줄일 수 있다.
- 소스 맵 (Source Maps): 번들링된 코드를 원본 코드로 매핑하여 디버깅을 용이하게 한다.
Middy & Esbuild 도입 이유
리소스 적합성
Middy는 AWS Lambda를 위함 프레임워크이고 무엇보다, 경량 프레임워크로 AWS Lambda에서 가지고 있는 리소스의 제한에서 큰 이점을 가지고 있다.
| 프레임워크 | 설치 명령어 (Node.js) | 설치 용량 (압축 해제 후, 근사치) |
|---|---|---|
| Middy (코어) | npm install @middy/core | 약 30KB |
| Express.js | npm install express | 약 1MB ~ 2MB |
기본적인 설치만을 했을 때에는 최대 70% 가까이 용량 차이가 난다. 이 정도의 차이는 지난번 aws-sdk와 비교했을때 Cold Start에서 최대 200ms의 지연이 발생할 수 있다.
중복코드 제거, 재사용성, 유지보수
프로젝트 구조를 개선하고자 했을 때, 핵심 비즈니스 로직을 분리하여 재사용할 수 있는 방식으로 생각해보았다. 하지만, 도메인 기반 패키징 방식으로 생각해보았을 때 경로 설정, 필요 코드 빌드와 같은 부분에서 많은 제한이 있었다.
그래서 Middy와 함께 Esbuild를 이용하여 Bundling, Tree Shaking을 통해 핵심 비즈니스 로직 구현과 코드의 간결성, 재사용성 그리고 유지보수성을 높일 수 있다고 생각했다.
Middy & Esbuild 도입 후 목표
- 핵심 비즈니스 로직으로 분리
- 재사용 및 유지보수성 향상
- 실제 사용되는 모듈이나 함수만을 이용하여 용량 최소화
- 모든 소스를 하나의 파일에서 단출하게 관리 및 AWS Lambda Console 관리 가능
4번의 경우 지금 개발팀에서 지향하는 방식이 아니다. AWS Lambda Console에서는 NodeJS를 이용해 개발, 테스트, 배포가 가능하다. 하지만, Typescript를 이용하는 상황에서 AWS Lambda Console에서 유지보수를 하게 되면 프로젝트 관리가 재대로 이루어지지 않을 가능성이 있다. 그래서, 초기에는 Esbuild시 코드 난독화를 하려 했으나, 내부적으로 하지 않는 것으로 결정하였다.
Middy & Esbuild 적용 후 결과
코드 품질 및 아키텍처 개선
- 2개 도메인 모듈에 대해서 5개 파일에 흩어졌던 조회 로직을 1개의 공통 도메인의 조회 로직으로 통합하여 중복코드 최대 200줄 감소
- 서비스 계층 오케스트레이션을 통해서 단순한 서비스 호출이 아닌, 복잡한 모듈 간의 흐름을 단일 서비스 계층에서 관리하여 응집도 향상 및 결합도 감소
- 미들웨어에서의 Logging, JWT 토큰 검증, API 응답 데이터 파싱 등을 처리하여 응집도 향상 및 결합도 감소 및 유지보수 용이성 향상
- 데이터 유효성 검증을 별도 미들웨어에서 처리하여, 비즈니스 로직과의 관심사 분리 및 가독성 향상
빌드 시간 단축
| 구분 | Esbuild 사용 전 | Esbuild 사용 후 |
|---|---|---|
| Build Time | 11,000 ~ 12,000ms | 200 ~ 300ms |
최대 98% 빌드 시간 단축
향후 개선 방향
Esbuild의 불필요한 메서드 번들링과 중복 코드
Esbuild에서 Class를 통째로 Export할 경우 호출되지 않는 메서드까지도 트리 쉐이킹(Tree Shaking) 대상으로 간주하지 않고 모두 최종 번들 파일에 포함되는 이슈가 있다.
서비스 비즈니스 로직을 클래스로 구현하였는데, Esbuild를 이용함에 있어서 비즈니스 로직을 클래스가 아닌 별도 함수로 구현하는게 적합하며, 실제 빌드 결과물에서 불필요한 함수까지 번들링될 필요가 없다고 본다. 그럼에도, 클래스로 구현한 이유는 트랜잭션 관리와 중복 코드 최소화였다.
다중 트랜잭션에서의 트랜잭션의 원자성을 보장하려면, 비즈니스 로직을 구성하는 모든 하위 함수(메서드)들은 동일한 트랜잭션 스코프(Scope) 내에서 실행되어야하는데, 개별 메서드로 비즈니스 로직을 구현할 때에 트랜잭션 객체를 하위 계층(서비스, 리포지토리)의 모든 메서드가 사용할 수 있도록 매개변수를 통해 명시적으로 전달해야하는 점 그리고 트랜잭션이 필요한지 아닌지에 따라 각 메서드를 두 가지 버전으로 구현해야한다는 점에서 중복 코드가 발생하게 된다.
서비스 비즈니스 로직를 클래스로 구현하여도 하위 클래스 생성자에 트랜잭션 객체를 명시적으로 전달해야한다는 점에서 여전히 중복 코드가 발생한다. 하지만, 메서드 레벨의 중복 코드를 피할 수 있고 번들링되는 결과물의 크기가 성능에 영향이 없기에 더 나은 방식이라고 생각하였다.
향후 개선 방향으로 빌드 효율성(함수)과 코드 품질(중복 최소화)을 모두 달성하기 위한 해결책은 함수 기반 구조를 유지하면서 트랜잭션 객체 전달을 아키텍처적으로 숨기는 방식으로 개선해야할 것으로 보인다.