패치의 악몽을 피하기 위해

패치시에 문제가 하나도 발생하지 않는다면 얼마나 좋겠냐만은… 패치 과정에서 실수가 생기는 경우가 많은 것이 사실이다. 패치 준비과정에서의 피로와, 수작업으로 인해 사소한 실수가 큰 파장을 일으키는 것이 현실.

주로 점검시에 발생하는 문제는 패치 준비가 제대로 되지 않아 생기는 문제가 많다. 다음은 실 서비스 적용시 문제를 덜 일으키기 위한 방법이다.  

테스트는 최대한 실 서버와 같은 환경에서

  • 가급적이면 서버는 테스트 코드나 테스트 데이터를 적게 사용해야 한다.

QA와 같은 테스트는 가급적이면 실 서버와 같은 환경과 데이터로 하는 것이, 실 서버 패치 시에 생기는 문제를 줄일 수 있다. 점검시 실서버 QA의 경우 특히나 가급적이면 테스트 데이터를 사용하지 말자.

또한 환경 설정 파일 (cfg, ini를 비롯한 데이터 파일)의 검사도 반드시 하도록 하자. 만약, 테스트 데이터를 사용했다면, 테스트 데이터를 이용하고 있기에 실 서비스 오픈을 중지 시키자.

DB 스키마 적용 및, 데이터 복사는 가급적이면 자동화 하자.

  • DB스키마 적용이나, 데이터 복사를 수작업으로 했을때는 빼놓는 테이블이 생길 수 있고, 데이터 복사도 마찬가지다.

개발시에 실 서비스에서 변경된 스키마에 대한 쿼리문을 저장해두고, 패치시 사용한다면 문제를 줄일 수 있다. 하지만, 이런 작업도 번거로운 편이기 때문에, 가능하다면  실 서비스 스키마와 패치할 개발 DB 스키마와의 차이에 따른 쿼리를 생성해내는 생성기를 만들면 더욱 편하다.

대부분 DBMS는 export를 지원하므로, CFG같은 데이터 참조용 테이블의 경우는 export된 스키마와 데이터를 import해서 그대로 적용하면 쉽게 실 서비스에 적용 시킬 수 있다.

실 서비스에 적용할 바이너리/데이터에 대한 점검은 신중히 하자.

  • 실 서비스에 적용될 파일은 반드시 테스트 해보자. 특히 파일 복사시 잘못된 파일이 복사 될 수 있으니, 이를 검사해주는 유틸리티를 만들어 검사하는 것도 좋은 방법이다. 

물론, 가장 좋은 방법은 패치 절차 자체를 자동화하는 것이다.

또한 데이터 (설정 파일 또는 디비 데이터)도 마찬가지다. 실서비스용이 아닌 데이터라는 것을 체크해두고, 해당 데이터로 서비스 오픈 시도시 에러를 발생시키도록 하자.

Lan에서 환경에서 잘 적용된 것들이, Wan 환경에서도 잘 적용될꺼란 생각을 버려라.

  • Wan 환경에서는 추가적인 지연이 있기 때문에 성능이 안정적이지 않다.

그렇기에 Lan에서 잘 작동하는 듯이 보였던 잘못된 코드들이 Wan에서는 실패할 가능성이 있다.

실제 서버에 적용하기 전에 Wan환경에서 서버를 켠 후, 스트레스 클라이언트를 통해 테스트 한다면 잘못된 코드를 찾는데에 도움이 될 것이다.

서버 프로그래밍시 주의 사항

멀티 쓰레드 시 동기화는 주의 깊게 하라

  • 멀티 쓰레드에서 같은 데이터를 동시에 접근하지 못하도록 동기화는 필수다.
  • 현재 사용중인 데이터가 특정 시점까지 변해선 안 된다면, 데이터 사용이 끝나기 전까지 다른 쓰레드에서 접근이 불가능 하도록 해야 한다.
  • 물론, 동기화를 항시 고려하지 않게끔 이를 구조적으로 분리해두는 것이 더 좋다.  

    DB처리를 하러 간 사이에 벌어질 수 있는 일에 주의하라

  • DB처리를 위해 블럭이 되는 서버가 아니고선, DB처리를 하러 간 사이에는 간극이 존재한다.  이 때를 주의하자. 또, 서버가 여러 대인 경우, 다른 서버로 처리하러 떠난 상태까지 고려하도록 하자.

돈은 signed 형을 사용한다.

  • unsigned형을 쓰다 잘못해 언더플로우가 나는 것보다, 돈이 –가 되는 것이 낫다.  

    검사 시점을 주의하라

  • 즉시 이뤄지는 처리가 아닌, 다른 곳에서 인증이나 처리를 하고 온 후 처리 되는 경우라면, 민감해야 한다. 다른 곳으로 처리를 하러 간 도중에, 이 값을 어떤 식으로든 사용하게 된다면, 오차가 생기기 때문이다.
  • 돈의 경우에도 사용되는 서버가 여러곳이라고 한다면, DB와 각 서버에서 들고 있는 돈의 값에 차이가 있을 수 있기에, 처리 주체와 과정을 일체화시키고, lock을 통한 동기화가 필요하다.
  • 디비로 처리를 요청하고 결과를 기다리는 과정에서, 클라이언트에서 데이터를 원한다 해도 절대로 응해선 안된다. 결과를 기다리는 과정은 lock 상태여야 한다.
  • 클라이언트에 검사 결과를 전달했다 하더라도, 클라이언트에서 온 결과를 신용해선 안된다. 반드시 서버에서 트랜잭션 잡고 다시 한번 더 검사하라.

패킷 버퍼 크기에 주의 하자

  • 한 패킷이 큰 것이던, 패킷이 너무 많이 발생한 상황이던 간에 패킷 최대 크기를 넘어서는 상황이 나오지 않도록 조절해야 한다. (분산 패킷 등을 통해서 처리 하는 것이 좋다)
  • 클라이언트에서 올려보내는 패킷의 경우 너무 많이 올려보내면 연결을 닫는 것이 좋다. * 클라이언트에서 보내는 패킷의 경우 유효 시간으로 관리하는 것이 좋지만, 이 역시 비정상적으로 많은 량이 발생시에는 연결을 닫는 것이 좋다.
  • 서버와 서버간의 패킷의 경우 무조건 소화시켜야한다. 버그가 있어 많은 량이 되더라도 우선 처리하고 봐야한다. 

단일 패킷의 최대 크기를 설정하라.

  • 단일 패킷의 최대 크기를 지정하라. 패킷 하나의 크기가 설정 되어 있어야 버퍼의 효율적 사용에 유연하다. * 최대 크기 이상의 패킷은 비정상으로 판단할 수 있는 수단이 되기 때문에 정하는 것이 좋다.  

    저장 주체가 변경될 때에는 반드시 저장해주고 처리 하자.

  • 20분에 한번 저장하거나, 변동 상황이 누적되었을 때 저장하는 경우, 그 주체가 변했을 때를 주의 하자. 교환 같은 처리의 경우, 교환 될 아이템은 DB에 저장하고 교환 처리 하여 무결성을 유지하도록 하자.

# EventSelect와, AsyncSelect 도 recv시 실패를 하는 상황이 존재한다.

  • blocking socket이 아닌 non-blocking socket에서도 recv함수가 SOCKET_ERROR(-1)를 리턴하는 경우가 존재한다. 다른 스레드에서 close 소켓을 한 상황등에서 그렇다. FD_READ 신호가 왔지만, SOCKET_ERROR가 리턴되는 상황에도 대비해서 소켓 관련 코드를 작성하라.

# 끊긴 상황과, 끊는 동작을 명확히 구분하라.

  • 접속이 끊어진 상황과 자신이 접속을 끊는 동작과 명확하게 구분하라. 두가지 상황에서 이뤄져야할 처리는 많이 다르다. 의도치 않은 동작이 이루어졌는지 서버와, 클라이언트 사이의 규약을 정하고, 그 규약에서 어긋난 상황을 기록하고 수정하라.

트랜잭션이 필요할때를 판단하라.

  • 아이템 4개를 한꺼번에 지급해야 한다고 했을때, 코드에서 루프를 돌며 4개를 지급하는 것은 옳지 않다. 아이템 지급중에 오류가 발생하거나, 조건을 만족하지 않았을 때 롤백을 해야 하는데, 그 롤백이 코드에선 쉽지 않기 때문이다. (물론 가능하긴하다. 취소용 sp 또는 메소드를 제공함으로써. 그러나 db에서 처리하는 것보단 불편한 것이 사실이다.)

클라이언트 요청 처리에 대한 주의 사항

프로그램은 기본적으로 조건과 상황에 따른 판단과, 결정으로 이루어진다.

네트워크 프로그램은 클라이언트와 상호 작용하기 마련인데, 처음 서버 프로그램을 작성할 때 할 수 있는 실수는 클라이언트를 너무 쉽게 믿어버린 다는 것이다.

  • 클라이언트가 이 약속을 안지키면 어떻게 할 것인가?
  • 클라이언트에 버그가 있다면 어떻게 할 것인가?
  • 정상적인 클라이언트를 이용하지 않는다면 어떻게 할 것인가??

서버는 반드시 방어적으로 동작해야 한다. 클라이언트가 잘못된 요청을 하더라도, 서버는 그 요청을 무시하거나, 잘못된 요청이라는 것을 알려주는 것에 그쳐야지 서버에 예외 또는 버그가 발생해선 절대 안된다.

다음은 직접 경험했던 상황들의 예이다. 직접 작성한 코드와, 유지 보수하던 코드에서 발생한 문제가 뒤섞여있지만, 클라이언트가 어떤 동작을 하던 서버는 잘못된 동작을 해선 안된다는 예가 될 수 있다고 본다.

순서가 있는 패킷이라도, 가정만 하지 말고 검사하자.

  • 내가 만든 퀘스트 시스템의 경우 퀘스트를 받을 때, 퀘스트 정보 받아오기 패킷에서 퀘스트를 받을 수 있는지를 검사하고, 그 결과가 OK일떄만 새 퀘스트 받기가 올라오도록 되어있었다.
  • 해결책은 새 퀘스트 받기 패킷이 올라왔을 때도, 퀘스트 받을 수 있는지 여부는 검사하는 방법으로 해결 할 수 있었다. 이 경우, 넷 리미터라는 프로그램을 통해서 패킷 보내기 사용량 제한을 걸어둔 후, 새 퀘스트 받기 버튼을 여러회 누른다. 그리고 패킷 보내기 제한을 풀어버리면, 새 퀘스트 받기 패킷만 여러번 올라와 퀘스트가 여러개 받아지게 된다.

아이템 장착 해제 시에는, 해당 아이템이 장착 중인지를 반드시 검사한다!

  • 클라이언트가 중복 착용/해제 시도를 안 할거라는 확신을 하지 말아라. 확신은 서버에서 직접 검사를 한 후에 해라.

 

가정 하지 마라. 코드에서 직접 제약을 걸어라.

  • 어떤 처리에 대한 클라이언트와의 규약 (예를 들면, 비번 방 지정은 방장만 할 수 있다던가, 게임 방에서만 가능하다던가 하는 가정)은 반드시 지켜질 수 있도록 코드에서 그 동작이 불가능 하도록 만들어라.

범위 검사도 확실히 해라.

  • 클라이언트에서 올라온 모든 파라미터에 대해서는 유효성 검사를 하는 것이 좋다.

(서평) 테스트 주도 개발

사실 저는 테스트를 별로 좋아하지 않습니다.

테스터 분들이 보시면 기분나빠하실지도 모르지만, 개발보다 지루한 작업이기 때문이죠.

하지만 테스트는 언젠가 해야하고, 프로그램의 품질에 지대한 영향을 끼칩니다.

프로그래머는 모든 상황을 생각할 수 없습니다. 현재의 코드가 미칠 여파를 모두 생각해내는건 사실상 불가능하죠.

프로그래머의 논리적 빈틈은 테스트를 통해서 해결해야합니다.

그런 코드의 빈틈을 테스터에게만 맡길 수 있을까요?

그 프로그램을 작성한 사람보다 빈틈을 더 잘 찾을 수 있을까요?

프로그래머가 하는 테스트가 꼼꼼하다면 프로그램이 공개 되고 나서 발생할 문제를 상당수 방지 하거나 해결 할 수 있습니다.

테스트 주도 개발은 기본적으로 리팩토링을 기반으로 이루어집니다.

테스트를 통해 개발을 하며, 리팩토링을 통해 같은 기능을 하지만 좀 더 좋은 코드를 자연스레 갖춰나갈 수 있게 됩니다.

개발자 선에서 이루어지는 테스트 도중 더 좋은 아이디어를 떠올릴 수도 있고, 빈틈 하나 하나를 채워가며 높이는 완성도는 프로그램의 가치를 높여줄 것이라고 확신합니다.

함수의 리턴 값의 주소를 사용하려 할 때 생기는 문제

char* GetStr()
{
    static char szStr[] = "Hello";
    return szStr;
}

void PrintStr(char **str)
{
    printf("%s",*str);
}


int main(int argc, char **arv)
{
    PrintStr(&GetStr());
    return 0;
}

  위 코드는 아래와 같은 컴파일 에러를 발생시킨다. error C2102: ‘&’ requires l-value

컴파일러의 에러는, l-value. 즉, 어딘가에 저장된 값에만 주소 연산자를 사용할 수 있다는 말이다.

내가 이 코드를 작성한 의도는 char형 포인터의 포인터 (이중 포인터)를 매개 변수로 받는 PrintStr함수의 매개변수로, char형 포인터를 반환하는 GetStr함수의 반환 값의 주소를 넘기면 정상적으로 동작할 거라는 생각에서였다.

그래서 내가 생각한 해답은, GetStr함수가 반환할 char형 포인터 주소인

GetStr() 0x00427b60 "Hello"           char *

가 아니라, GetStr의 반환 값이 임시 저장되어 있는, 레지스터의 주소를 대입하려 하는 건 아닐까 생각했다.

  main함수를 이렇게 바꾸면 정상적으로 동작한다.

int main(int argc, char **arv)
{
    char *str = GetStr();
    PrintStr(&str);
    return 0;
}

이 코드를 디스 어셈블 해본 결과다.

char *str = GetStr();

00412BDE  call        GetStr (411500h) 
00412BE3  mov         dword ptr [str],eax

역시 추측이 맞았다. GetStr함수를 부른 결과는 eax 레지스터에 저장되어 있었고, 그 값을 어딘가에 복사하기 전까진, 레지스터에 있는 값이므로, 레지스터에 주소 연산자를 사용할 수 없기 때문에 컴파일 에러를 낸 것이었다.