게임 서버 개발
실시간 멀티플레이어 게임 서버의 아키텍처, 네트워크 프로토콜,
게임 로직 구현 및 성능 최적화에 대해 학습하고 구현했습니다.
서버 아키텍처
확장 가능한 구조
네트워크 프로토콜
RESTful API & 실시간 통신 (개발 중)
게임 로직
핵심 시스템
성능 최적화
최적화 기법
게임 데모
게임 데모를 시작하려면 "게임 시작" 버튼을 클릭하세요.
게임은 새 창에서 실행되어 키보드 입력에 영향을 주지 않습니다.
정적 데이터 관리 시스템
구현 내용
정적 데이터 기본 구조
- • StaticBase Entity: 모든 정적 데이터의 기본 구조
- • ID: 고유 식별자
- • ENUM_ID: 열거형 식별자
- • 타입 안전성: TypeScript 인터페이스로 보장
StaticBaseRepository
- • LRU 캐싱: 최근 사용된 쿼리 결과 캐시
- • 메모리 저장: Record<number, T> 형태로 데이터 저장
- • 빠른 조회: ID 기반 O(1) 조회 성능
- • 동적 쿼리: 복잡한 조건으로 데이터 필터링
데이터 로딩 시스템
- • JSON 파일 기반: ./data/ 폴더의 JSON 파일에서 로드
- • class-transformer: plainToInstance로 객체 변환
- • 불변성 보장: deepFreeze로 객체 동결
- • 핫 리로드: 서버 재시작 없이 데이터 업데이트
관리되는 데이터 타입
- • 캐릭터 데이터: 이름, 티어, 무기, 공격력, 방어력, 체력
- • 가챠 데이터: 상태, 횟수, 티어 비율, 비용
- • 밸런스 데이터: 게임 밸런스 관련 수치
- • 비용 데이터: 아이템, 스킬 등의 비용 정보
- • 미션 데이터: 퀘스트, 미션 관련 정보
- • 보상 데이터: 미션, 이벤트 보상 정보
성능 최적화
- • LRU 캐시: 최대 1000개 쿼리 결과 캐시
- • 메모리 효율성: 불변 객체와 깊은 동결
- • 쿼리 최적화: JSON.stringify로 캐시 키 생성
- • 배치 조회: findByIdIn으로 여러 ID 한번에 조회
StaticRepository 통합 관리
- • 중앙 집중식 관리: 모든 정적 데이터 리포지토리 통합
- • 동적 리로드: 특정 데이터 타입만 선택적 리로드
- • 전체 리로드: 모든 데이터를 한번에 리로드
- • 버전 관리: DataVersionRepository로 버전 추적
정적 데이터 테스트
테스트 방법
- • 1단계: 드랍다운 클릭하여 데이터 타입 목록 로드
- • 2단계: 원하는 데이터 타입 선택
- • 3단계: 보라색 "전체 데이터 조회" 버튼으로 모든 데이터 조회
- • 4단계: ID 입력 후 파란색 "ID로 데이터 조회" 버튼 클릭
- • 5단계: 조건 설정 후 보라색 "조건부 쿼리" 버튼으로 필터링 조회
- • 지연 로딩: 드랍다운 클릭 시에만 API 호출
- • API 키 자동 관리: 캐싱 및 자동 갱신
데이터 조회 설정
조건부 쿼리 설정
export abstract class StaticBase {
ID: number;
ENUM_ID: string;
}컨텍스트 관리 시스템
CLS (Continuation Local Storage)
개념
- • 비동기 컨텍스트 관리: Node.js에서 비동기 작업 간 데이터 공유
- • 요청별 격리: 각 HTTP 요청마다 독립적인 컨텍스트
- • 미들웨어 통합: Express/NestJS 미들웨어와 자동 연동
- • 타입 안전성: TypeScript와 완벽 호환
주요 특징
- • 자동 컨텍스트 생성: 요청 시작 시 자동으로 컨텍스트 생성
- • 컨텍스트 전파: Promise 체인을 통한 자동 전파
- • 메모리 효율성: 요청 완료 시 자동 정리
- • 디버깅 지원: 컨텍스트 추적 및 로깅
nestjs-cls 설정
- • global: true: 전역 모듈로 등록
- • middleware.mount: true: 미들웨어 자동 마운트
- • 자동 초기화: 요청별 컨텍스트 자동 생성
- • 타입 지원: ClsService 타입 정의
Portfolio CLS 구조
ContextProvider
ClsService를 래핑한 편의 클래스
주요 데이터
요청별 사용자 및 데이터베이스 정보
import { ClsModule } from 'nestjs-cls';
@Module({
imports: [
ClsModule.forRoot({
global: true,
middleware: { mount: true }
}),
],
})
export class AppModule {}전역 예외 처리 시스템
AllExceptionFilter 개요
핵심 기능
- • 전역 예외 처리: 모든 예외를 캐치하여 일관된 응답 제공
- • 예외 분류: 비즈니스 에러와 시스템 에러를 구분
- • 표준화된 응답: 클라이언트에게 일관된 에러 형태 제공
- • 상세한 로깅: 디버깅을 위한 완전한 에러 정보 기록
주요 특징
- • @Catch(): 모든 예외를 캐치하는 전역 필터
- • HttpAdapterHost: HTTP 어댑터를 통한 응답 처리
처리 흐름
1. 예외 캐치
ArgumentsHost를 통해 HTTP 컨텍스트 접근
2. 예외 분류
ServerErrorException vs Unknown Error 구분
3. 응답 생성
HTTP 상태 코드와 에러 코드 설정
4. 로깅
요청 정보, 세션, 예외 스택 포함
@Catch()
export class AllExceptionFilter implements ExceptionFilter {
constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
catch(exception: any, host: ArgumentsHost): void {
const { httpAdapter } = this.httpAdapterHost;
const ctx = host.switchToHttp();
const isServerErrorException =
exception.response && isNumber(exception.response);
const httpStatusCode = isServerErrorException
? HttpStatus.OK
: (exception?.response?.statusCode ?? HttpStatus.INTERNAL_SERVER_ERROR);
const errorCode = isServerErrorException
? exception.response
: INTERNAL_ERROR_CODE.UNKNOWN_FATAL_ERROR;
const message = exception?.message;
const body = isServerErrorException
? ResponseEntity.error(errorCode, message)
: {
code: errorCode,
timestamp: ContextProvider.getNow()?.toISOString() ?? new Date(),
path: httpAdapter.getRequestUrl(ctx.getRequest()),
error: exception.name,
message: message,
};
httpAdapter.reply(ctx.getResponse(), body, httpStatusCode);
const requestContext = ctx.getRequest();
// logging
const request = {
url: requestContext.url,
method: requestContext.method,
headers: requestContext.headers,
body: requestContext.body,
};
const errorMessage = {
request: request,
session: undefined,
exception: isServerErrorException
? exception
: // unknown error
{
...body,
...(exception?.stack ? { stack: exception.stack } : {}),
},
};
Logger.error(errorMessage);
}
}재화 시스템 (Goods System)
게임 서버의 재화 소모, 지급, 관리 시스템
재화 시스템 개요
게임 서버의 핵심 비즈니스 로직인 재화 시스템을 담당하는 Provider 구조입니다.전략 패턴과 Facade 패턴을 활용하여 재화의 소모, 지급, 관리를 체계적으로 처리합니다.
ConsumptionProvider
- • 재화 소모: 사용자 재화 차감
- • 수량 검증: 보유량 확인
- • 타입별 처리: 통화, 아이템 등
- • 원자성 보장: 트랜잭션 처리
RewardProvider
- • 재화 지급: 보상 아이템 지급
- • 캐릭터 처리: 레벨업 vs 신규 획득
- • 중복 제거: 동일 재화 통합
- • 데이터 검증: 정적 데이터 확인
GoodsProvider
- • 통합 관리: 소모와 지급 통합
- • 데이터 검증: 비즈니스 규칙 적용
- • 병합 처리: 결과 통합 및 정리
- • Facade 패턴: 복잡한 로직 단순화
설계 패턴
- • 전략 패턴: 재화 타입별 처리 로직 분리
- • Facade 패턴: 복잡한 로직을 단순한 인터페이스로 제공
- • 의존성 주입: NestJS DI를 통한 느슨한 결합
- • 단일 책임 원칙: 각 Provider의 명확한 역할 분담
핵심 컴포넌트
- • Delegate 패턴: 타입별 처리 함수 위임
- • Map 기반 그룹핑: 재화 타입별 데이터 분류
- • 배치 처리: 여러 아이템 한번에 처리
- • 중복 제거: 동일 재화 통합 처리
데이터 흐름
- • 요청 데이터 검증 → 비즈니스 규칙 적용
- • 재화 타입별 그룹핑 → Map을 활용한 분류
- • Delegate 함수 호출 → 타입별 처리 로직 실행
- • 데이터베이스 업데이트 → 트랜잭션 처리
- • 결과 통합 및 반환 → 중복 제거 후 응답
성능 최적화
- • 배치 조회: findByIdIn() 메서드 활용
- • 메모리 효율성: Map을 활용한 그룹핑
- • 중복 제거: 불필요한 데이터 처리 최소화
- • 원자성: 트랜잭션을 통한 데이터 일관성
type DelegateFunctionReturn = CurrencyDto | CharacterDto;
type ConsumeDelegate = Partial<
Record<GoodsType, (materials: Cost[]) => Promise<DelegateFunctionReturn[]>>
>;
@Injectable()
export class ConsumptionProvider {
private readonly consumeDelegate: ConsumeDelegate = {
[GOODS_TYPE.CURRENCY]: (
materials: Cost[],
): Promise<DelegateFunctionReturn[]> => this._consumeCurrency(materials),
[GOODS_TYPE.CHARACTER]: (
materials: Cost[],
): Promise<DelegateFunctionReturn[]> => this._consumeCharacter(materials),
};
constructor(
private readonly userDetailProvider: UserDetailProvider,
private readonly dataCurrencyRepository: DataCurrencyRepository,
private readonly characterProvider: CharacterProvider,
private readonly dataCharacterProvider: DataCharacterProvider,
) {}
/**
* 소모
*/
async consume(materials: Cost[]): Promise<GoodsOutDto> {
const materialMap = new Map<GoodsType, Cost[]>();
for (const material of materials) {
const goodsType = material.goodsType;
materialMap.has(goodsType)
? materialMap.get(goodsType).push(material)
: materialMap.set(goodsType, [material]);
}
const results = [];
for (const [goodsType, materials] of [...materialMap]) {
const func = this.consumeDelegate[goodsType];
if (!func) {
throw new ServerErrorException(INTERNAL_ERROR_CODE.DATA_INVALID, [
`${goodsType}타입은 소모 할수 없는 재화 입니다.`,
]);
}
results.push(await func(materials));
}
return await this._distinctGoods(flatten(results));
}
/**
* 통화 소모 by enumId
*/
private async _consumeCurrency(materials: Cost[]): Promise<CurrencyDto[]> {
const userDetail = await this.userDetailProvider.getUserDetail();
// 각 material 의 재화는 이전에 통합을 통해 독립적인 dataId(currency Type)으로 들어온다.
for (const material of materials) {
const dataCurrency = this.dataCurrencyRepository.findById(
material.dataId,
);
if (!dataCurrency) {
throw new ServerErrorException(INTERNAL_ERROR_CODE.DATA_INVALID, [
`${material.dataId}는 재화 데이터에 존재하지 않은 아이디 입니다.`,
]);
}
const currencyProp = CURRENCY_PROPS[dataCurrency.CURRENCY_TYPE];
if (!currencyProp) {
throw new ServerErrorException(
INTERNAL_ERROR_CODE.USER_DETAIL_UNKNOWN_CURRENCY_TYPE,
);
}
// 통화 수량 확인
if (userDetail.currency[currencyProp] < material.quantity) {
const errorCode = `USER_DETAIL_CURRENCY_${currencyProp.toUpperCase()}_NOT_ENOUGH`;
throw new ServerErrorException(INTERNAL_ERROR_CODE[errorCode]);
}
userDetail.currency[currencyProp] -= material.quantity;
}
await this.userDetailProvider.updateUserDetail(userDetail);
return [CurrencyDto.fromEntity(userDetail.currency)];
}
/**
* 통화 소모 by enumId
*/
private async _consumeCharacter(materials: Cost[]): Promise<CharacterDto[]> {
const dataIds = materials.map((it) => it.dataId);
const characters =
await this.characterProvider.getCharactersByDataCharacterIds(dataIds);
const characterByDataCharacterId = Object.fromEntries(
characters.map((it) => [it.dataCharacterId, it]),
);
for (const material of materials) {
const { dataId, quantity } = material;
const character = characterByDataCharacterId[dataId];
const dataCharacter = this.dataCharacterProvider.getDataCharacter(dataId);
if (!dataCharacter) {
throw new ServerErrorException(INTERNAL_ERROR_CODE.DATA_INVALID, [
`${dataId}는 캐릭터 데이터에 존재하지 않은 아이디 입니다.`,
]);
}
if (!character) {
throw new ServerErrorException(
INTERNAL_ERROR_CODE.CHARACTER_NOT_FOUND,
[dataId],
);
}
// 통화 수량 확인
if (character.count < quantity) {
throw new ServerErrorException(
INTERNAL_ERROR_CODE.CHARACTER_NOT_ENOUGH,
[dataId],
);
}
character.count -= quantity;
}
await this.characterProvider.saveCharacters(characters);
return CharacterDto.fromEntities(characters);
}
/**
* 중복 재화 처리
*/
private async _distinctGoods(
results: DelegateFunctionReturn[],
): Promise<GoodsOutDto> {
// 통화 중복 처리
const currencyDto = await this._distinctCurrency(
results.filter((it) => it instanceof CurrencyDto) as CurrencyDto[],
);
const goodsDto = GoodsOutDto.of({
currency: currencyDto,
});
return ObjectUtil.removeEmptyProperties(goodsDto);
}
/**
* 중복 통화 처리
*/
private async _distinctCurrency(
currenciesDto: CurrencyDto[],
): Promise<CurrencyDto> {
if (currenciesDto.isEmpty()) {
return null;
}
if (currenciesDto.length === 1) {
return currenciesDto[0];
}
const userDetail = await this.userDetailProvider.getUserDetail();
return CurrencyDto.fromEntity(userDetail.currency);
}
}업적 관리 시스템
게임 내 업적과 미션을 효율적으로 관리하는 시스템으로, CLS 기반 상태 관리와 확장 가능한 아키텍처를 제공합니다.
시스템 개요
업적 관리 시스템은 게임 내 다양한 업적과 미션을 효율적으로 처리하는 핵심 시스템입니다. CLS(Continuation Local Storage)를 활용한 상태 관리와 확장 가능한 아키텍처로 설계되었습니다.
핵심 기능
- • 업적 등록 → CLS 기반 임시 저장
- • 미션 진행도 업데이트 → 실시간 진행도 관리
- • 누적 미션 처리 → 단계별 미션 자동 생성
- • 배치 처리 → 여러 업적 동시 처리
설계 원칙
- • 확장성 → 새로운 업적 타입 추가 용이
- • 성능 → 배치 처리 및 중복 제거
- • 유지보수성 → 명확한 책임 분리
- • 일관성 → CLS 기반 상태 관리
시스템 아키텍처
3계층 구조로 설계된 업적 관리 시스템은 각 계층이 명확한 역할을 가지며, 확장 가능한 구조를 제공합니다.
AchievementProvider
Facade Pattern
- • 업적 등록 및 관리
- • CLS 기반 상태 관리
- • 업적 처리 통합
BaseAchievementProvider
Abstract Base
- • 공통 업적 처리 로직
- • 미션 필터링
- • 다음 단계 미션 처리
MissionAchievementProvider
Concrete Implementation
- • 미션 진행도 업데이트
- • 누적 미션 처리
- • DB 저장 및 관리
데이터 흐름
- • 업적 발생 → AchievementProvider.achieve() 호출
- • CLS 저장 → ContextProvider에 업적 정보 저장
- • 업적 병합 → 동일 업적 count 합산
- • 미션 처리 → MissionAchievementProvider에서 진행도 업데이트
- • 다음 단계 → 누적 미션의 다음 단계 자동 생성
- • DB 저장 → 변경된 미션 정보 저장
- • 정리 → CLS에서 업적 정보 제거
import { Injectable } from '@nestjs/common';
import { MissionAchievementProvider } from '@libs/common/provider/achievement/mission-achievement.provider';
import { ContextProvider } from '@libs/common/provider/context.provider';
import { AchievementDelegateFunctionReturn, BaseAchievement } from '@libs/common/interface/base-achievement.interface';
import { MissionConditionType } from '@libs/dao/static/common.constants';
import { Achievement } from '@libs/common/interface/achievement.interface';
@Injectable()
export class AchievementProvider {
private readonly achievementDelegate: BaseAchievement[] = [];
constructor(
private readonly missionAchievementProvider: MissionAchievementProvider,
) {
this.achievementDelegate.push(this.missionAchievementProvider);
}
/**
* 업적 등록
*/
static achieve(
missionConditionType: MissionConditionType,
conditions: number[],
count = 1,
): void {
const achievement = {
missionConditionType: missionConditionType,
conditions: conditions,
count: count,
};
const achievements = AchievementProvider.getAchievements();
achievements.push(achievement);
ContextProvider.set('achievements', achievements);
}
/**
* 업적 처리
*/
async updateAchievements(): Promise<AchievementDelegateFunctionReturn> {
const achievements: Achievement[] = AchievementProvider.getAchievements();
if (achievements.isEmpty()) {
return;
}
// 같은 업적 count 병합
const achievementsMap = new Map<string, Achievement>();
for (const achievement of achievements) {
const { missionConditionType, conditions, count } = achievement;
const key = `${missionConditionType}_${JSON.stringify(conditions)}`;
const existAchievement = achievementsMap.get(key);
existAchievement
? (existAchievement.count += count)
: achievementsMap.set(key, achievement);
}
// 업적 달성
const allAchievementResult = [];
for (const achievementDelegate of this.achievementDelegate) {
const achievementResult = await achievementDelegate.achieve([
...achievementsMap.values(),
]);
if (achievementResult) {
allAchievementResult.push(...achievementResult);
}
}
AchievementProvider.releaseAchievements();
return allAchievementResult;
}
static getAchievements(): Achievement[] {
return ContextProvider.get('achievements') ?? [];
}
static releaseAchievements(): void {
ContextProvider.set('achievements', undefined);
}
}캐릭터 시스템 (Character System)
게임 내 캐릭터 관리, 레벨업, 조회 기능을 제공하는 시스템으로, 재화 시스템과 업적 시스템과 연동됩니다.
시스템 개요
캐릭터 시스템은 게임 내 캐릭터의 조회, 레벨업, 관리를 담당하는 핵심 시스템입니다.재화 시스템과 업적 시스템과 밀접하게 연동되어 통합적인 게임 경험을 제공합니다.
핵심 기능
- • 캐릭터 조회 → 사용자 소유 캐릭터 목록
- • 레벨업 → 재화 소모를 통한 캐릭터 강화
- • 데이터 관리 → 캐릭터 정보 저장 및 업데이트
- • 업적 연동 → 레벨업 시 업적 자동 등록
시스템 구조
- • Controller → API 엔드포인트 제공
- • Service → 비즈니스 로직 처리
- • Provider → 캐릭터 데이터 관리
- • DTO → 데이터 전송 객체
시스템 아키텍처
계층화된 구조로 설계되어 각 계층이 명확한 역할을 가지며, 다른 시스템과의 연동이 용이합니다.
CharacterController
API Layer
- • 캐릭터 조회 API
- • 레벨업 API
- • 인증 및 권한 관리
CharacterService
Business Logic
- • 레벨업 비즈니스 로직
- • 재화 소모 처리
- • 업적 시스템 연동
CharacterProvider
Data Access
- • 캐릭터 데이터 조회
- • 캐릭터 정보 저장
- • 레벨 업데이트
데이터 모델
- • CharacterDto → API 응답용 데이터 전송 객체
- • Character Interface → 도메인 모델 정의
- • CharacterLevelUpInDto → 레벨업 요청 데이터
@Controller('game/character')
@ApiTags('Character')
@Auth()
export class CharacterController {
constructor(private readonly characterService: CharacterService) {}
@Post('get')
@ApiResponseEntity({
type: CharacterDto,
isArray: true,
summary: '캐릭터 정보',
})
async getCharacters(): Promise<ResponseEntity<CharacterDto[]>> {
const characterDto = await this.characterService.getCharacters();
return ResponseEntity.ok().body(characterDto);
}
@Post('level-up')
@ApiResponseEntity({
type: CharacterDto,
summary: '캐릭터 레벨 업',
})
async levelUpCharacter(
@Body() characterLevelUpInDto: CharacterLevelUpInDto,
): Promise<ResponseEntity<Character>> {
const characterDto = await this.characterService.levelUpCharacter(
characterLevelUpInDto,
);
return ResponseEntity.ok().body(characterDto);
}
}배틀 시스템 (Battle System)
게임 내 전투 입장, 종료, 랭킹 시스템을 제공하는 핵심 시스템으로, Redis 기반 실시간 랭킹과 재화 보상 시스템과 연동됩니다.
시스템 개요
배틀 시스템은 게임의 핵심 전투 기능을 담당하는 시스템으로, 전투 입장부터 종료, 랭킹 관리까지 전체적인 전투 생명주기를 관리합니다.Redis를 활용한 실시간 랭킹과트랜잭션 기반의 안전한 데이터 처리를 제공합니다.
핵심 기능
- • 전투 입장 → 캐릭터 검증 및 전투 세션 생성
- • 전투 종료 → 결과 처리 및 보상 지급
- • 랭킹 관리 → Redis 기반 실시간 랭킹
- • 보상 시스템 → 시간/킬 기반 보상 계산
시스템 구조
- • BattleController → API 엔드포인트 제공
- • BattleService → 비즈니스 로직 처리
- • BattleProvider → 전투 보상 계산
- • SoloBattleProvider → 솔로 랭킹 관리
시스템 아키텍처
계층화된 구조로 설계되어 각 계층이 명확한 역할을 가지며, Redis와 MySQL을 적절히 활용한 하이브리드 데이터 관리 구조를 제공합니다.
BattleController
API Layer
- • 전투 입장 API
- • 전투 종료 API
- • 랭킹 조회 API
BattleService
Business Logic
- • 전투 세션 관리
- • 결과 처리 로직
- • 트랜잭션 관리
Provider Layer
Data & Logic
- • BattleProvider
- • SoloBattleProvider
- • GoodsProvider 연동
데이터 저장소
- • Redis → 전투 세션, 실시간 랭킹
- • MySQL → 점수 기록, 전투 히스토리
- • 정적 데이터 → 보상 테이블, 밸런스
@Controller('game/battle')
@ApiTags('Battle')
@Auth()
export class BattleController {
constructor(private readonly battleService: BattleService) {}
@Post('solo/enter')
@ApiResponseEntity({
type: BattleEnterOutDto,
summary: '전투 입장',
})
@UserLevelLock()
async enterBattle(
@Body() battleEnterInDto: BattleEnterInDto,
): Promise<ResponseEntity<BattleEnterOutDto>> {
const battleEnterOutDto =
await this.battleService.enterBattle(battleEnterInDto);
return ResponseEntity.ok().body(battleEnterOutDto);
}
@Post('solo/finish')
@ApiResponseEntity({
type: BattleFinishOutDto,
summary: '전투 종료',
})
@UserLevelLock()
async finishBattle(
@Body() battleFinishInDto: BattleFinishInDto,
): Promise<ResponseEntity<BattleFinishOutDto>> {
const battleFinishOutDto =
await this.battleService.finishBattle(battleFinishInDto);
return ResponseEntity.ok().body(battleFinishOutDto);
}
@Post('solo/ranking/get')
@ApiResponseEntity({
type: GetSoloRankingOutDto,
summary: '솔로 랭킹',
})
async getSoloRanking(
@Body() getSoloRankingInDto: GetSoloRankingInDto,
): Promise<ResponseEntity<GetSoloRankingOutDto>> {
const getSoloRankingOutDto =
await this.battleService.getSoloRanking(getSoloRankingInDto);
return ResponseEntity.ok().body(getSoloRankingOutDto);
}
@Post('multi/ranking/get')
@ApiResponseEntity({
type: GetSoloRankingOutDto,
summary: '솔로 랭킹',
})
async getMultiRanking(
@Body() getSoloRankingInDto: GetSoloRankingInDto,
): Promise<ResponseEntity<GetSoloRankingOutDto>> {
return ResponseEntity.ok().body(GetSoloRankingOutDto.of({
totalCount: 0,
myRanking: null,
ranking: [],
}));
}
}상점 시스템 (Shop System)
게임 내 가챠 시스템을 제공하는 핵심 시스템으로, 확률 기반 보상 지급과 재화 소모를 통한 캐릭터 획득 기능을 제공합니다.
시스템 개요
상점 시스템은 게임의 핵심 수익화 기능인 가챠 시스템을 담당하는 시스템으로, 확률 기반의 보상 지급과 재화 소모를 통한 캐릭터 획득 기능을 제공합니다.정적 데이터 기반의 확률 테이블과트랜잭션 기반의 안전한 거래 처리를 제공합니다.
핵심 기능
- • 가챠 실행 → 확률 기반 캐릭터 획득
- • 재화 소모 → 가챠 비용 차감
- • 보상 지급 → 획득한 캐릭터 지급
- • 업적 연동 → 가챠 횟수 업적 등록
시스템 구조
- • ShopController → API 엔드포인트 제공
- • ShopService → 비즈니스 로직 처리
- • GachaProvider → 가챠 확률 계산
- • GoodsProvider → 재화 관리
시스템 아키텍처
계층화된 구조로 설계되어 각 계층이 명확한 역할을 가지며, 정적 데이터 기반의 확률 테이블과 재화 시스템과의 연동을 통한 안전한 가챠 처리를 제공합니다.
ShopController
API Layer
- • 가챠 실행 API
- • 인증 및 권한 관리
- • 사용자 레벨 락
ShopService
Business Logic
- • 가챠 비즈니스 로직
- • 재화 소모 처리
- • 트랜잭션 관리
Provider Layer
Data & Logic
- • GachaProvider
- • GoodsProvider
- • AchievementProvider
데이터 저장소
- • 정적 데이터 → 가챠 확률 테이블, 캐릭터 데이터
- • MySQL → 사용자 재화, 캐릭터 보유 현황
- • 랜덤 유틸 → 확률 계산 및 랜덤 선택
@Controller('game/shop')
@ApiTags('Shop')
@Auth()
export class ShopController {
constructor(private readonly shopService: ShopService) {}
@Post('gacha')
@ApiResponseEntity({
type: GachaOutDto,
summary: '가챠',
})
@UserLevelLock()
async gacha(@Body() gachaInDto: GachaInDto): Promise<ResponseEntity<GachaOutDto>> {
const gachaOutDto = await this.shopService.gacha(gachaInDto);
return ResponseEntity.ok().body(gachaOutDto);
}
}