Node.js 프레임워크 없이 개발한 1년 회고 (feat. NestJS)

NodeJS로 프레임워크 없이 1년 정도 개발하다가 최근 새로운 프로젝트에 NestJS를 처음 도입했다.
결론부터 얘기하면 훨씬 편하고 빠르고 안정적인 개발이 가능해졌다.


노드 프로젝트 처음부터 프레임워크를 썼다면 좋았을 것이다.
하지만 돌이켜 생각해보면 프레임워크 없이 개발한 경험이 개발의 시야를 넓히는데 도움이 된 것 같다.

프레임워크에서 제공해주는 다양한 기능들을 직접 구현하며 어려움도 있었지만, 지금은 프레임워크가 왜 유용한지 온몸으로 느껴진다고 해야할까…

장고로 개발을 처음 시작했을 땐 그저 프레임워크 사용법을 익히고 코드를 작성하는데 급급했다.


아키텍처 구조

프레임워크 없이 개발할 때 가장 먼저 고민했던건 아키텍처 구조다.
처음엔 라우팅, 서비스 구현, 예외처리 등 모든 로직을 하나의 파일에서 처리했다.
그리고 모든 api들을 별도의 Aws Lambda로 구성했다. 처음엔 최대한 단순한 구조를 지향했기 때문이다.

하지만 점점 문제가 생기기 시작했다. 서비스 로직이 단순할 땐 괜찮았지만 복잡해질수록 이 구조로는 감당하기 어려웠다. 그리고 Lambda를 여러개 사용하니 콜드스타트 이슈와 Aws Stack 리소스 한도 문제도 문제도 발생됐다.

이후 변경한 구조에서는 유사한 기능별로 람다는 합치며 콜드스타트와 리소스 한도 문제를 줄였고, 라우팅을 담당하는 코드와 서비스를 담당하는 코드를 분리시켰다. 서비스 코드는 최대한 함수로 분리해서 가독성을 높이려고 했다.

그리고 네스트를 써보니 내가 지향했던 아키텍처가 이런 방식이었음을 이해하게 됐다.


객체지향 프로그래밍

프레임워크 사용 전에는 객체지향 프로그래밍 방식과는 다소 거리가 있었다.
시스템이 복잡하지 않았을땐 아무래도 상관없었지만 복잡해질수록 객체지향 프로그래밍 또는 함수형 프로그래밍 등의 방법론의 필요성을 느꼈다.

NestJS는 객체지향 프로그래밍을 잘 구현할 수 있게 도와주는 프레임워크다.
객체지향의 핵심은 객체간의 협력이랬던가..
NestJS의 핵심기능인 Dependency Injection를 사용하면 마법 같이 객체간의 협력을 만들어 낼 수 있다.


라우팅

라우팅 또한 직접 만들었었다. Handler에서 접수하는 Event에 담긴 path, httpMethod 정보로 라우팅을 해줬다.
요청이 오는 경로가 다양해지고, proxy 서버를 구현하기 시작하면서 라우팅을 직접 구현하는건 점점 복잡해졌다. 나중에는 정규식까지 동원해서 경로를 찾아줘야 했다.

exports.handler = async (event: APIGatewayProxyEvent) => {
    const proxy = event.pathParameters?.proxy ?? ''
    if (event.resource === '/user/{proxy+}') {
        if (proxy === 'signup' && event.httpMethod === 'POST') {
            return await baseHandler(event, signup.post)
        } else if (proxy === 'signin' && event.httpMethod === 'POST') {
            return await baseHandler(event, signin.post)
        ...

만약 처음부터 프레임워크를 썼다면 라우팅에 대해서 고민해본적이 있었을까. 라우팅은 서비스의 가장 기본적인 기능이고 효율성을 담당하는 부분으로 한번쯤 구현방식에 대해 생각해보는 것도 좋은 것 같다.

네스트에서 라우팅은 너무 쉽다. 데코레이터로 정해진 규칙대로 정의만 해주면 된다. 네스트가 다 알아서 해준다. 너무 좋다..


유효성 검증 (Validation)

프레임워크가 제공하는 가장 편리한 기능 중 하나인 것 같다.
Client로 요청받는 데이터(Body, Query, Param, 헤더 등)와 반환 데이터는 유효성 검증이 필수다.

프레임워크가 없을 땐 validation 체크하는 코드와 서비스 코드가 뒤섞여 있었다.
그러다보니 validation이 누락된 데이터도 생겼고 유효성 문제로 에러가 계속해서 발생헀다. (그 당시 js에서 ts로 바꿔준건 그나마 다행이었다..)
결국 나중에는 NestJS에서 쓰는 ‘class-validator’만 별도로 설치해서 사용 했다. (이럴거면.. 프레임워크 써라고..)

Validation은 처음 한번만 제대로 적용해두면 크게 수정할 일이 없다.
그러니 가능하면 느슨하지 않게 최대한 fit하게 유효성 검사를 적용해두면 맘편히 다른 것에 집중할 수 있다.


로컬 서버

장고를 사용할 때 로컬 서버건 그냥 당연히 주어지는 것이었다.
하지만 이것 또한 프레임워크의 혜택이었으니.. 로컬서버 또한 직접 구축해야 했다.

도커 이미지로 구현하는 AWS의 Sam을 이용했다.
문제는 너무 느리고 도커이미지 생성을 위한 yaml 코드도 직접 작성을 해줘야 한다.

반면 네스트의 로컬 서버는 서버 config만 설정해주면 사용할 수 있다.
무엇보다 중요한건 엄청 빠르다.
이것만으로도 충분히 프레임워크를 사용할 가치가 있지 않을까 생각이 든다.


SQL & TypeORM

프레임워크를 사용하지 않았던 것 처럼 최대한 단순한 구조를 위해 Orm도 사용하지 않았다.
대신 Raw Query를 사용했는데 이때 sql를 많이 익혔고 db에 대한 이해력도 많이 올라갔다.

네스트를 쓰면서는 객체지향 프로그래밍에 집중하기 위해 TypeORM을 적용했다.
TypeORM의 좋은점은 간단한건 orm 메서드로 db를 쿼리하고 복잡한건 쿼리빌더를 사용해서 마치 Raw Sql 마냥 사용할 수 있다.


DB Connection

프레윔워크가 없을 땐 DB Client와의 connect와 end를 직접 관리해줘야 했다.
그러다보니 매번 연결 코드를 써줘야했고 누락되면 DB에 치명적인 부하가 생길 위험도 있었다.

import { getClient } from "/opt/nodejs/db";
const client = getClient();
client.connect();
const dbResponse = await client.query(`select * from my_tables`);
client.end();
const data = dbResponse.rows;

이정도가 쿼리 한번 조회하기 위해서 작성해야하는 코드다.
코드가 복잡해질수록 실수할 확률도 높아지고 가독성도 그리 좋지 않다.

네스트에서는 이 또한 고민할 필요가 없다. DB 정보만 정해진 방법대로 정의해두면 다 알아서 연결 해준다.
이 또한 프레임워크만 사용했더라면 고민해본적이 없지 않았을까.


수명 주기

프로젝트가 커질수록 수명 주기의 단계는 복잡해진다.
수명 주기에 따라 단계를 나누는건 캡슐화를 통해 역할을 분리하며 좀 더 객체지향적인 프로그래밍을 가능하게 해준다.
이는 개발의 효율성을 높여주고 가독성, 유지/보수 등을 용이하게 해준다.

프레임워크가 없을 땐 단계를 나누다 보면 수명 주기가 되었다.
반면 네스트에서는 수명 주기가 있고 그 안에 역할이 있다.

그동안 시행착오를 겪으며 만들어왔던 시스템들이 NestJS에 오니까 이미 제공해주고 있다. 그것도 더 높은 수준으로.


결론

“바퀴를 다시 만들지 마라.”
결론은 가능하면 프레임워크를 사용하는게 좋다.

그럼 처음부터 프레임워크를 사용하지 않은 것에 후회하냐? 그렇지는 않다.
직접 바퀴를 만드느라 고생한적도 있지만, 바퀴는 왜 둥글어야 하는지와 바퀴를 조립하는 방법을 알게됐다.

앞으로도 프레임워크를 비롯하여 여러 구축된 시스템을 사용하게 될텐데 숲을 보는 안목을 키우려고 노력하자. 끗!

```

댓글남기기