[42서울] ft_transcendence 구현 1 - 과제 명세, Node.js와 Fastify 프로젝트 세팅 및 Fastify 아키텍처 패턴
서론
드디어 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개)
- 프레임워크를 사용하여 백엔드 구축.
- 표준 사용자 관리, 인증, 토너먼트 전반의 사용자.
- 원격 인증 구현하기 - google.
- 원격 플레이어.
- 2단계 인증(2FA) 및 JWT 구현.
- 기본 Pong을 서버측 Pong으로 교체하고 API를 구현합니다.
- 실시간 채팅.
- AI 상대.
- 로그 관리를 위한 인프라 설정.
- 마이크로서비스로 백엔드 설계.
마이너 모듈 (2개 → 주요 모듈 1개)
- 프레임워크 또는 툴킷을 사용하여 프론트엔드 구축.
- 백엔드에 데이터베이스 사용.
- 사용자 및 게임 통계 대시보드 (전적 시스템)
주요 모듈 1~6번, 마이너 모듈 1, 2번을 선택했다.
다음으로 과제에서 사용했던 기술 스택에 대해서 정리해봤다.
기술 스택 선택 이유
Node.js (과제 명세 필수)
ft_transcendence 프로젝트에서는 백엔드 구현을 위해 Node.js를 사용하도록 명세되어 있다. Node.js는 비동기 이벤트 기반 아키텍처를 사용하여 실시간 웹 애플리케이션에 특히 적합하다. WebSocket 통신을 통한 실시간 게임 구현에 있어서 Node.js의 비동기 특성은 큰 장점이다.
Fastify (과제 명세 필수)
- 높은 성능: Fastify는 Express보다 훨씬 빠른 성능을 제공한다. 벤치마크 테스트에 따르면 Fastify는 Express보다 최대 2배 빠르다.
- 타입스크립트 지원: Fastify는 타입스크립트와의 통합이 원활하며, 플러그인 시스템에서도 타입 안정성을 유지할 수 있다.
- 플러그인 시스템: Fastify의 플러그인 시스템은 코드를 모듈화하고 재사용하기 쉽게 만들어준다.
- 스키마 기반 검증: 내장된 JSON 스키마 검증을 통해 API 요청과 응답을 효과적으로 검증할 수 있다.
TypeScript
TypeScript를 선택한 이유는 다음과 같다:
- 타입 안정성: 정적 타입 검사를 통해 런타임 오류를 줄이고 코드 품질을 향상시킬 수 있다.
- IDE 지원: 코드 자동 완성, 리팩토링, 네비게이션 등 개발자 경험을 크게 향상시킨다.
- 유지보수성: 타입 정의를 통해 코드 문서화가 자연스럽게 이루어져 유지보수가 용이하다.
- 대규모 애플리케이션: 특히 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();
마무리
다음 글에서는 실제 구현에 대한 내용을 정리해보겠다.