해당 블로그의 내용은 게임 서버 프로그래밍 교과서의 내용을 요약, 정리한 내용입니다.
SQL 기초에 관한 부분은 생략했습니다.

플레이어의 정보 저장

싱글플레이 게임을 만들 때 플레이어 정보는 보통 플레이어의 로컬 저장소 안에 저장한다.
저장 형태는 텍스트이거나 바이너리이며, 해킹을 방지하고자 암호화된 형태로 저장하기도 한다.

하지만, 온라인게임에서 이러한 방식을 사용한다면,

  1. 플레이어가 자리를 옮겼을 때 자기가 플레이하던 정보를 이어서 할 수가 없다.
  2. 플레이어가 해킹을 할 줄 안다면 플레이 정보를 조작할 수 있다.

따라서 온라인 게임에서는 플레이어 정보를 클라이언트가 아닌 서버에만 저장한다.
서버에 데이터를 저장하는 방법은 크게 두가지가 있는데, 단순 파일과 데이터베이스를 이용하는 방법이다.

데이터베이스 vs 단순 파일

단순 파일로 저장하는 방법은 플레이어 각각의 데이터를 저장하거나 불러오는 속도가 빠르지만 그 외 데이터 관리, 분석, 데이터 백업 및 복원 기능은 데이터베이스가 훨씬 빠르다.

데이터는 단순 파일과는 다르게 둘 이상의 프로세스나 스레드가 동시에 액세스하는 상황을 중재해 준다. 따라서 데이터 경쟁 현상이 발생하지 않는다.

데이터베이스에 잘못된 범위의 값이 들어가거나 잘못된 조건 값이 들어가는 경우를 방지할 수 있다.

데이터베이스의 데이터 구성

데이터베이스는 표(table) 형태의 집합이다.
테이블의 집합은 데이터베이스 인스턴스라 한다.

데이터베이스에는 행 단위로 데이터를 넣거나 뺄 수 있다.
이를 레코드라 한다.
레코드 안에는 표의 열이 있는데, 이를 필드라고 한다.
필드는 이름 말고도 타입이라는 것을 추가로 가지고 있다.

SQL 질의 구문(query)

데이터베이스에 액세스 할 때 필수적으로 알아야 할 문법.

CRUD(create, read, update, delete)

데이터베이스에 액세스하면서 주로 하는 것은 추가하기, 읽기, 변경, 삭제하기이다. 이를 모아서 CRUD라 한다.

insert

새 레코드를 삽입하는 방법은 insert를 이용하는 방법이다.

insert into table1 (a,b,c) values (1,2,3)

table1에 a=1, b=2, c=3이 들어 있는 새 레코드를 추가하는 query이다.

select

table1에 있는 레코드를 얻으려면 select로 시작하는 문법을 사용한다.

select a,b,c from table1 where a=1

table1에 있는 레코드 중 필드 a가 1인 레코드를 찾는다.

update

table1안의 레코드 필드 값을 변경하는 것은 update로 시작하는 구문이다.

update table1 set b=2 where a=1

table1에 있는 레코드 중 필드 a가 1인 레코드를 찾아 그것의 필드 b를 2로 변경한다.

delete

table1에 있는 레코드 중 조건에 맞는 레코드를 찾아 삭제를 할 때는 delete를 사용한다.

delete from table1 where a=1

table1에 있는 레코드 중 필드a인 것을 모두 삭제한다.

인덱스와 키

인덱스는 필드 단위로 설정할 수 있다.
인덱스를 설정해 놓으면 특정 조건에서 어마어마하게 빠른 속도로 원하는 레코드를 찾을 수 있다.

인덱스는 검색 뿐만이 아니라 기존 레코드를 변경하거나 레코드를 삭제할 때에도 도움이 된다.

테이블 하나에 인덱스를 2개 이상 넣는 것도 가능하다.

인덱스는 레코드 둘 이상을 찾아낸 후 그것들을 정렬하는 용도로 사용할 수 있다.
인덱스는 검색 뿐만이 아니라 중복된 값을 방지하는데에도 사용할 수 있다.

인덱스의 단점은, 인덱스가 걸쳐 있는 레코드에 변화가 일어날 때, 인덱스도 같이 업데이트 해야 한다는 점이다. 따라서 필요하지 않은 인덱스는 가급적 사용하지 않는 것이 좋다.

또한 데이터에는 primary key가 있다. 이는 다음과 같은 특징을 갖는다.

  • 한 테이블에 하나만 추가할 수 있다.
  • 중복을 허락하지 않는다. 즉, 한 테이블에 값이 같은 코드가 2개 이상 들어갈 수 없다.
  • 필드 값은 null이 허락되지 않는다.

플레이어 정보를 데이터베이스에 저장하는 방법1

플레이어 데이터를 저장하다 보면 데이터가 트리 형태로 저장될 수 있다.
이러한 트리 형태의 데이터를 저장하는 방법은

  1. 플레이어 데이터 전체를 문서 형태로 만들어서 테이블에 넣는다.
  2. 플레이어 데이터를 구성하는 트리 노드 각각을 테이블에 넣는다.

먼저 첫번째 방법은, 플레이어 데이터 형태를 Json 문서 형태로 넣는 것이다. Json 문서는 트리 구조를 문자열 형태로 표현한다.

하지만 Json문자열의 길이가 얼마나 길어질지 장담할 수 없기 때문에 문자열 길이가 제한되지 않는 text타입의 필드를 사용한다.

플레이어 정보를 데이터베이스에 저장하는 방법2

Json이나 XML로 플레이어 정보를 저장하는 방식은 이해하기 쉽지만, 플레이어 정보 중에서 원하는 조건 값을 자주 찾아야 할 때는 이 방식에 한계가 있을 수 있다.

이럴 때에는 플레이어의 데이터 정보를 여러 테이블에 나누어 저장해야 할 수도 있다.
트리의 각 노드는 속성을 여러 개 가지고 있다. 따라서 각 노드는 데이터베이스의 레코드로 저장해야 한다.

데이터베이스에 어떤 테이블을 넣어야 할지 계획을 잡을 때 자주 사용하는 방식은 외래 키(foreign key)를 사용하는 방법이다.외래 키란 테이블의 어떤 필드가 다른 테이블의 특정 필드 값을 가리키는 것을 의미한다.

외래 키를 갖고 자주 CRUD를 하므로, 외래 키에 대해서 인덱스를 설정하는 것이 좋다. 다만, 외래 키는 중복을 허용해야 한다.

질의 구문 실행(query)

플레이어ID와 비밀번호를 읽으려면 where구문에서 플레이어ID를 넣게 해야 한다.

select Id, password from UserAccount where ID='Hong Gil Dong'

플레이어가 가진 캐릭터들의 이름을 얻는 방법은

select ID from Character where OwnerUserAccountID='Hong Gil Dong'

플레이어가 가진 캐릭터 하나에 대한 모든 정보를 갖고 싶으면

select * from Character where ID='Little Elf'

여기서 *는 모든 필드를 가져온다.

트랜잭션

만약 다음과 같은 구문이 실행된다고 하자.

update UserAccount set Money=Money+100 where ID='YM'
update UserAccount set Money=Money-100 where ID='SH'

하지만 이러한 구문 중 하나만 실행된다면 큰일이 발생한다.

그래서 데이터베이스에는 방어 장치가 있다.
두 구문을 시작하기 전에 begin transaction을 먼저 실행한 후, 두 구문을 실행한 후 commit을 실행하면 된다.

begin transaction
update UserAccount set Money=Money+100 where ID='YM'
update UserAccount set Money=Money-100 where ID='SH'
commit

만약 구문들을 실행하다가 뭔가 잘못되었다면 commit 대신 rollback transaction을 실행한다.
그러면 begin transaction을 실행 한 수 변경된 데이터들이 원상 복구된다.

게임 서버에서는 트랜잭션을 많이 사용하는 경우는 주로 논리적 실패 때문에 롤백하는 경우가 대부분이다. 즉, 서버 메모리에서 사전 검증을 다 해버리는 특성 때문에 트랜잭션이 필요한 상황은 적다.

게임 서버에서 질의 구문 실행

데이터베이스를 게임 서버에서 액세스하려면, 프로그래밍 언어에서 사용 가능한 데이터베이스 연결 모듈 혹은 데이터베이스 클라이언트 모듈을 사용해야 합니다.

대부분의 모듈에서는 공통된 사용법이 있으며, 이를 위주로 설명한다.

먼저, 데이터베이스에 접속할 때는 데이터베이스 서버의 주소, 사용할 데이터베이스 인스턴스 이름 등의 정보를 이용한다.

DbConnection db = new DbConnection();
db.Open("Server=db01.mygame.com;userid=serverbot;password=good_day_one;database=GameDB;);

이후, query문을 실행할 객체, 명령 객체를 생성한다.
그 명령 객체에 query문을 문자열로 만들어 넣어서 실행한다.

DbCommand cmd = new DbCommand(db);
cmd.Execute("insert into table1 (a,b,c) values (123,456,789)");

혹은 채워질 값을 별도로 입력하여 실행하는 방법도 있다.

DbCommand cmd = new DbCommand(db);
cmd.Parameters[0] = 123;
cmd.Parameters[1] = 456;
cmd.Parameters[2] = 789;
cmd.Execute("insert into table1 (a,b,c) values (?,?,?)");

보안을 위한 주의 사항

데이터베이스는 해커가 노리는 주요 대상이다. 따라서 데이터베이스를 안전하게 보호하려는 노력이 필요하다.

  • 데이터베이스의 모든 것을 다룰 수 있는 관리자 계정은 오직 관리자만 직접 다룰 수 있게 한다.
  • 게임 서버가 사용하는 계정은 게임 서버가 다루는 테이블 이외에는 건드리지 못하게 한다.

또한 데이터베이스에는 게임 서버 이외의 다른 곳에서는 접속하지 못하게 네트워크를 격리해야 한다.

query injection

만약 다음과 같은 코드가 있다고 가정해 보자.

sprintf(queryString, "select * from t1 where a='%s'", userName);
db.execute(queryString);

하지만, 해커가 userName에 악의적으로 다음의 코드를 보낼 수 있다.

';delete from table1;select * from table1 where a='

그렇게 되면

select * from t1 where a='';delete from table1;select * from table1 where a=''

이러한 구문이 실행된다.

이는

DbCommand cmd = new DbCommand(db);
cmd.Parameters[0] = userName;
cmd.Execute("select * from table1 where a=?");

이러한 방법으로 해결할 수 있다.
이는 매개변수화된 query문이라 하며, query문에는 우리가 만든 문장만 들어가고, 다른 입력 값들은 별도의 매개변수로 격리해서 입력하는 것을 말한다.

profile
코린이

0개의 댓글