Koa 기반 project NestJS 변환: 레거시 코드 개선

June
None
Published in
7 min readJun 28, 2023
출처: nestjs

안녕하세요. 휴먼스케이프 june입니다.

오늘은 Koa 기반의 프로젝트에 NestJS를 적용한 과정을 공유하려고 합니다.

거대해지는 서비스 레이어

우리가 참여한 서비스는 기존에 Koa로 개발되었고, MVC 패턴으로 설계되었습니다. 초기에는 간단한 서비스를 개발하기에 적합한 직관적이고 경제적인 구조였지만, 비즈니스 로직이 점점 복잡해지면서 코드의 가독성과 의존성 관리가 어려워지며 함수 재사용이 어려운 상황이었습니다.

우리는 코드 가독성을 향상시키고 관심사를 분리하기 위해 다양한 방법을 고려했습니다.

우리는 아래의 여러 장점을 이유로 NestJS를 도입하기로 결정했습니다.

  1. 코드 가독성과 유지보수성 향상
  • koa 기반 프로젝트는 구조의 자유도가 높아 팀마다 원하는 구조를 가져갈 수 있고, 우리의 Koa 기반 프로젝트는 단순하고 직관적인 구조로 개발되었습니다. 하지만 비즈니스 로직이 복잡해지고 코드베이스가 커지면서 현재의 구조로 확장성 있는 개발을 기대하기 어려워 졌습니다.
  • NestJS는 모듈, 컨트롤러, 서비스 등의 구조를 제공하여 코드를 구성하고 관리할 수 있는 강력한 구조를 제공합니다. 이를 통해 코드를 모듈화하고 관심사를 분리하여 가독성을 향상시킬 수 있습니다.

2. 구조 강제와 표준화

  • NestJS는 의존성 주입과 같은 특성을 활용하여 구조를 강제하고, 일관된 코드 작성을 장려합니다. 이는 팀원 간의 협업이 원활해지고, 코드의 일관성과 유지보수성을 향상시킵니다.

NestJS는 어댑터 패턴으로 작성되어 있으며, nest-koa-adapter 라이브러리를 사용하여 Koa와 NestJS를 혼용하면서 자연스럽게 NestJS로 전환할 수 있으리란 기대가 있었습니다.

Nestjs도입 외에 아래와 같은 옵션도 고려했습니다.

  • Koa 프로젝트의 코드 구조를 개선하는 방법을 고민했습니다. 기존 로직을 수정하는 작업이기 때문에 러닝 커브가 적을 수 있었지만, 구조를 강제하기 어려워 다시 이전 구조로 돌아갈 가능성이 크다는 판단을 했습니다. 또, 기존 로직을 해석하고 수정하는 대신 코드를 새로 작성하는 것이 더 쉬울 수 있다고 판단했습니다.
  • 각 서비스를 Microservice로 분리하는 방안도 고려했습니다(MSA). 그러나 각 서비스를 배치해야 할 인력 부족과 분리된 서비스 간 통신을 위한 API 작성에 드는 시간 대비 가성비가 좋지 않다고 판단했습니다.

Nestjs 도입기

위의 장단점을 고려한 후, 우리는 NestJS를 도입하기로 결정했습니다. 기존 Koa 프로젝트의 app.js에서 nest-koa-adapter를 사용하여 NestJS를 연결했습니다.

async function bootstrap() {
const koa = new Koa();
// error-handling
koa.on('error', (err, ctx) => {
console.error('onerror', err.stack);
sentry.withScope((scope) => {
scope.addEventProcessor(async (event) =>
sentry.Handlers.parseRequest(event, ctx.request),
);
const { userId, email, deviceId } = ctx.req.user ?? {};
scope.setUser({ userId, email, deviceId });
sentry.captureException(err);
});
});
koa.proxy = true

const app = await NestFactory.create<NestKoaApplication>(
AppModule,
new KoaAdapter(koa),
);

app.use(cors());
app.use(helmet());

하지만 문제가 발생했습니다. NestJS와 Koa에서 사용하는 라이브러리 간 충돌이 발생하여 Nestjs와 Koa를혼용할 수 없는 상황이었습니다. 앞으로 발생할 수 있는 이슈를 고려하여 라이브러리 충돌을 해결하는 것보다는 독립된 환경으로 빠르게 전환하는 것이 좋다고 판단했습니다.

Docker-compose를 이용한 다중 컨테이너 오케스트레이션

따라서 우리는 Docker Compose를 사용하여 NestJS와 Koa 서버를 동시에 실행하고, Nginx의 리버스 프록시를 이용하여 /v1/ 경로는 NestJS로, 그 외의 경로는 Koa로 라우팅되도록 설정했습니다.

혹시 모를 충돌이 우려되어 npm monorepo를 사용하지 않고, 두 개의 프로젝트를 완전히 분리시켜 개발했습니다.

Docker-compse.yaml

version: '3.8'
services:
koa:
build:
context: .
dockerfile: ./Dockerfile.koa
nest:
extra_hosts:
- 'host.docker.internal:host-gateway'
build:
context: .
dockerfile: ./Dockerfile.nest
nginx:
container_name: nginx
image: nginx
restart: always
ports:
- "80:80"
depends_on:
- koa
- nest
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf

nginx.conf

user  nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}

http {
upstream koa {
server koa:3000;
}

upstream nest {
server nest:4000;
}

server {
listen 80;

location / {
proxy_pass http://koa/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

location /v1/ {
proxy_pass http://nest/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}

회고

위의 작업을 수행하며 느낀 아쉬운점과 잘한점은 다음과 같습니다.

아쉬운점

  • 낯선 프레임워크를 도입하려다 보니 코드 한줄 한줄에 대해 고민하는 시간이 길어졌습니다. 때로는 고민보다 빠르게 개발해보고 다시 수정하는게 나을 수 있다는 생각을 하게 되었습니다.

잘한점

  • nestjs 프로젝트 개발 중 이슈가 발생했을 때 이슈의 원인을 정확하게 분석하고, 라이브러리 충돌이라는 결론이 나왔을 때 빠르게 Docker-compose를 이용하는 쪽으로 의사결정하는 과정이 결과적으로 빠르게 nestjs를 도입하는데에 큰 도움이 되었습니다.

NestJS와 Koa 서비스를 동시에 실행한 후에는 점차적으로 NestJS에서 비즈니스 로직을 재작성하고 새로운 구조에 대해 지속적으로 고민하고 있습니다. 컨버팅 작업이 완료된 후에는 컨버팅 후기를 다시 공유하도록 하겠습니다.

이상으로 Koa 프로젝트에 NestJS를 적용한 과정과 레거시 개선을 통한 코드 구조 개선에 대해 공유드렸습니다.

--

--