MySQL
관계형 데이터베이스 (RDBMS)
관계형 데이터베이스란?
테이블(Table) 형태로 데이터를 저장하고, 테이블 간의 관계를 통해 데이터를 연결하는 데이터베이스 시스템
핵심 특징
- • 테이블 기반: 행(Row)과 열(Column) 구조
- • 스키마: 명확한 스키마로 데이터 무결성 보장
- • SQL: 표준 질의 언어로 데이터 조작
- • 트랜잭션: ACID 속성으로 일관성 유지
관계의 종류
- • One-to-One (1:1): 한 레코드가 다른 테이블의 한 레코드와만 연결 (예: 사용자-프로필)
- • One-to-Many (1:N): 한 레코드가 여러 레코드와 연결 (예: UserDetail 1 ⇄ N Character, UserDetail 1 ⇄ N Mission)
- • Many-to-Many (N:M): 양쪽 모두 여러 레코드와 연결 (조인 테이블 필요)
게임 서버 설계
- • FK 컬럼명 통일: 자식 테이블의 FK 컬럼을
user_id로 통일 - • 인덱스:
user_id와 사용 빈도 높은 복합 인덱스((user_id, data_*)) - • ORM:
@JoinColumn({ name: 'user_id', referencedColumnName: 'id' }) - • 제약 운영:
createForeignKeyConstraints: false인 경우, 서비스 레벨 정합성(존재 확인·트랜잭션·정합성 배치) 보완이 필수
@Entity('user_detail')
@BaseTimeEntity()
export class UserDetailEntity extends AbstractEntity {
@Index({ unique: true })
@Column({ comment: '유저 id', unsigned: true })
userId: number;
@OneToMany(() => CharacterEntity, (character) => character.userDetail)
characters: Relation<Promise<any[]>>;
@OneToMany(() => MissionEntity, (mission) => mission.userDetail)
missions: Relation<Promise<any[]>>;
}커넥션 풀 관리
데이터베이스 샤딩
샤딩 전략
- • 수평 분할: game00, game01로 데이터 분산
- • 사용자 기반: 사용자 ID로 샤드 결정
- • 로드 밸런싱: 트래픽 분산으로 성능 향상
- • 확장성: 필요시 game02, game03 추가 가능
샤드 분배 로직
- • 해시 기반: 사용자 ID 해시값으로 샤드 결정
- • 모듈로 연산: user_id % 2 = 0 → game00, 1 → game01
- • 일관성: 동일 사용자는 항상 같은 샤드
- • 균등 분배: 50:50 비율로 데이터 분산
샤딩 장점
- • 성능 향상: 쿼리 병렬 처리
- • 확장성: 수평적 확장 가능
- • 가용성: 한 샤드 장애 시 다른 샤드 정상 동작
- • 백업 효율성: 샤드별 독립 백업
샤딩 고려사항
- • 트랜잭션: 크로스 샤드 트랜잭션 복잡
- • 조인: 샤드 간 조인 불가
- • 리밸런싱: 데이터 재분배 시 복잡
- • 모니터링: 샤드별 성능 모니터링 필요
Portfolio 샤딩 구현
실제 구현
- • GAME00_DB: game00 데이터베이스
- • GAME01_DB: game01 데이터베이스
- • COMMON_DB: 공통 데이터 (사용자 정보 등)
- • 샤드 라우팅: 사용자 ID 기반 자동 라우팅
샤드 데이터베이스
GAME00_DB_HOST: portfolio-manager-database
GAME00_DB_NAME: game00
GAME01_DB_HOST: portfolio-manager-database
GAME01_DB_NAME: game01
샤드 분배 로직
user_id % 2 == 0 → game00
user_id % 2 == 1 → game01
일관된 해시 함수 사용
동일 사용자 → 동일 샤드
데이터 분류
COMMON: 사용자 정보, 인증
GAME00: 게임 데이터 (짝수 사용자)
GAME01: 게임 데이터 (홀수 사용자)
독립적인 스키마 구조
샤딩 모니터링
샤드별 연결 수 모니터링
샤드별 쿼리 성능 추적
데이터 분산 비율 확인
샤드 장애 감지 및 알림
export default registerAs('base-database', () => ({
type: 'mysql' as const,
charset: 'utf8mb4',
timezone: 'Z',
extra: {
// 커넥션 풀 설정
connectionLimit: Number(process.env.DB_CONNECTION_LIMIT) || 10, // 최대 연결 수 (기본 10개)
acquireTimeout: Number(process.env.DB_ACQUIRE_TIMEOUT) || 60000, // 연결 획득 타임아웃 (60초)
timeout: Number(process.env.DB_TIMEOUT) || 60000, // 쿼리 실행 타임아웃 (60초)
reconnect: true, // 연결 끊김 시 자동 재연결
// 성능 최적화 설정
supportBigNumbers: true, // 큰 숫자 지원 (BIGINT 등)
bigNumberStrings: false, // 큰 숫자를 문자열로 반환하지 않음
dateStrings: false, // 날짜를 문자열로 반환하지 않음
// SSL 설정 (운영환경에서만)
ssl: process.env.NODE_ENV === 'production' ? {
rejectUnauthorized: false // SSL 인증서 검증 비활성화 (운영환경)
} : false,
// 기타 설정
multipleStatements: false, // 다중 쿼리 실행 비활성화 (보안)
debug: false, // 디버그 모드 비활성화
trace: false, // 트레이스 모드 비활성화
},
maxQueryExecutionTime: 1000,
logging: [],
bigNumberStrings: false,
namingStrategy: new SnakeNamingStrategy(),
}));락 처리 메커니즘
MySQL 네임드 락
GET_LOCK 함수
- • GET_LOCK: 사용자별 고유 락 생성
- • RELEASE_LOCK: 락 해제
- • Timeout 설정: 락 대기 시간 제한
- • Connection 기반: 연결별 락 관리
특징
- • 애플리케이션 레벨: DB 테이블과 무관
- • 이름 기반: 문자열로 락 식별
- • 자동 해제: 연결 종료 시 자동 해제
- • 재진입 가능: 동일 연결에서 재획득 가능
사용자 레벨 락 구현
- • @UserLevelLock: 데코레이터로 간편한 락 적용
- • Guard 기반: NestJS Guard로 락 처리
- • ContextProvider: 세션 정보 활용
- • 자동 관리: 획득/해제 자동화
사용 사례
- • 사용자별 락킹: 동일 사용자 동시 접근 방지
- • 리소스 보호: 외부 API 호출 제한
- • 배치 작업: 중복 실행 방지
- • 분산 락: 여러 서버 간 동기화
교착상태 방지
- • 타임아웃 설정: 무한 대기 방지
- • 락 순서 통일: 일관된 락 획득 순서
- • 재시도 로직: 실패 시 재시도
- • 락 범위 최소화: 필요한 최소 범위만 락킹
성능 최적화
- • 락 시간 단축: 빠른 락 해제
- • Connection Pool: 연결 재사용
- • 비동기 처리: 락이 필요 없는 작업 분리
- • 캐싱 활용: 락이 필요한 작업 최소화
테스트
테스트 방법
- • 1단계: 로그인으로 Session ID와 User ID 발급
- • 2단계: 락 테스트 실행
- • 동시 접근: 여러 탭에서 동시 테스트
- • 락 대기: 락이 걸린 상태에서 대기 확인
사용자 정보
@Controller('database')
export class DatabaseController {
@Post('user-level-lock')
@ApiResponseEntity({
summary: '락 네임드 락 확인하기',
})
@Auth()
@UserLevelLock()
async testUserLevelLock(
@Body() userLevelLockInDto: UserLevelLockInDto,
): Promise<ResponseEntity<number>> {
const goodsOutDto = await this.databaseService.testUserLevelLock(
userLevelLockInDto.addGold,
);
return ResponseEntity.ok().body(goodsOutDto.currency.gold);
}
@Post('user-level-unlock')
@ApiResponseEntity({
summary: '언락 네임드 락 확인하기',
})
@Auth()
async testUserLevelUnlock(
@Body() userLevelLockInDto: UserLevelLockInDto,
): Promise<ResponseEntity<number>> {
const goodsOutDto = await this.databaseService.testUserLevelLock(
userLevelLockInDto.addGold,
);
return ResponseEntity.ok().body(goodsOutDto.currency.gold);
}
@Post('row-level-lock')
@ApiResponseEntity({
summary: '락 행 락 확인하기',
})
async testRowLevelLock(
@Body() rowLevelLockInDto: RowLevelLockInDto,
): Promise<ResponseEntity<number>> {
const testTable =
await this.databaseService.testRowLevelLock(rowLevelLockInDto);
return ResponseEntity.ok().body(testTable.count);
}
@Post('row-level-unlock')
@ApiResponseEntity({
summary: '언락 행 락 확인하기',
})
async testRowLevelUnlock(
@Body() rowLevelLockInDto: RowLevelLockInDto,
): Promise<ResponseEntity<number>> {
const testTable = await this.databaseService.testRowLevelUnlock(
rowLevelLockInDto
);
return ResponseEntity.ok().body(testTable.count);
}
}트랜잭션 처리 시스템
구현 내용
트랜잭션 기본 원리
- • ACID 속성 보장
- • 원자성(Atomicity): 모두 성공 또는 모두 실패
- • 일관성(Consistency): 데이터 무결성 유지
- • 격리성(Isolation): 동시 실행 트랜잭션 분리
- • 지속성(Durability): 커밋된 변경사항 영구 저장
다중 DB 트랜잭션
- • Common DB: 사용자 Role 정보 관리
- • Game DB: 게임 내 Gold 정보 관리
- • 분산 트랜잭션: 여러 DB 간 일관성 보장
- • 에러 포인트: 의도적 실패로 롤백 테스트
데이터 일관성 보장
- • @Transactional 데코레이터: 자동 트랜잭션 관리
- • TypeORM QueryRunner: 다중 DB 트랜잭션
- • ContextProvider: 세션 기반 DB 선택
- • UserLevelLock: 사용자별 락킹
롤백 & 복구 메커니즘
- • TransactionInterceptor: 자동 커밋/롤백
- • TypeOrmHelper.rollbackTransactions(): DB 롤백
- • RedisHelper.rollbackMulti(): Redis 롤백
- • ServerErrorException: 의도적 에러 발생
- • Error Point 1: Role 업데이트 후 롤백
- • Error Point 2: Gold 업데이트 후 롤백
- • Error Point 3: 세션 업데이트 후 롤백
TypeORM 구현
- • QueryRunner: 트랜잭션 관리자
- • startTransaction(): 트랜잭션 시작
- • commitTransaction(): 변경사항 확정
- • rollbackTransaction(): 변경사항 취소
트랜잭션 테스트
테스트 방법
- • 1단계: 로그인으로 Session ID와 User ID 발급
- • 2단계: Role/Gold/Error Point 설정
- • 3단계: 트랜잭션 테스트 실행
- • Error Point 0: 정상 트랜잭션 (성공)
- • Error Point 1: 1번 포인트 실패 (롤백)
- • Error Point 2: 2번 포인트 실패 (롤백)
- • Error Point 3: 3번 포인트 실패 (롤백)
사용자 정보
트랜잭션 테스트 설정
@ApiTags('database')
@Controller('database')
export class DatabaseController {
@Post('transaction')
@ApiResponseEntity({
summary: '트랜잭션 확인하기',
})
@Auth()
@UserLevelLock()
async testTransaction(
@Body() transactionInDto: TransactionInDto,
): Promise<ResponseEntity<string>> {
await this.databaseService.testTransaction(transactionInDto);
return ResponseEntity.ok().body('업데이트 성공');
}
}TypeORM 마이그레이션 (synchronize 없이 Dry-run 적용)
왜 synchronize=false 인가?
- • 안정성: 자동 스키마 변경 위험 제거
- • 가시성: 실제 적용 전 변경 SQL을 검토
- • 감사/릴리즈: 버전별 SQL 산출물 보관
구성
- • scripts: migration:create/run/dry-run/revert
- • 멀티 DB: 데이터베이스별 config 분리
- • Dry-run: 실행 로그 파싱해 SQL 파일 생성
{
"scripts": {
"migration:run": "npm run typeorm -- migration:run",
"migration:create": "node libs/dao/src/migration/migration-exec-file/migration-create-file.ts",
"migration:dry-run": "node libs/dao/src/migration/migration-exec-file/migration-dry-run.ts",
"migration:revert": "node libs/dao/src/migration/migration-exec-file/migration-revert-file.ts"
}
}