Windsurf 사례 연구: 핀테크 스타트업의 Express.js 모놀리스를 NestJS 마이크로서비스로 2주 만에 마이그레이션한 방법
개요: 3개월 프로젝트를 2주로 단축한 AI 기반 코드 마이그레이션
서울 기반 핀테크 스타트업 PayFlow(가명)는 5년간 운영해 온 Express.js 모놀리스 애플리케이션의 기술 부채가 한계에 도달했습니다. 결제 처리, 사용자 인증, 리스크 분석, 알림 서비스가 하나의 코드베이스에 뒤엉킨 12만 줄 규모의 레거시 시스템을 NestJS 기반 마이크로서비스 아키텍처로 전환해야 했습니다. 수동 리라이트 예상 기간은 3개월이었지만, Windsurf의 AI 에이전트 Cascade를 활용해 단 2주 만에 마이그레이션을 완료했습니다.
문제 상황
- Express.js 4.x 기반 모놀리스: 120,000+ LOC, 47개 라우트 파일- 테스트 커버리지 23%로 리팩토링 리스크 높음- 순환 의존성 38건, 미사용 코드 약 15%- 수동 마이그레이션 견적: 개발자 4명 × 3개월 = 12인월
Windsurf 환경 설정
1단계: Windsurf 설치 및 프로젝트 초기화
# Windsurf 설치 (공식 사이트에서 다운로드 후)
macOS/Linux
curl -fsSL https://windsurf.com/install.sh | sh
프로젝트 디렉토리에서 Windsurf 실행
cd /path/to/payflow-monolith
windsurf .
2단계: Cascade 에이전트 설정
Windsurf 에디터에서 Cmd+L (macOS) 또는 Ctrl+L (Windows/Linux)로 Cascade 패널을 열고 마이그레이션 컨텍스트를 설정합니다.
# .windsurf/settings.json 프로젝트 설정
{
"cascade": {
"model": "gpt-4-turbo",
"apiKey": "YOUR_API_KEY",
"contextWindow": "full-project",
"autoIndex": true
},
"project": {
"source": "express",
"target": "nestjs",
"testFramework": "jest"
}
}
## 마이그레이션 워크플로우
Phase 1: 의존성 분석 및 서비스 분리 (Day 1-2)
Cascade에게 프로젝트 전체 구조를 분석하도록 지시합니다.
# Cascade 프롬프트 (Windsurf Chat에서 입력)
> 이 Express.js 프로젝트의 모든 라우트, 미들웨어, 모델 간
> 의존성 그래프를 분석하고, 마이크로서비스로 분리 가능한
> 바운디드 컨텍스트를 식별해줘. 순환 의존성도 모두 표시해.
Cascade는 전체 코드베이스를 스캔한 후 5개의 마이크로서비스 경계를 식별했습니다:
| 서비스 | 원본 파일 수 | 의존성 | 순환 참조 |
|---|---|---|---|
| auth-service | 14개 | user-model, jwt-util | 2건 |
| payment-service | 21개 | transaction-model, pg-client | 5건 |
| risk-service | 8개 | ml-scoring, transaction-log | 0건 |
| notification-service | 6개 | email-client, sms-gateway | 1건 |
| user-service | 11개 | user-model, profile-util | 3건 |
# Cascade 프롬프트
> payment 관련 Express 라우트를 NestJS 마이크로서비스로 변환해줘.
> Controller, Service, Module, DTO 패턴을 적용하고
> class-validator로 입력 검증을 추가해.변환 전 (Express.js):
// routes/payment.js
router.post('/payments', async (req, res) => {
try {
const { amount, currency, merchantId } = req.body;
const tx = await db.transactions.create({
amount, currency, merchantId,
status: 'pending'
});
const result = await pgClient.charge(tx.id, amount);
res.json({ success: true, transactionId: tx.id });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
변환 후 (NestJS — Cascade 자동 생성):
// payment-service/src/payment/dto/create-payment.dto.ts
import { IsNumber, IsString, IsPositive } from 'class-validator';
export class CreatePaymentDto {
@IsNumber()
@IsPositive()
amount: number;
@IsString()
currency: string;
@IsString()
merchantId: string;
}
// payment-service/src/payment/payment.controller.ts
import { Controller, Post, Body } from ‘@nestjs/common’;
import { PaymentService } from ’./payment.service’;
import { CreatePaymentDto } from ’./dto/create-payment.dto’;
@Controller(‘payments’)
export class PaymentController {
constructor(private readonly paymentService: PaymentService) {}
@Post()
async createPayment(@Body() dto: CreatePaymentDto) {
return this.paymentService.processPayment(dto);
}
}
// payment-service/src/payment/payment.service.ts
import { Injectable } from ‘@nestjs/common’;
import { TransactionRepository } from ’./transaction.repository’;
import { PgClientService } from ’../pg-client/pg-client.service’;
@Injectable()
export class PaymentService {
constructor(
private readonly txRepo: TransactionRepository,
private readonly pgClient: PgClientService,
) {}
async processPayment(dto: CreatePaymentDto) {
const tx = await this.txRepo.create({
…dto,
status: ‘pending’,
});
await this.pgClient.charge(tx.id, dto.amount);
return { success: true, transactionId: tx.id };
}
}
Phase 3: 자동 테스트 생성 (Day 9-11)
Cascade에게 변환된 모든 서비스에 대한 유닛·통합 테스트를 자동 생성하도록 요청합니다.
# Cascade 프롬프트
> payment-service의 모든 Service, Controller에 대해
> Jest 유닛 테스트와 e2e 테스트를 생성해줘.
> 엣지 케이스(음수 금액, 빈 merchantId, DB 연결 실패)를 포함해.
// payment-service/src/payment/payment.service.spec.ts describe('PaymentService', () => { let service: PaymentService; let txRepo: jest.Mocked; let pgClient: jest.Mocked ; beforeEach(async () => { const module = await Test.createTestingModule({ providers: [ PaymentService, { provide: TransactionRepository, useValue: { create: jest.fn() } }, { provide: PgClientService, useValue: { charge: jest.fn() } }, ], }).compile(); service = module.get(PaymentService); txRepo = module.get(TransactionRepository); pgClient = module.get(PgClientService); });
it(‘should process a valid payment’, async () => { txRepo.create.mockResolvedValue({ id: ‘tx-001’ } as any); pgClient.charge.mockResolvedValue(undefined); const result = await service.processPayment({ amount: 50000, currency: ‘KRW’, merchantId: ‘M-100’, }); expect(result.success).toBe(true); expect(result.transactionId).toBe(‘tx-001’); });
it(‘should propagate PG client errors’, async () => { txRepo.create.mockResolvedValue({ id: ‘tx-002’ } as any); pgClient.charge.mockRejectedValue(new Error(‘PG timeout’)); await expect( service.processPayment({ amount: 10000, currency: ‘KRW’, merchantId: ‘M-101’ }), ).rejects.toThrow(‘PG timeout’); }); });
# 테스트 실행 cd payment-service npm run test — —coverage
커버리지: 23% → 87%로 향상
Phase 4: 순환 의존성 해결 및 통합 (Day 12-14)
# Cascade 프롬프트
> 식별된 38건의 순환 의존성을 해결해줘.
> 이벤트 기반 패턴이나 인터페이스 분리를 사용하고,
> 서비스 간 통신은 NestJS의 @nestjs/microservices TCP 전송으로 구현해.// NestJS 마이크로서비스 간 TCP 통신 설정// payment-service/src/main.ts import { NestFactory } from ‘@nestjs/core’; import { Transport, MicroserviceOptions } from ‘@nestjs/microservices’; import { AppModule } from ’./app.module’;
async function bootstrap() { const app = await NestFactory.createMicroservice( AppModule, { transport: Transport.TCP, options: { host: ‘0.0.0.0’, port: 3001 }, }, ); await app.listen(); } bootstrap();
최종 성과
| 지표 | 마이그레이션 전 | 마이그레이션 후 |
|---|---|---|
| 아키텍처 | 모놀리스 (1 서비스) | 5개 마이크로서비스 |
| 테스트 커버리지 | 23% | 87% |
| 순환 의존성 | 38건 | 0건 |
| 소요 기간 | 예상 3개월 | 실제 2주 |
| 비용 절감 | 12인월 | 2인월 (83% 절감) |
@파일명으로 Cascade 대화에 특정 파일을 고정하면 관련 파일 간 변환 정확도가 크게 향상됩니다.- **Flows 활용:** Windsurf Flows 기능으로 반복적인 변환 패턴(예: Express 미들웨어 → NestJS Guard)을 템플릿화하면 일관성 있는 변환이 가능합니다.- **Write 모드 vs Chat 모드:** 대규모 멀티파일 변환 시 Write 모드를 사용하면 Cascade가 직접 파일을 생성·수정합니다. 코드 리뷰 후 Accept/Reject로 제어하세요.- **터미널 연동:** Cascade는 터미널 출력을 읽을 수 있으므로 npm run build 에러를 붙여넣으면 즉시 수정 코드를 제안합니다.- **증분 마이그레이션:** 한 번에 전체를 변환하지 말고 서비스 단위로 분리 → 변환 → 테스트 → 검증 사이클을 반복하세요.
## Troubleshooting: 자주 발생하는 문제 해결
| 증상 | 원인 | 해결 방법 |
|---|---|---|
| Cascade가 대규모 파일을 건너뜀 | 컨텍스트 윈도우 초과 | 파일을 분할하거나 @파일명으로 명시적으로 참조 |
| 변환된 NestJS 코드에서 DI 에러 발생 | Module의 providers 누락 | Cascade에게 "AppModule에 모든 provider를 등록해줘"라고 요청 |
| 순환 참조 해결 후 런타임 에러 | forwardRef() 누락 | @Inject(forwardRef(() => ServiceName)) 패턴 적용 요청 |
| 테스트 생성 시 mock 타입 불일치 | TypeScript strict 모드 충돌 | jest.Mocked을 명시적으로 사용하도록 프롬프트 수정 |
| Windsurf 인덱싱이 느림 | node_modules 포함 | .windsurfignore에 node_modules/ 및 dist/ 추가 |
Q1: Windsurf Cascade는 Express.js 외에 다른 프레임워크 마이그레이션도 지원하나요?
네, Cascade는 코드 컨텍스트를 이해하는 AI 에이전트이므로 Koa, Fastify, Spring Boot, Django 등 다양한 프레임워크 간 마이그레이션에 활용할 수 있습니다. 핵심은 명확한 프롬프트로 소스/타깃 프레임워크, 원하는 패턴, 제약 조건을 구체적으로 지시하는 것입니다.
Q2: AI가 생성한 코드의 품질과 보안을 어떻게 보장할 수 있나요?
Windsurf의 Write 모드에서 생성된 코드는 반드시 Accept 전에 diff를 확인하세요. 추가로 ESLint 보안 플러그인(eslint-plugin-security), SonarQube 정적 분석, 그리고 Cascade가 생성한 테스트 코드의 커버리지를 검증하는 것을 권장합니다. PayFlow 사례에서도 보안 감사를 별도로 진행하여 SQL 인젝션 관련 이슈 2건을 추가 수정했습니다.
Q3: 2주 마이그레이션 기간 동안 운영 서비스 중단 없이 전환이 가능했나요?
스트랭글러 피그(Strangler Fig) 패턴을 적용했습니다. API Gateway를 앞단에 배치하고, 마이크로서비스가 준비된 엔드포인트부터 순차적으로 트래픽을 전환했습니다. Cascade는 이 과정에서 API Gateway 라우팅 설정 코드도 함께 생성해주어 무중단 전환이 가능했습니다.