개요
데이터베이스는 기본 파일시스템위에서 b트리, 해쉬테이블등으로 효율적인 데이터 관리와 동시에 여러 질의, 원자성 등의 추가적인 기능들을 사용할 수 있게 만들어진 데이터 관리 시스템을 말한다. 이때 DBMS이라는 프로그램으로 데이터베이스 만들고 관리 할 수 있다. DBMS는 사용자가 DBMS의 인터페이스로 데이터베이스를 생성하면 파일 시스템위에 자신의 자료구조와 알고리즘을 이용하여 자신의 데이터베이스 파일을 구축하고 기능들을 제공해준다. 이러한 기능들을 파일 시스템 위에 직접 구현하여 사용할 수도 있겠지만 인력과 시간이 상당히 많이 들어가기에 대부분의 경우 상용 DBMS를 이용하여 데이터베이스를 사용한다.
보통 게임 뿐 아니라 대부분의 분야에서 서버에 데이터베이스를 사용할때는 DBMS로 운용되는 데이터베이스 서버를 따로두고 일반 서버가 이러한 데이터베이스 서버와 통신하며 데이터를 관리한다. DBMS는 일반 서버와 같은 하드웨어에 띄어서 사용할수도 있겠지만 하드웨어를 구분해두는 것이 일반적이다. 즉 일반서버에서도 네트워크 통신으로 데이터베이스에 접근하게 된다.
관계형 데이터베이스
데이터베이스는 여러 타입이 있는데 대표적으로 관계형 데이터베이스가 있다. 관계형 데이터베이스는 다음과 같은 계층구조를 가진다.
- 데이터베이스 서버 : 데이터베이스 전체
- 데이터베이스 인스턴스 : 한 테마의 테이블들을 모아둔 그룹
- 테이블 : 한 종류의 레코드들이 모인 그룹
- 레코드 : 한 데이터 레코드로써 여러 타입의 필드들을 가진다.
- 필드 : 레코드의 한 원소로 정수, 문자열, 참조키등의 값이다.
테이블은 행렬처럼 표현되며 한 행이 한 레코드를 나타내고 한 행의 한 열이 레코드의 한 필드이다.
필드 타입에는 정수, 문자열, 참조키등이 있는데 문자열의 경우 고정길이, 최대길이, 길이자유에 해당하는 타입들이 존재한다.
데이터베이스 질의구문
데이터베이스에는 SQL이라는 질의구문으로 여러 질의를 할 수 있다. 질의는 대표적으로 CRUD, 즉 생성, 읽기, 수정, 삭제를 수행할 수 있다.
다음은 몇가지 예이다.
insert into table1 (a,b,c) values (1,2,3)
table1에 a=1,b=2.c=3인 레코드를 하나 추가한다.
select a,b,c from table1 where a=1
table1에서 a의 값이 1인 레코드들의 a,b,c값들을 가지고온다.
update table1 set b=2 where a=1
table1에서 a의 값이 1인 레코드들의 b를 2로 수정한다.
delete from table1 where a=1
table1에서 a의 값이 1인 레코드들을 삭제한다.
프라이머리 키
레코드들은 자신이 속한 테이블 내에서 자신을 식별할수있는 다른 레코드들과 겹치지 않는 값을 가지는 필드가 있어야한다. 이를 프라이머리 키라고한다. 프라이머리 키는 중복될 수 없으며 이를 이용하여 한 레코드를 식별할 수 있다.
인덱스
데이터베이스에서는 한 테이블에 있는 특정 조건의 레코드들을 찾는 경우가 굉장히 많다. 위 질의문 예시의 where a=1이 a필드의 값이 1인 레코드들을 찾는 경우이다. 이때 해당 테이블의 레코드들이 찾고자하는 기준 필드에 대해 정렬되어 있지않다면 조건에 해당하는 레코드들은 테이블 전수조사로 찾을수 밖에 없다. 즉 O(n)의 성능이 든다. 이를 개선하기 위해 테이블의 특정 필드에 대해서 정렬된 포인트 자료구조를 만들어 둘 수 있는데 이를 인덱스라고한다. 이는 다음과 같이 구현된다.
- 테이블의 실제 레코드들은 정렬하지않고 저장한다.
- 각 레코드들에 대해 인덱싱 할 필드값과 해당 레코드의 포인터 값을 가진 미니 레코드를 만든다.
- 해당 미니 레코드들을 인덱싱 할 필드값을 기준으로 정렬하여 b트리나 배열등에 저장해둔다. 이것이 인덱스다.
- 이후 해당 테이블에서 해당 필드에 대한 값, 범위등의 찾기가 발생하면 해당 필드의 인덱스에서 해당 필드 값을 가진 미니 레코드를 찾고 (정렬된 자료구조이기에 O(logN)성능) 해당 미니 레코드의 포인터로 실제 레코드를 가져온다.
위 방식을 이용하면 인덱스를 만들어둔 필드에 대해 탐색을 하면 O(logN)의 성능으로 탐색이 가능하다. 따라서 자주 탐색 조건으로 사용될 필드에 대해서는 인덱스를 만들어두는 것이 좋다. 하지만 인덱스가 많을수록 테이블에 레코드를 추가,수정,삭제할때의 비용이 커지기에(인덱스들도 수정해주어야 하기때문) 필요한 인덱스만 만드는 것이 좋다. 보통 기본적으로 프라이머리 키에 대해서는 인덱스가 존재한다.
참조키
참조키는 필드의 한 타입인데 특정 테이블의 특정 레코드의 프라이머리키를 값으로 가지는 필드이다. 즉 해당 레코드를 가르키는 포인팅 필드가 된다. 참조키는 단순히 해당 레코드와 관련된 다른 레코드를 가르키는 용도로도 사용할 수 있지만 한 레코드가 다른 레코드들과 일대다 관계를 가질때도 유용하게 쓰인다.
한 예시로 게임의 한 유저가 여러 캐릭터를 가진다고 해보자. 그리고 유저 테이블의 한 레코드가 한 유저를 가르키고 캐릭터 테이블의 한 레코드가 캐릭터를 가르킨다고 해보자. 이때 한 유저 레코드는 캐릭터 레코드와 일대다 관계를 가진다. 즉 한 유저 레코드는 여러 캐릭터 레코드와 관계(소유 관계)를 가진다. 이는 캐릭터 레코드에 자신을 소유한 유저 레코드에 대한 참조키를 추가하면 구현할 수 있다. 한 유저가 가진 모든 캐릭터 레코드를 탐색하고 싶다면 캐릭터 테이블에서 해당 유저의 키를 참조키 필드의 값으로 가지고 있는 레코드들을 select로 모두 가져오면 된다. 이때 해당 참조키 필드에 대한 인덱스를 만들어 둔다면 O(LogN)의 성능으로 가져올 수 있게 된다.
저장 프로시저
서버에서 데이터베이스 서버에 질의문을 던지는데에는 네트워크 레인터시등의 오버헤드가 발생한다. 이를 줄이기 위해 데이터베이스에 정의해둘 수 있는 질의문으로 이루어진 스크립트 함수인 저장 프로시저라는 것을 사용할 수 있다. 저장 프로시저는 매개변수를 받고 일련의 질의문을 순서대로 처리하는 SQL 함수라고 볼 수 있다. 이렇게 일련의 처리를 프로시저로 정의해두고 서버에서 사용하면 성능면에서나 생산성면에서나 큰 이득이 된다. 프로시저는 다음 질의문으로 실행할 수 있다.
EXEC 프로시저이름 @매개변수이름='값' @...
트랜잭션
데이터베이스에서도 원자성이 보장된 일련의 처리가 있을 수 있다. 두 유저가 돈을 거래했다고 해보자. a유저가 b에게 100원을 줬다면 a의 돈을 100을 빼고 b의 돈을 100을 올려야한다. 근데 a의 돈을 100을 빼고 다음을 처리하기 전에 서버가 터져버린다면 문제가 된다. 이를 위해 일련의 처리가 완전히 수행되거 아예 수행되지 않도록하는 트랜잭션이라는 수행 범위를 만들 수 있다. 이는 다음과 같이 구현한다.
begin transaction
질의문1
질의문2
...
commit (또는 rollback transaction)
위에서 begin transaction과 맨아래 줄 사이의 질의문들은 원자성이 보장된다. 정확히는 해당 질의문들은 수행하고 나도 바로 결과가 적용되지않고 마지막에 commit이 호출된 순간 그 결과들이 모두 적용된다. 만약 commit이 아닌 rollback transaction이 호출되면 질의문들이 아예 호출이 되지않은것으로 처리된다.
이때 begin transaction을 한 후 특정 질의문을 실행하면 해당 질의문에 연관된 레코드는 잠금 상태가 되어 다른 프로세스에서 해당 레코드에 접근할 수 없게된다. 만약 접근을 시도하면 대기상태가되며 commit또는 rollback transaction중 하나가 호출될때까지 기다리다 호출된 후 그 결과에 따른 값이 적용된 레코드를 접근하게 된다. 이때 각 질의문이 실행되기 직전마다 해당 레코드에 대해 하나식 락이 걸리고 commit또는 rollback이 호출되면 한번에 락이 풀리는 구조이다. 따라서 접근하는 레코드들의 순서가 서로다른 트랙잭션이 동시에 수행되면 데드락이 걸릴 수 있다. 따라서 트랜잭션에서 접근하는 레코드들의 순서는 고정되어야한다.
트랜잭션에서 발생하는 레코드 잠금은 접근하는 레코드에만 국한되는 것이 아닌 연관된 다른 레코드나 어쩌면 테이블전체가 잠금되는 경우가 발생할 수 있다. 따라서 트랜잭션은 꼭 필요한 상황이 아니라면 최대한 적게 사용해야한다. 보통 서버 메모리에서 검증하거나 안전처리를 할 수 있는 부분은 최대한 하고 데이터베이스에서는 꼭 필요한 부분만 트랜잭션으로 처리하도록 구현한다. 또 트랜잭션의 잠금수준을 완화하는 등의 방법도 존재하나 이는 안정성을 줄일 위험이 있기에 신중하게 사용해야한다. 아니면 스스로 별개의 로그를 남겨 일관성을 지키도록 하는 로직을 구현해도 된다.
보안
데이터베이스는 해커의 주요 목적이기에 보안이 중요하다. 데이터베이스 보안에서 가장 기본은 계정 관리이다. 데이터베이스 접근 권한을 여러 계정으로 나누어 각 서버별로 꼭 필요한 권한을 가진 계정을 제공해주고 최고 등급의 관리자 계정은 관리자가 신중하게 관리해야한다.
이외에 데이터베이스와 관련된 query injection이라는 해킹방법도 존재한다. 만약 서버에서 사용자 이름을 입력받고 해당 이름 스트링을 쿼리 스트링과 합쳐 데이터베이스 서버에 질의문을 던진다고 하자. 그러면 해커가 사용자 이름에 주석을 넣거나 ;를 넣은뒤 자신이 실행하고 싶은 쿼리문을 그대로 넣으면 서버에서는 해당 스트링을 합쳐 여러줄의 쿼리를 데이터베이스에 질의하게되고 해커가 의도한 질의문이 실행되게 될 수 있다. 이를 막기 위해서는 사용자 입력에 제한을 주거나 프로그래밍 언어에서 사용하는 데이터베이스 모듈에서 제공해주는 매개변수화된 질의 구문 함수를 사용해야한다. 그러면 모듈안에서 매개변수로써의 문자열과 쿼리문으로써의 문자열을 별개로 처리해주거나 추가적인 보안 검증을 해줌으로써 인젝션을 막을 수 있다.
참고서적
게임 서버 프로그래밍 교과서
'네트워크 > 게임 서버 프로그래밍' 카테고리의 다른 글
[게임 서버][6장]프라우드넷 (0) | 2023.03.21 |
---|---|
[게임 서버][5장]게임 네트워킹 (1) | 2023.03.21 |
[게임 서버][4장]게임 서버와 클라이언트 (0) | 2023.03.21 |
[게임 서버][3장]소켓 프로그래밍 (0) | 2023.03.21 |
[게임 서버][2장]컴퓨터 네트워크 (0) | 2023.03.21 |