안녕하세요, 이중혁입니다

배우고 경험한 기술들을 하나씩 정리하는 공간

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"
  }
}