대외활동/42서울

[42서울] ft_transcendence 구현 1 - 과제 명세, Node.js와 Fastify 프로젝트 세팅 및 Fastify 아키텍처 패턴

dev_ares 2025. 4. 28. 18:40
728x90
반응형

서론

드디어 42서울의 마지막 과제인 ft_transcendence(트센)이다. 여기까지 오는 데 많은 동료들이 블랙홀에 빠지거나 자신만의 길을 찾아 떠났다. 나도 지금까지 42서울을 계속하며 그만둘까 힘들었던 적도 많았지만, 그래도 1년 6개월 많은 고민과 노력 끝에 마지막 과제를 할 수 있게 되었다.

 

과제 명세 서론

 

내가 트센을 접할 무렵 과제 명세가 바뀌었다. 백엔드를 맡기로 한 나한테 적용된 부분은

기존 : (Python) Django + PostgreSQL (또는 순수 Ruby)

수정 : (Node.js) Fastify + SQLite (또는 순수 Php)

 

나는 Spring Java/Kotlin 기반의 백엔드 개발자였는데 과제 명세로 처음 접해보는 런타임과 아예 처음 들어보는 프레임워크를 사용하게 되어 적잖이 당황스러웠다. 이는 곧 과제 명세에서 어느 정도 이유를 알 수 있었다. 더하여 찾아보니 Fastify는 생각보다 괜찮은? 프레임워크였다.

 

원래는 영어임

 

ㄷㄷ 엄청난 시간 손실..

 

42는 다른 대외활동과 달리 특정 기술 스텍의 숙련도가 아닌, 개발 자체에서 발생하는 문제 해결 능력을 기르는 데 초점이 맞춰진 대외활동이라는 것을 다시 한번 느낀다. 익숙하지 않은 기술을 사용하여 복잡한 작업을 숙지하고 완료할 수 있는 능력을 드러내는 것이라니..

처음 사용하는 런타임, 프레임워크, 데이터베이스, 기술 스택(웹소켓 등), 실시간 게임 로직 등 과제를 하며 많은 어려움에 부딪쳤지만 지금까지 길러왔던 문제해결 능력이 많은 도움이 되었는지 레퍼런스가 별로 없는 환경에서도 결과물을 잘 구현할 수 있었다(물론 개발 과정에서 정말 많은 고민을 했지만, 절대적인 시간이 오래 걸리지는 않았다). 만들고 나서 나도 신기했다. 

 

과제 명세

ft_transcendence(트센)은 Ping-Pong(탁구) 게임 웹사이트를 구현하는 과제이다.

먼저 과제의 공통 요구사항이 있으며, 추가로 과제가 제공하는 여러 주요 모듈 중 7개의 모듈을 선택해서 구현하는 과제이다. 

 

백엔드 기준으로 내가 정리한 공통 요구사항이다.

  • Docker 사용
  • API 키 환경변수화
  • 토너먼트 시스템, 2p, 4p (누가 누구와 대결하는지, 플레이 순서가 명확하게 표시되어야 함)
  • 매치메이킹 시스템 (토너먼트 시스템은 참가자들의 매치메이킹을 조직하고 다음 경기를 발표해야 함)

보안 문제

  • 데이터베이스에 저장된 모든 비밀번호는 해시
  • SQL 인젝션/XSS 공격으로부터 보호
  • 모든 측면에 대해 HTTPS 사용 (ws 대신 wss)
  • 서버 측에서 양식 및 모든 사용자 입력에 대한 유효성 검사 메커니즘을 구현해야 함

이것들 많고도 다른 내용도 있지만 백엔드에서 필요한 최소사항으로 정리해봤다. 

 

다음은 주요 모듈 7개이다. 마이너 모듈 2개는 주요 모듈 1개로 취급된다. 이 부분에 대해서는 개인 일정 때문에 기존에 해봤던 기술 위주로 선택하였다.

주요 모듈 (6개)

  1. 프레임워크를 사용하여 백엔드 구축.
  2. 표준 사용자 관리, 인증, 토너먼트 전반의 사용자.
  3. 원격 인증 구현하기 - google.
  4. 원격 플레이어.
  5. 2단계 인증(2FA) 및 JWT 구현.
  6. 기본 Pong을 서버측 Pong으로 교체하고 API를 구현합니다.
  7. 실시간 채팅.
  8. AI 상대.
  9. 로그 관리를 위한 인프라 설정.
  10. 마이크로서비스로 백엔드 설계.

마이너 모듈 (2개 → 주요 모듈 1개)

  1. 프레임워크 또는 툴킷을 사용하여 프론트엔드 구축.
  2. 백엔드에 데이터베이스 사용.
  3. 사용자 및 게임 통계 대시보드 (전적 시스템)

주요 모듈 1~6번, 마이너 모듈 1, 2번을 선택했다.

 

다음으로 과제에서 사용했던 기술 스택에 대해서 정리해봤다.

기술 스택 선택 이유

Node.js (과제 명세 필수)

ft_transcendence 프로젝트에서는 백엔드 구현을 위해 Node.js를 사용하도록 명세되어 있다. Node.js는 비동기 이벤트 기반 아키텍처를 사용하여 실시간 웹 애플리케이션에 특히 적합하다. WebSocket 통신을 통한 실시간 게임 구현에 있어서 Node.js의 비동기 특성은 큰 장점이다.

Fastify (과제 명세 필수)

  1. 높은 성능: Fastify는 Express보다 훨씬 빠른 성능을 제공한다. 벤치마크 테스트에 따르면 Fastify는 Express보다 최대 2배 빠르다.
  2. 타입스크립트 지원: Fastify는 타입스크립트와의 통합이 원활하며, 플러그인 시스템에서도 타입 안정성을 유지할 수 있다.
  3. 플러그인 시스템: Fastify의 플러그인 시스템은 코드를 모듈화하고 재사용하기 쉽게 만들어준다.
  4. 스키마 기반 검증: 내장된 JSON 스키마 검증을 통해 API 요청과 응답을 효과적으로 검증할 수 있다.

TypeScript

TypeScript를 선택한 이유는 다음과 같다:

  1. 타입 안정성: 정적 타입 검사를 통해 런타임 오류를 줄이고 코드 품질을 향상시킬 수 있다.
  2. IDE 지원: 코드 자동 완성, 리팩토링, 네비게이션 등 개발자 경험을 크게 향상시킨다.
  3. 유지보수성: 타입 정의를 통해 코드 문서화가 자연스럽게 이루어져 유지보수가 용이하다.
  4. 대규모 애플리케이션: 특히 WebSocket과 같은 복잡한 통신 로직을 구현할 때 타입 시스템을 통해 안정성을 확보할 수 있다.

프로젝트 세팅

프로젝트 구조

프로젝트는 다음과 같은 구조로 설계했다:

├── src
│   ├── global       # 전역 설정 및 유틸리티
│   ├── plugins      # Fastify 플러그인
│   ├── routes       # API 라우트
│   ├── schemas      # 요청/응답 스키마
│   ├── types        # 타입 정의
│   └── utils        # 유틸리티 함수

이러한 구조는 관심사 분리(Separation of Concerns)를 통해 코드의 모듈성과 유지보수성을 높이는 데 중점을 두었다.

데이터베이스 설정 (Prisma)

ORM으로는 Prisma를 선택했다. Prisma는 타입스크립트와의 통합이 뛰어나며, 타입 안전한 데이터베이스 쿼리를 작성할 수 있다는 장점이 있다. 데이터베이스 연결을 위해 Fastify 플러그인을 구현했다:

// src/plugins/prismaPlugin.ts
import fp from 'fastify-plugin';
import { FastifyPluginAsync } from 'fastify';
import { PrismaClient } from '@prisma/client';

export const prisma = new PrismaClient();

const prismaPlugin: FastifyPluginAsync = async (fastify) => {
  fastify.decorate('prisma', prisma);

  fastify.addHook('onClose', async () => {
    await prisma.$disconnect();
  });
};

export default fp(prismaPlugin);

Swagger 설정

API 문서화를 위해 Swagger를 설정했다. Fastify의 @fastify/swagger 플러그인을 사용하여 쉽게 구현할 수 있었다:

// src/plugins/swagger.ts
import fp from 'fastify-plugin';
import swagger from '@fastify/swagger';

export default fp(async (fastify) => {
  fastify.register(swagger, {
    routePrefix: '/docs',
    swagger: {
      info: {
        title: 'ft_transcendence API',
        description: 'API documentation for ft_transcendence',
        version: '1.0.0'
      },
      externalDocs: {
        url: 'https://swagger.io',
        description: 'Find more info here'
      },
      host: 'localhost:8083',
      schemes: ['http'],
      consumes: ['application/json'],
      produces: ['application/json']
    },
    exposeRoute: true
  });
});

Fastify 아키텍처 패턴

플러그인 기반 아키텍처

Fastify는 플러그인 기반 아키텍처를 권장하며, 이는 코드의 모듈화와 재사용성을 높인다. 우리 프로젝트에서는 fastify-plugin을 사용하여 여러 플러그인을 구현했다:

// src/plugins/auth/authService.ts
import fp from 'fastify-plugin';

export default fp(async (fastify) => {
  fastify.decorate('authService', {
    // 인증 관련 메서드들...
  });
});

라우트 구성

각 도메인별로 라우트를 분리하여 관리했다:

import { FastifyPluginAsync } from 'fastify';

const authRoute: FastifyPluginAsync = async (fastify) => {
  fastify.post('/login/google', {
    schema: googleAuthSchema,
    handler: async (request, reply) => {
      // 서비스를 호출하는 코드들...
    },
  });
};
// src/main.ts
// 라우트 등록
await fastify.register(authRoute, { prefix: '/api/auth' });
await fastify.register(tournamentRoute, { prefix: '/api/tournaments' });
await fastify.register(matchRoutes, { prefix: '/api/matchs' });

에러 핸들링

글로벌 에러 핸들러를 구현하여 모든 오류를 일관되게 처리했다:

// src/global/exceptions/exceptionHandler.ts
export const exceptionHandler = (error, request, reply) => {
  // 에러 처리 로직
  if (error instanceof GlobalException) {
    return reply.status(error.statusCode).send({
      code: error.code,
      message: error.message
    });
  }
  
  // 기타 예상치 못한 오류 처리
  request.log.error(error);
  return reply.status(500).send({
    code: 'SERVER_ERROR',
    message: '서버 내부 오류가 발생했습니다.'
  });
};

 

환경변수 관리

// Google OAuth 관련 설정
export const GOOGLE_USERINFO_URL = 'https://www.googleapis.com/oauth2/v3/userinfo';

// JWT 관련 설정
export const JWT_SECRET = process.env.JWT_SECRET as string;
export const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY as string;

서버 시작

// src/main.ts
const fastify = Fastify({
  // logger: true,
});

// health check api
fastify.get('/ft/ping', async () => {
  return 'pong\n';
});

const start = async () => {
  try {
    await fastify.listen({ port: 8083, host: '0.0.0.0' });
    console.log('Server Start!!');
  } catch (error) {
    fastify.log.error(error);
    process.exit(1);
  }
};

start();

 

 

마무리

다음 글에서는 실제 구현에 대한 내용을 정리해보겠다.

728x90
반응형