728x90
게임 서버 프로그래밍 교과서
7장. 데이터베이스 기초
7.1 플레이어의 정보 저장
- 온라인 게임에서는 플레이어 정보를 클라이언트가 아닌 서버에만 저장한다. 클라이언트는 로그인한 후 플레이어 정보를 서버에서 받아 사용한다.
- 플레이어 정보를 서버에 저장하는 방법은 크게 두 가지가 있다.
- 파일
- 데이터베이스
단순파일 DB 비고
소프트웨어 비용 | 없다. | 없거나 높다 | 오픈 소스 제품은 제한적으로 무료다 |
저장 및 로딩속도 | 빠르다 | 느리다 | DB도 결국 파일 시스템을 사용한다. |
데이터 관리, | |||
분석 속도 | 느리다 | 빠르다 | DB는 빠른 검색을 위한 인덱스 기능이 있다 |
데이터 백업 및 | |||
복원 기능 | 없다 | 있다. | - |
원자성 | 불가능하다 | 가능하다 | DB의 트랜잭션 기능이다 |
일관성 | |||
(잘못된 상태의 데이터를 원천 봉쇄한다.) | 없다 | 있다 | DB의 제약(Constraints) 기능이다. |
고립성 | |||
(경쟁 상태부터 자유롭게 해주는 기능이다.) | 없다 | 있다 | DB의 락 기능이다 |
지속성 | |||
(장애직전의 상태로 복구 가능한지에 관한 것이다.) | 없다 | 있다 | DB의 로그 버퍼 기능이다. |
7.2 데이터베이스의 데이터 구성
- 데이터에서 다루는 데이터는 표(table) 형태의 집합이다. 테이블의 집합은 데이터베이스 인스턴스(instance)라고 한다. 데이터베이스가 다루는 가장 큰 단위의 데이터 집합이다.
- 각 데이터베이스 인스턴스 안에는 테이블이 1개 이상 들어간다.
- 데이터베이스에서는 행 단위로 데이터를 넣거나 뺄 수 있다. 이 행을 레코드(record)라고 한다.
- 또 레코드 안에는 표의 열이 있는데, 이를 필드(field)라고 한다. 필드는 이름 말고도 타입이라는 것을 추가로 가지고 있다. 타입은 다음 중 하나이다.
- 정수
- 소수
- 문자열
- 날짜, 시간
- 그외(바이너리, xml, guid 등)
- 자주 쓰는 필드 타입
- int와 float는 각각 정수, 소수를 저장한다.
- 데이터베이스에 저장되는 필드의 글자 수에는 제한이 있다. 이 제한은 char, nchar, varchar, nvarchar 뒤에 붙는 숫자로, 글자 수가 넘어가면 글자가 잘리거나 오류가 발생한다. text는 글자 수에 제한이 없지만 액세스 속도가 더 느리다.
- 데이터베이스의 필드는 null을 저장할 수도 있다. 필드를 정의할 때 null을 허락함을 켜면 된다. null이란 값 자체가 없음을 의미한다. 0은 null이 아니고, “”(빈 문자열)도 null이 아니다. null이 존재하는 이유는 0과 값 없음을 구별해야 하는 상황을 위해서이다.
7.3 SQL 질의 구문
- 데이터베이스에 액세스할 때 필수적으로 알아야 할 문법은 질의(SQL query)이다. 데이터베이스에 액세스 하면서 주로 하는 것은 레코드 추가하기, 읽기, 변경, 삭제하기 이다. 각각을 CRUD(create, read, update, delete)라고 한다.
- 새 레코드를 삽입할 때는 insert로 시작하는 구문을 사용한다.
- 테이블의 모든 레코드를 얻으려면 select로 시작하는 구문을 사용한다.
- 테이블 안의 레코드 필드 값을 변경하는 것은 update로 시작하는 구문이다.
- 테이블 안의 레코드 필드 값을 삭제하는 것은 delete로 시작하는 구문이다.
7.4 인덱스와 키
- 데이터베이스에서 이러한 인덱스 기능을 쓸 수 있다. 인덱스는 필드 단위로 설정할 수 있다. 인덱스를 설정해 놓으면 특정 조건에서 어마어마하게 빠른 속도로 원하는 레코드를 찾을 수 있다.
- 인덱스의 select 구문, 즉 검색만을 위한 것이 아니다. 기존 레코드를 변경하거나 레코드를 삭제할 때도 인덱스는 빨리 찾는데 큰 도움이 된다.
- 인덱스는 레코드 둘 이상을 찾아낸 후 그것들을 정렬하는 용도로도 사용할 수 있다.
- 인덱스는 빠른 검색 용도뿐만 아니라, 중복된 값을 방지하는 용도로도 사용한다. 인덱스에는 유니크라는 속성을 추가로 지정할 수 있다. 이 유니크 속성을 추가한 인덱스, 즉 유니크 인덱스가 있을 때, 인덱스에 해당하는 필드 값이 같은 레코드가 2개 이상 들어갈 수 없게 막아주는 역할을 한다.
- 인덱스가 걸쳐 있는 레코드에 변화가 일어날 때, 인덱스도 같이 업데이트해야 한다. 인덱스가 없을 때 보다는 기록할 때 더 많은 시간이 걸린다. 따라서 필요하지 않은 인덱스는 가급적 걸지 않는것이 좋다.
- 데이터베이스에는 인덱스 말고도 프라이머리 키(primary key)가 있다.
- 프라이머리 키는 특수한 형식의 인덱스로 다음 성질이 있다.
- 한 테이블에 하나만 추가할 수 있다.
- 중복을 허락하지 않는다. 즉, 한 테이블에 값이 같은 코드가 2개 이상 들어갈 수 없다.
- 필드 값은 null이 허락되지 않는다.
7.5 플레이어 정보를 데이터베이스에 저장하는 방법
- 데이터를 저장하는 방법
- 플레이어 데이터 전체를 문서 형태로 만들어서 테이블에 넣는다.
- 플레이어 데이터를 구성하는 트리 노드 각각을 테이블에 넣는다.
- 플레이어 데이터를 문서 형태로 만들면 JSON(Javascript Object Notation) 문서 형태로 나온다. JSON 문서는 트리 구조의 데이터를 문자열 형태로 표현하며, 트리의 각 노드는 속성을 여러 개 저장할 수 있다.
- 여기서 말하는 속성이란 이름과 값의 짝을 의미한다.
- JSON 대신 XML 형식을 사용해도 된다. XML도 JSON과 마찬가지로 트리노드와 각 노드의 속성을 저장하는 것이 가능하다.
- JSON이나 XML로 플레이어 정보를 저장하는 방식은 이해하기 쉽다는 장점이 있다. 그러나 플레이어 정보 중에서 원하는 조건 값을 자주 찾아야 할 때는 이 방식에 한계가 있을 수 있다. 그럴 때 플레이어의 데이터 정보를 여러 테이블에 나누어 저장해야 할 수도 있다.
- 트리의 각 노드는 속성을 여러개 가지고 있다. 따라서 각 노드는 데이터베이스의 레코드로 저장해야 한다. 데이터베이스의 어떤 테이블을 넣어야 할지 계획을 잡을 때 자주 사용하는 방식은 외래 키(foreign key)를 사용하는 것이다. 외래 키란 테이블의 어떤 필드가 다른 테이블의 특정 필드 값을 가리키는 것을 의미한다.
- 외래 키를 갖고 CRUD를 자주 한다. 따라서 외래 키에 대해서 인덱스를 설정하는 것이 좋다. 다만 여기서 외래 키는 중복을 허락해야 하므로 논 유니크 인덱스로(non unique index) 설정해야 한다.
- 트리의 각 노드는 테이블의 레코드가 된다.
- 각 노드의 소유자, 즉 부모 노드는 외래 키가 된다.
- 데이터베이스 구조가 복잡할 때 개체-관계 다이어그램(E-R Diagram)이나 UML(Unified Modeling Language, 통합 모델링 언어)이 많이 사용된다.
7.6 질의 구문 실행
- 게임 서버가 데이터베이스에 질의 구문을 자주 던지는 것은 비효율적이다. 이유는 다음과 같다.
- 질의 구문 던지기 후 결과 받기 과정이 수행되는 동안 디바이스 타임(device time)이 발생한다.
- 게임 서버와 데이터베이스 간 네트워크 레이턴시를 모으면 꽤 긴 기간이다.
- 데이터베이스는 질의 구문을 받으면 이를 처리할 수 있는 준비 작업 연산을 한다. 데이터베이스 엔진은 받은 질의 구문을 분석하고 무슨 일을 해야 할지 계획한다. 처음 한 번만 이 일을 하고 이후부터는 생략할 수 있는 방법은 저장 프로시저(stored procedure)라는 기능을 사용한다.
- 데이터베이스 측에 미리 질의 구문 집합을 저장해 둘 수 있다. 이를 저장 프로시저라고 한다. 저장 프로시저는 데이터베이스 안에서 직접 실행되는 스크립트 프로그램이라고 할 수 있다.
- 데이터베이스에 저장한 저장 프로시저는 사용되기 전에 실행 계획 준비 작업이 완료된 상태로 보관된다.
트랜잭션
- 구문이 모두 실행되든지 하나도 실행되지 않든지 둘 중 하나의 결과만 보장되게 한다.
- 구문을 실행하기 전 begin transaction 구문을 먼저 실행하면 된다. 이를 트랜잭션의 시작이라고 한다. 그리고 두 구문을 실행한 후 커밋(commit)을 실행한다. 이렇게 하면 구문의 실행 결과는 데이터베이스에 영구적으로 남는다.
- 데이터베이스의 트랜잭션은 뮤텍스와 비슷하다. 그러기에 트랜잭션에서도 데드락을 주의해야 한다. 따라서 데이터베이스 트랜잭션을 할 때는 꼭 필요한 최소한의 구간에서만 하는 것이 좋다. 트랜잭션의 영향을 받는 레코드는 최소 개수로 하고, 트랜잭션을 시작부터 끝날 때 까지 데이터를 액세스 하는 횟수 역시 최소로 하는 것이다. 그리고 교착상태로 질의 구문 실행이 매우 오랫동안 블로킹되거나 실패를 출력할 때도 반드시 별도로 처리해 주어야 한다. 데드락으로 수십초 블로킹을 하면 타임 아웃 오류가 발생한다.
- 타임 아웃 오류가 발생했을 때 질의 구문을 다시 실행한다든지 하는 방법으로 오류를 핸들링 처리한다.
- 트랜잭션이 관여되는 레코드는 다른 질의 구문 프로세스에서 액세스 할 경우 블로킹을 일으켜 병렬 처리 효율성이 떨어진다.
- 불행하게도 트랜잭션 관련 레코드만 잠금되는 것이 아니라, 그 레코드 주변에 있는 다른 레코드 및 해당 레코드가 있는 테이블 전체가 잠금되기도 한다. 따라서 예상하는 것보다 더 넓은 범위가 잠금되기도 한다.
7.7 게임 서버에서 질의 구문 실행
- 데이터베이스를 게임 서버에서 액세스하려면, 사용하고 있는 프로그래밍 언어에서 사용 가능한 데이터베이스 연결 모듈 혹은 데이터베이스 클라이언트 모듈을 사용해야 한다.
- 게임 서버에서 데이터베이스에 액세스 하려면 먼저 데이터베이스에 접속해야 한다. 이를 위해 데이터베이스 연결 객체를 만들고, 이것으로 데이터베이스에 접속한다. TCP 커넥션과 유사하다.
- 데이터베이스에 접속할 대는 연결 문자열(connection string)이나 연결 정보 구조체를 넣어 주어야 한다. 연결 문자열 이나 연결 정보 구조체에는 데이터베이스 서버의 주소, 사용할 데이터베이스 인스턴스의 이름, 연결에 사용할 사용자 ID와 비밀번호가 들어간다. 사용자ID와 비밀번호는 게임 서버 자체가 사용하는 계정 이름인 경우가 일반적이다.
- 데이터베이스의 필드 값 타입은 숫자나 문자열이다. 대부분 프로그래밍 언어의 기본 데이터 타입과 비슷하지만 완전히 일치하지는 않는다. 데이터베이스는 여러 프로그래밍 언어에서 사용 가능해야 한다. 어떤것은 최대 길이나 정밀도(precision)가 있는 데이터 타입도 있으며, 물론 null 값을 수용 가능한 것도 있다. 따라서 필드에 값을 읽거나 쓸 때 혹은 명령 객체에 매개변수를 넣거나 꺼내 올 때는 다음 추가작업을 해야한다.
- 값을 읽을 때는 읽은 값을 가지고 있는 객체, 즉 값 객체를 얻는다.
- 값 객체가 null인지 검사한다. 그리고 값 객체의 실제 값을 원하는 값으로 변환한다.
- 필드에 값을 넣을 때는 다음과 같이 해야 한다.
- 원하는 값을 넣은 값 객체를 생성한다.
- 값 객체를 명령 객체나 레코드셋 객체에 넣는다.
7.8 보안을 위한 주의사항
- 데이터베이스의 모든 것을 다룰 수 있는 관리자 계정은 오직 관리자만 직접 다룰 수 있게 한다.
- 게임서버가 사용하는 계정은 게임서버가 다루는 테이블 이외에는 건드리지 못하게 한다.
- 데이터베이스에는 게임 서버 이외의 다른 곳에서는 접속하지 못하게 네트워크를 격리할 필요가 있다.
728x90
'Study > TIL(Today I Learned)' 카테고리의 다른 글
24.04.13 서버 개발기, C++ (0) | 2024.04.14 |
---|---|
24.04.12 서버 프로그래밍, 백준 (0) | 2024.04.14 |
24.04.10 서버 프로그래밍 (0) | 2024.04.10 |
24.04.09 서버 프로그래밍, 서버 공부 (0) | 2024.04.10 |
24.04.08 서버 프로그래밍, 백준 (0) | 2024.04.09 |