본문 바로가기
프로젝트

24/10/10 - [팀] 타워 디펜스 게임(1): ERD 다이어그램, JSON 파일과 DB

by Jini_Lamp 2024. 10. 10.

돌고 돌아 다시 팀 프로젝트가 시작됐다.

 

이번에 만들 건 타워 디펜스 게임인데, 마찬가지로 클라이언트는 이미 주어진 상황에서 개발을 시작한다.

다만 이때까지와 다른 점이 있다면, 이번에는 정말 게임이 진행된다는 점? 물론 점핑 액션 게임도 있지만 팀플로는 이번이 처음이다.

 

먼저, 전체적인 게임의 흐름을 보면 다음과 같다.

1. 유저는 회원가입을 진행할 수 있고, 생성된 계정으로 로그인을 할 수 있다.

2. 게임을 시작할 때마다
2-1. 기본 골드가 5000원 지급
2-2. 현재 레벨: 1
2-3. 점수: 0 2-4. 초기 타워 개수: 3
2-5. 초기 타워HP: 300
2-6. 초기 타워 구입 비용: 1000
2-7. 몬스터 레벨: 1
2-8. 몬스터 초기 hp: 100
2-9. 몬스터 초기 공격력: 20
2-10. 몬스터 생성 주기: 5초 라는 데이터 값을 기준으로 게임이 시작된다.

3. 게임을 처음 시작하는거면 최고 점수는 0이다.
3-1. 이전에 게임을 진행한 기록이 있다면, 이전 기록 중에서 가장 높은 점수가 최고 점수로 되어 있다.

4. 타워의 위치(좌표)가 배열 형태로 저장되어 있어야 한다.

 

또한 클라이언트는 이미 다음과 같은 변수를 가지고 있다.

공통 데이터
1. let baseHp = 0; // 기지 체력
2. let userGold = 0; // 최초 유저 골드
3. let towerCost = 0; // 타워 구입 비용
4. let numOfInitialTowers = 0; // 초기 타워 개수

유저 데이터
1. let monsterLevel = 0; // 몬스터 레벨
2. let monsterSpawnInterval = 0; // 몬스터 생성 주기
3. let score = 0; // 게임 점수
4. let highScore = 0; // 기존 최고 점수
5. 타워 좌표들(배열)

 

이를 기준으로 했을 때, 처음 작성한 ERD 다이어그램은 다음과 같다.

 

 

 

첫번째 ERD 다이어그램

첫번째 ERD 다이어그램

 

먼저, 회원가입과 로그인 기능이 들어가므로 User 테이블이 필요하다.

GameState 테이블은 유저가 게임을 진행하는 동안 사용된다. 따라서 User 테이블과 1:N 관계를 맺었다.

Tower 테이블은 각 타워의 기본 능력치와 맵에 설치된 좌표를 저장한다. 게임을 진행하는 동안에는 여러개의 타워가 설치될 수 있기에 GameState 테이블과는 1:N 관계이다.

Monster 테이블 또한 각 몬스터의 기본 정보가 담겨 있으며, Tower 테이블과 마찬가지인 이유로 GameState와 1:N 관계이다.

CommonGameData 테이블은 공통 데이터를 저장하는 곳인데, 게임이 실행되어 GameState가 생성될 때마다 여기에 저장할 값들을 저장하고 있다. 해당 테이블은 게임이 시작할 때마 사용하므로 아무런 관계도 없다.

 

 

 

두번째 ERD 다이어그램

그런데 생각해보니, CommonGameData 테이블은 필요하지 않을 것 같다.

왜냐하면 GameState 테이블이 생성될 때, 기본 값으로 CommonGameData 테이블에 있는 값을 미리 너어주면 되기 때문이다.

그래서 나는 CommonGameData 테이블을 삭제했다.

 

또한 Tower 테이블과 Monster 테이블도 GameState 테이블과 관계를 맺을 필요 없이 독립적으로 존재해도 될 것 같다.

왜냐하면 해당 테이블에는 각각 타워와 몬스터의 데이터만 넣어주고, 생성하는 건 클라이언트 코드에서 Class를 통해 생성되니, 해당 부분을 아예 코드로 관리하면 될 것 같았기 때문이다.

다만 타워의 좌표 부분만 테이블로 관리하면 될 것 같아서, 두번째로 수정한 ERD 다이어그램은 다음과 같다.

두번째 ERD 다이어그램

더보기

1. User: 유저가 계정을 생성할 때 사용

2. GameState: 계정이 있는 유저가 게임을 시작할 때마다 생성(그래서 User 테이블과 1:N 관계)

3. Tower: 각 타워의 기본 정보가 담겨 있는 테이블. 유저가 게임을 진행하고 있을 때, 해당 테이블에서 값을 가져와 원하는 타워를 게임 맵에 생성할 때 사용된다.

4. Monster: 각 몬스터의 기본 정보가 담겨 있는 테이블. 유저가 게임을 진행하고 있을 때, 해당 테이블에서 값을 가져와 몬스터가 랜덤한 종류로 생성된다.

5. TowerPositon: 타워가 생성될 때마다 생성된 위치를 저장할 테이블. 따라서 GameState 테이블과는 1:N 관계이다. 즉, 게임 진행 중 타워가 생성될 때마다 데이터가 저장되는 테이블이다.

 

 

 

세번째 ERD 다이어그램

그런데 다시 또 문제가 생겼다.

두번째 ERD의 경우, 게임이 진행되는 동안 계속 GameState 테이블과 TowerPositon 테이블을 사용해야 한다. 그런데 이렇게 되면 게임의 실시간성과 반복적인 데이터 쓰기 작업 때문에 문제가 발생할 수 있다. 무엇보다 TowerPositon의 경우, 게임 진행 중에만 존재하는 임시적인 데이터이다. 이런 데이터를 실시간으로 DB에 저장하는 것은 비효율적이지 않을까?

 

아래는 게임 진행 중 실시간 상태를 관리하는 다른 방법이다.

이번 프로젝트에선 사용되지 않았지만, 공부를 위해 이곳에 몇 자 적어 놓는다.

더보기

1. 메모리 기반 상태 관리 (인메모리 캐시)

  • 게임이 진행되는 동안 GameState와 같은 데이터를 데이터베이스 대신 서버 메모리인메모리 캐시에서 관리하는 방법이 효율적입니다.
  • RedisMemcached와 같은 인메모리 데이터 저장소를 사용하면, 데이터를 메모리에 빠르게 저장하고 필요할 때 조회할 수 있습니다. 이러한 방식으로 실시간 게임 데이터에 대한 빠른 접근을 가능하게 할 수 있습니다.
  • 주요 게임 데이터는 게임이 끝나거나 특정 이벤트 시점에서만 DB에 기록하고, 실시간으로 변경되는 데이터는 메모리에서 관리하는 것이 성능을 최적화할 수 있습니다.

2. 주기적인 데이터 저장 (Checkpoints)

  • 게임이 진행되는 동안 모든 상태를 실시간으로 저장하는 대신, 주기적인 저장(Checkpoint) 방식을 사용할 수 있습니다. 게임이 시작할 때와 중요한 이벤트나 특정 시점마다 데이터를 DB에 저장하는 것입니다.
  • 예를 들어, 일정 주기(예: 5분마다) 또는 특정 트리거(레벨 업, 스테이지 클리어 등)가 발생했을 때 한 번에 데이터를 저장하면, 매번 데이터베이스에 쓰기 작업을 하지 않고도 데이터를 안정적으로 관리할 수 있습니다.

3. 세션 기반 상태 관리

  • 게임을 진행하는 동안에는 세션(Session) 또는 인스턴스 메모리를 사용하여 게임 상태를 유지합니다. 이렇게 하면 실시간으로 DB에 접근하는 대신 게임의 세션이 종료되기 전까지는 모든 상태를 메모리 상에서 관리할 수 있습니다.
  • 게임이 종료되거나 중단될 때만 최종 상태를 데이터베이스에 기록하는 방식으로 DB 접근을 최소화할 수 있습니다.

4. 비동기 처리 및 배치 저장

  • 게임 상태를 실시간으로 저장하는 대신, 상태 변경 사항을 비동기적으로 처리하거나, 배치(Batch) 저장 방식으로 한꺼번에 저장하는 방법도 고려할 수 있습니다.
  • 변경된 데이터를 바로 DB에 기록하지 않고 메모리에 임시 저장한 후, 비동기적으로 처리하여 주기적으로 DB에 반영하는 방식입니다. 이 방식은 게임의 실시간 성능을 유지하면서 데이터도 안정적으로 관리할 수 있게 합니다.

5. 게임 종료 시 저장

  • 게임이 종료되면 그동안의 게임 진행 데이터를 한 번에 저장하는 방식이 있습니다. 유저가 게임을 종료하거나, 게임 세션이 끝났을 때 그동안의 GameState를 DB에 기록하면 실시간 DB 접근을 최소화할 수 있습니다. 이 방식은 실시간 성능을 유지하면서 데이터 손실을 방지할 수 있습니다.
  • 또한, 중간에 유저가 강제 종료하거나 예상치 못한 오류가 발생했을 때는, 주기적으로 백업하거나 세이브하는 방식도 함께 사용할 수 있습니다.

 

여기서 내가 사용한 방법은 DB 트랜잭션을 최소화하는 방법이다.

먼저, GameState 테이블의 경우, 변화가 있을 때마다 계속 테이블에 접근할 필요가 없다. 어차피 최종적으로는 게임이 종료된 시점의 데이터들이 기록될테니, 마찬가지로 게임이 종료되는 시점에서 해당 테이블을 생성하고 데이터들을 저장해도 아무런 문제가 없다. 따라서 GameState 테이블을 GameScore 테이블로 바꿨다.

 

TowerPositon 테이블도 굳이 생성될 필요 없다.

왜냐하면 게임이 끝난 이후엔 더이상 사용될 일이 없는 데이터들이기 때문이다. 따라서 이는 추후 배열과 핸들러를 통해 관리하기로 하고, 해당 테이블을 삭제했다.

 

대신에 Stage 테이블이 생겼다.

생각해보니 해당 프로젝트에선 점수를 2000점 씩 모을 때마다 다음 스테이지로 넘어가는데, 그러기 위해선 각 스테이지마다의 정보가 필요하기 때문이다.

 

따라서 최종적으로 완성된 ERD 다이어그램은 다음과 같다.

최종 ERD 다이어그램

 

Monster, Tower, Stage 테이블은 각각 생성될 몬스터, 타워, 스테이지의 값을 저장하고 있다.

User 테이블은 GameScore 테이블과 1:N 관계를 맺고 있으며, GameScore 테이블은 게임이 종료된 후 최종적인 레벨과 점수, 게임이 끝난 시간 등을 저장한다.

User 테이블에는 highScore 라는 속성이 있는데, 해당 속성에는 GameScore 테이블에 저장된 값들 중 score가 가장 높은 값이 저장된다. 이렇게 한 이유는 GameScore 테이블을 score 순으로 정렬해도 되지만, 만약 GameScore 테이블에 값이 많이 저장되면 오히려 정렬을 하는 속도가 더 느려질 수도 있기 때문이다. User 테이블에 highScore가 있을 경우, GameScore 테이블에 새로운 값이 생길 때마다 비교를 해야하는 번거로움이 있지만 이를 제외하면 정렬로 인해 속도가 저하될 일은 없다.

 

 

 

JSON 파일 관리와 DB 관리

이전에 점핑 게임에는 아이템이나 스테이지 등을 JSON 파일로 관리했다.(링크)

하지만 이번에는 DB로 관리를 하게 됐는데, 이유는 다음과 같다.

  • 대규모 데이터 처리에 적합
    DB 는 큰 규모의 데이터를 효율적으로 저장하고 검색할 수 있다. 수천, 수만 개의 타워 데이터를 처리해야 하는 경우에도 성능이 유지된다.
  • 검색 및 필터링 가능
    SQL 쿼리를 사용해 특정 조건에 맞는 데이터를 쉽게 검색하고 필터링할 수 있다.
  • 동시성 처리
    DB는 여러 사용자가 동시에 데이터를 읽고 쓰더라도 충돌을 방지하고 일관성을 유지할 수 있다.
  • 트랜잭션 관리
    DB는 트랜잭션을 통해 데이터의 변경이 일관성 있게 처리되도록 보장한다. 한 번에 여러 데이터를 안전하게 수정하거나 추가할 수 있다.
  • 유연한 관계 관리
    DB에서는 데이터를 정규화하고 다른 테이블과 관계를 맺어 관리할 수 있어, 예를 들어 타워 종류별 특성을 다른 테이블에서 관리하는 등 확장성과 관리 용이성을 제공한다.

이처럼 DB에서는 데이터 양이 크고, 다양한 조건으로 데이터를 검색하거나, 다수의 서버 간의 동시성이 중요한 경우에 사용된다.

이번에는 게임을 혼자서 진행하는 방식이지만, 다음 프로젝트는 이번 프로젝트와 연계하여 다수의 사람이 동시에 접속하는 게임을 만들 예정이라고 한다.

따라서 이를 대비해, 데이터를 DB로 관리하는 게 맞겠다 생각이 들어 JSON 파일이 아닌 DB로 데이터를 관리하게 되었다.

JSON 파일 관리의 장점
1.간단하고 휴대성 높음
: 파일 시스템에 JSON 파일을 저장하고 불러오는 방식은 매우 간단하며, 파일 자체를 쉽게 수정하거나 옮길 수 있다.

2.비정형 데이터
: JSON은 구조가 유연하여, 같은 테이블 내에서 꼭 동일한 필드를 유지하지 않아도 된다. 즉, 특정 타워에만 추가 속성을 붙일 때 매우 유용하다.

3.오프라인 지원
: 데이터베이스 연결이 불가능한 환경에서 사용할 수 있으며, 로컬에서 데이터를 빠르게 불러올 수 있다.

4.쉽게 동기화 가능
: 버전 관리 시스템(예: Git)을 이용해 다양한 환경에서 동일한 데이터를 유지하고 동기화할 수 있다.

그래서 주로 데이터 양이 적고, 동적 업데이트나 검색이 자주 일어나지 않으며, 게임 클라이언트에서 빠르게 참조할 정적 데이터를 저장할 때 사용된다.
즉, 간단한 프로토타입이나 오프라인 게임 기능에서 빠르게 구현할 때 사용된다.