레거시 Express를 Nest.js로 끌어올리는 현실적인 마이그레이션 전략

기존에 사용하던 Express.js 로직에서 Nest.js 로직으로 마이그레이션하는 전략을 알아보자.
Nest.js테스트레거시 코드
avatar
a month ago
·
8 min read

개요

기존 Express.js(v1) 기반의 API를 Nest.js(v2)로 점진적으로 마이그레이션하는 과정과 그에 따른 테스트 전략을 소개한다. Weather API 예시를 통해 실제 상황에서 적용 가능한 단계별 진행 방법을 알아보자.

예제 코드

아래 글에서 설명하고 있는 마이그레이션 테스트 방법이 구현되어 있는 Repository 를 참고하라.

https://github.com/mansukim1125/nestjs-test

왜 마이그레이션을 하는가?

많은 팀들이 새로운 기술 도입을 고민할 때 "왜" 그리고 "언제" 시작해야 하는지 고민하게 된다. 우리 팀의 경우:

  1. 신규 기능 개발이나 정책 개선과 같은 굵직한 개발 요청사항이 없는 시기를 활용

  2. 유지보수성과 코드 품질 향상을 위한 내부 개선 작업의 일환 (일종의 백로그 처리..)

  3. 새로운 기능 개발 작업이 들어오면 즉시 중단하고 대응할 수 있는 유연한 업무 구조 설계

이러한 배경에서 Express → Nest 마이그레이션을 시작하게 되었다.

마이그레이션 전략 개요

Express에서 Nest.js로 마이그레이션 시 가장 중요한 것은 기존 기능을 유지하면서 안정적으로 전환하는 것이다. 이를 위해 다음과 같은 단계를 따른다.

테스트 코드를 작성하지 않고 그냥 Nest.js 로 이관하면 안 되나요?

  • 가능은 하지만 개발자 입장에서 불안할 것 같다.

  • 테스트 시 API 수동 호출을 통한 테스트를 하게 되고, 이로 인한 휴먼 에러 발생 가능성 및 개발자의 피로도가 높아진다.

  • 사실 기존에 개발을 할 때 테스트 코드를 작성해두었다면 마이그레이션 시 위와 같은 불안감이 덜했을 것 같기는 하다..

마이그레이션 단계

본 글은 3 번까지의 과정을 다룬다.

  1. 현재 Express(v1) API의 E2E 테스트 작성

    1. 기존 기능이 동작하는지 확인

    2. All pass 확인

  2. Nest.js(v2)로 API 로직 마이그레이션

  3. 테스트를 구동시켜 v1 → v2 검증

  4. (선택) API 문서화

  5. 클라이언트 API 연동 및 QA 테스트

  6. 상용 배포

  7. (선택) 추후 마이그레이션된 로직이 재사용되는 경우, 해당 로직에 대해 단위 테스트를 작성하여 보강한다.


1. ExpressJS(v1) API에 대한 E2E 테스트 작성

마이그레이션을 안전하게 수행하기 위해 기존 Express**(v1)** API에 대한 E2E 테스트를 정성을 들여 작성해준다..

예제: Express 기반 Weather API

  • 예제 API 코드 (Github 링크)

    'use strict';
    
    const express = require('express');
    const router = express.Router();
    
    // NOTE: weathers 데이터는 생략
    
    router.get('/v1/weather', (req, res, next) => {
      console.log('weather controller v1');
      
      const { city } = req.query;
      
      if (!city || city.length < 1) {
        return next(new Error('city is required'));
      }
      
      const weatherInfo = weathers[city];
      if (!weatherInfo) {
        return res.status(404).json({
          message: 'No weather found',
        });
      }
      
      return res.status(200).json({
        ...weatherInfo,
      });
    });
    
    exports.router = router;

Supertest를 활용한 v1/v2 API E2E 테스트

  • v1 테스트 코드 (Github 링크)

    // src/__test__/e2e/v1/v1.weather.e2e.spec.ts
    
    'use strict';
    
    import { createNestApp } from '../../../createNestApp';
    
    import * as request from 'supertest';
    import { INestApplication } from '@nestjs/common';
    
    const apiVersion = 'v1';
    
    describe(`/${apiVersion}/weather`, () => {
      let app: INestApplication;
    
      beforeAll(async () => {
        app = (await createNestApp()).nestApp;
        await app.init();
      });
    
      afterAll(async () => {
        await app.close();
      });
    
      it('should return 400 if city parameter is missing', async () => {
        // 도시 파라미터가 주어지지 않았을 경우
        const res = await request(app.getHttpServer()).get(
          `/${apiVersion}/weather`,
        );
    
        expect(res.status).toBe(400);
        expect(res.body).toEqual({
          message: 'city is required',
        });
      });
    
      // NOTE: 기타 테스트는 생략
    });

위 테스트가 기존 Express.js 로 구현된 API (v1) 에 대해 All Pass 되는 것을 확인했다면 Nest.js 로 구현하기에 앞서 v2 API 에 대해 E2E 테스트 코드를 만든다. (복붙)

  • v2 테스트 코드 (Github 링크)

    // src/__test__/e2e/v2/v2.weather.e2e.spec.ts
    
    // NOTE: 아래 구문을 제외하고 위 파일과 내용물이 동일하다.
    const apiVersion = 'v2';

이제 Nest.js(v2)로 동일한 기능을 구현한다. 이렇게 하면 기존 E2E 테스트 코드를 그대로 활용하면서 Nest.js 로직을 구현할 수 있다.


2. Nest.js(v2)로 API 마이그레이션

기존 Express 로 구현된 코드를 그대로 두고 Nest.js 구조로 마이그레이션 한다.

Controller, Service, Repository 구현 (Github 링크)

Controller

import { Controller, Get, Query } from '@nestjs/common';
import { WeatherService } from './weather.service';
import { IWeather } from '../common/interfaces/weather.interface';

@Controller('/v2/weather')
export class WeatherController {
  constructor(private readonly weatherService: WeatherService) {}

  @Get()
  async getWeather(@Query('city') city: string): Promise<IWeather> {
    return this.weatherService.getWeather(city);
  }
}

Service

import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { WeatherRepository } from './weather.repository';
import { IWeather } from '../common/interfaces/weather.interface';

@Injectable()
export class WeatherService {
  constructor(private readonly weatherRepository: WeatherRepository) {}

  public getWeather(city: string): IWeather {
    if (!city) {
      throw new BadRequestException({
        message: 'city is required',
      });
    }

    const weatherData = this.weatherRepository.getWeather(city);
    if (!weatherData) {
      throw new NotFoundException({
        message: 'No weather found',
      });
    }

    return weatherData;
  }
}

Repository

import { Injectable } from '@nestjs/common';
import { IWeather } from '../common/interfaces/weather.interface';

@Injectable()
export class WeatherRepository {
  private readonly weathers: Record<string, IWeather>;

  constructor() {
    this.weathers = {
      // NOTE: Mock data 생략
    };
  }

  public getWeather(city: string): IWeather | undefined {
    return this.weathers[city];
  }
}

이런 방식으로 기존 API의 기능을 보장하면서 점진적으로 Nest.js로 전환할 수 있게 된다.

3. 테스트를 구동시켜 v1 → v2 검증

첫 번째 단계에서 작성해둔 E2E 테스트 코드를 v2 에 대해 구동시켜 정상 동작하는지를 확인한다. 실패한 테스트가 존재하면 2단계로 돌아가 로직을 검토하고 수정 & 재 테스트를 반복한다.

현실적인 도전 과제들

여느 때와 마찬가지로, 각 프로젝트의 상황과 사용하고 있는 기술 스택이 상이하여 위와 같은 방법을 그대로 적용할 수는 없을 것이다.

DB 에 의존하는 로직이 다수 존재하여 DB 데이터 조회 메소드를 모킹해야 할 수도 있고, DB 이외에 S3나 AWS 리소스 등의 서드파티 API 를 모킹해야 할 수도 있다..







- 컬렉션 아티클