프로그래밍 일반 2019. 7. 9. 12:41

클리커 게임 표현식 구현하기

3년간 서버만 만들다가 오랜만에 클라이언트 작업을 시작하면서 재미있었던 내용이 있는데 공유해보도록 하겠습니다.

 

클리커 게임을 만들면서 재화 표현식은 어떻게 만드는게 좋을까? 엄청 큰 자료형에 정수를 무조건 담을까?

 

자릿수 표현은 어떻게 처리하지??

 

어비스리움의 표현식을 가져오면 아래와 같은데

1000 = 1A

1000A = 1B

 

double형 지수부를 이용하려고 생각했기 때문에 액셀을 이용해 패턴을 찾으려고 아래와 지수 표현식을 나눠 

패턴을 분석

  승수 (자릿수) 나머지 표현식
1.00E+ 0 0 0 1
1.00E+ 1 1 0 10
1.00E+ 2 2 0 100
E+ 3 0 1 1000 = 1A
E+ 4 1 1 10A
E+ 5 2 1 100A
E+ 6 0 2 1000A = 1B
E+ 7 1 2 10B
E+ 8 2 2 100B

패턴이 너무 간단 명료해서 코딩이 빠르게 끝남..

 

재화는 double형에 담으면 되고 출력을 위해 스트링형식으로 바꾸면 되기 때문에 아래와 같이 작업

 

 

/// <summary>
/// 확장 메서드 모음
/// </summary>
public static partial class ExtensionMethods
{
        const string Zero = "0";

        /// <summary>
        /// 단위 표현 스타일
        /// </summary>
        public enum CurrencyType
        {
            Default,
            SI,
        }

        /// <summary>
        /// 표현 가능한 화폐 단위, double형 e+308까지 자릿수 표현 지원
        /// </summary>
        static readonly string[] CurrencyUnits = new string[]
        {
            "",
            "A",
            "B",
            "C",
            "D",
            "E",
            "F",
            "G",
            "H",
            "I",
            "J",
            "K",
            "L",
            "M",
            "N",
            "O",
            "P",
            "Q",
            "R",
            "S",
            "T",
            "U",
            "V",
            "W",
            "X",
            "Y",
            "Z",
            "AA",
            "AB",
            "AC",
            "AD",
            "AE",
            "AF",
            "AG",
            "AH",
            "AI",
            "AJ",
            "AK",
            "AL",
            "AM",
            "AN",
            "AO",
            "AP",
            "AQ",
            "AR",
            "AS",
            "AT",
            "AU",
            "AV",
            "AW",
            "AX",
            "AY",
            "AZ",
            "BA",
            "BB",
            "BC",
            "BD",
            "BE",
            "BF",
            "BG",
            "BH",
            "BI",
            "BJ",
            "BK",
            "BL",
            "BM",
            "BN",
            "BO",
            "BP",
            "BQ",
            "BR",
            "BS",
            "BT",
            "BU",
            "BV",
            "BW",
            "BX",
            "BY",
            "BZ",
            "CA",
            "CB",
            "CC",
            "CD",
            "CE",
            "CF",
            "CG",
            "CH",
            "CI",
            "CJ",
            "CK",
            "CL",
            "CM",
            "CN",
            "CO",
            "CP",
            "CQ",
            "CR",
            "CS",
            "CT",
            "CU",
            "CV",
            "CW",
            "CX",
        };
        
        /// <summary>
        /// SI 표기법 생략...
        /// </summary>
        static readonly string[] SI = new string[]
        {
        };

        

        /// <summary>
        /// double형 데이터를 클리커 화폐 단위로 표현한다.
        /// 확장 메서드로 제공한다.
        /// </summary>
        /// <param name="number">double형 숫자</param>
        /// <param name="currencyType">클리커 단위타입</param>
        /// <returns>클리커 표현식</returns>
        public static string ToCurrencyString(this double number, CurrencyType currencyType = CurrencyType.Default)
        {
            // 정수부가 0인경우 0으로 퉁친다.
            if (-1d < number && number < 1d)
            {
                return Zero;
            }

            if (true == double.IsInfinity(number))
            {
                return "Infinity";
            }

            // 부호 출력 문자열
            string significant = (number < 0) ? "-" : string.Empty;

            // 보여줄 숫자
            string showNumber = string.Empty;

            // 단위 문자열
            string unitString = string.Empty;

            // 패턴을 단순화 시키기 위해 무조건 지수표현식으로 변경한 후 처리한다.
            string[] partsSplit = number.ToString("E").Split('+');

            // 예외 상황이다....
            if (partsSplit.Length < 2)
            {
                UnityEngine.Debug.LogWarning(string.Format("Failed - ToCurrencyString({0})", number));
                return Zero;
            }

            // 지수 (자릿수 표현)
            if (false == int.TryParse(partsSplit[1], out int exponent))
            {
                UnityEngine.Debug.LogWarning(string.Format("Failed - ToCurrencyString({0}) : partsSplit[1] = {1}", number, partsSplit[1]));
                return Zero;
            }

            // 몫은 문자열 인덱스
            int quotient = exponent / 3;

            // 나머지는 정수부 자릿수 계산에 사용 (10의 거듭제곱을 사용)
            int remainder = exponent % 3;

            // 1A 미만은 그냥 표현한다.
            if (exponent < 3)
            {
                showNumber = Math.Truncate(number).ToString();
            }
            else
            {
                // 10의 거듭제곱을 구해서 자릿수 표현값을 만들어준다.
                var temp = double.Parse(partsSplit[0].Replace("E", "")) * Math.Pow(10, remainder);

                // 소수 둘째자리까지만 출력한다.
                showNumber = temp.ToString("F").Replace(".00", "");
            }
			
             
            if (currencyType == CurrencyType.Default)
            {
                unitString = CurrencyUnits[quotient];
            }
            else
            {
                unitString = SI[quotient];
            }

            return string.Format("{0}{1}{2}", significant, showNumber, unitString);
        }


    /// <summary>
    /// 문자열로 입력한 클리커 재화를 double형으로 표현한다.
    /// </summary>
    /// <param name="currencyString">재화</param>
    /// <param name="stringType">문자열의 재화 표현 타입</param>
    /// <returns>double형 재화</returns>
    public static double ToCurrencyDouble(this string currencyString, CurrencyType stringType = CurrencyType.Default)
    {
        double result = 0;

        bool isNumber = double.TryParse(currencyString, out result);

        if (true == isNumber)
        {
            return result;
        }
        else
        {
            int length = currencyString.Length;
            int lastNumberIndex = -1;

            for (int i = length - 1; 0 <= i; --i)
            {
                if (true == char.IsNumber(currencyString, i))
                {
                    lastNumberIndex = i;
                    break;
                }
            }

            if (lastNumberIndex < 0)
            {
                throw new Exception("Failed currency string");
            }

            string number = currencyString.Substring(0, lastNumberIndex + 1);
            string unit = currencyString.Substring(lastNumberIndex + 1);

            int index = (CurrencyType.Default == stringType) ? Array.FindIndex(CurrencyUnits, p => p == unit) : Array.FindIndex(SI, p => p == unit);
            if (-1 == index)
            {
                throw new Exception("Failed currency string");
            }

            string exponentNumber = string.Format("{0}E+{1}", number, index * 3);

            return double.Parse(exponentNumber);
        }
    }
}

참 쉽다..끝

프로그래밍 일반 2017. 11. 15. 15:41

무 정지 서버 업데이트

2017년에 작성을 시작하여 바쁘다는 핑계로 마무리가 되지 않은 글을 마무리하지 못했는데 그래도 내용이 재미있기 때문에 3년이 흐른 지금 공개로 설정합니다.

참고로 요즘은 많은 서비스가 서버를 멈추지 않고 버전 업데이트를 진행합니다.



서비스를 정지하지 않고 서버 점검을 할 수 있어야하는 이유가 무엇일까?


고민할 필요 없다. 새벽에 출근하지 않기 위함이며 빨리 집에가기 위함이다. 개발자들에게 그것보다 중요한 이유가 필요한가?


세션형 온라인 게임에서 무 정지 서버 업데이트를 처리하기 위한 대략적인 개요는 대략 이러하다.


  • 서비스 이용 시 서버 어플리케이션이 변경되는 것을 유저가 느낄 수 없도록 해야 한다.
  • 패치 시 프로그래머 이외에 실제 작업자가 직접 패치 및 배포 작업을 할 수 있어야 한다. 즉 작업이 쉬워야 한다.
  • 작업이 쉽도록 하기 위해서는 빌드 및 배포 아키텍처가 제어 인터페이스 뒤에서 드러나지 않아야 한다.
  • 히스토리를 한눈에 알 수 있어야 한다.

위의 조건들을 만족하기 위해 빌드 자동화 및 배포 기능을 만족하는 툴 프로그램 제작 후 젠킨스를 이용해 해당 툴을 실행 시킨다.


젠킨스의 장점은 아래와 같은 빌드 설정들을 파라미터로 툴에 전달이 가능하며 빌드 전후 이벤트를 셋팅할 수 있고 사용이 쉽다.


빌드 파라미터 구성 예시빌드 파라미터 구성 예시




시스템 아키텍처시스템 아키텍처


위의 그림은 일반적인 세션형 서버 아키텍쳐이다. 최소한 본인이 경험한 다섯번의 프로젝트는 위와 크게 다르지 않으며 MMORPG 같은 경우만 게임 서버간의 관계 구조가 조금 다를 뿐... 단순화 시키면 위와 비슷하다. AWS 사용은 무 점검을 위한 필요 조건은 아니다. 


그러면 서버 패치 시 서비스를 중지 시키는 이유는 무엇일까?


1. 소스 코드에 버그가 있어 어쩔 수 없이 새로운 빌드의 서버 프로그램을 시작해야 하는 경우

2. 서버 프로그램이 데이터 로드나 환경 셋팅을 프로그램이 시작되면서 초기화될 때만 처리할 수 있는 구조인 경우

3. DB 테이블 변경을 적용하기 위해 서버 프로그램이 갱신되어야 하는 경우

4. 유저 접근을 차단하고 DB 작업이 필요한 경우



무 정지를 위해 기본적으로 크게 다섯 가지 정도 고려할 사항이 있다.


1. 최신 버전의 서버 어플리케이션에서 다양한 하위 버전의 클라이언트 수용이 가능해야한다.

 - 구버전 서버가 구동 중인 상태에서 신버전 서버 프로세스가 시작되면 높은 버전의 서버로 유저 진입이 가능해야 한다.

 - 유저가 모두 빠진 구버전 서버 프로세스는 자동으로 종료될 필요가 있다.

 - 버전별 매칭이 가능해야 한다.


2. 데이터 및 DB 패치

 - 데이터 변경 및 DB 변경에 대한 규칙 설정은 초반에 결정해야한다. 자동화하기 위해서는 규칙이 있어야 한다.

 - 데이터 로드용 구조체 생성은 자동화될 필요가 있다. 관리 대상이 늘어나면 사람은 실수 한다.

 - 서버 프로그램은 재시작하지 않고 변경된 데이터를 로드할 수 있어야 한다.

 - DB 스크립트를 이용한 패치도 자동화되어 있어야 한다.

 - DB 패치 후 구버전 서버도 문제 없이 구동이 가능해야 한다.


3. 서버 빌드 및 배포 방법

 - 반드시 자동화를 초반에 고려해야 한다. 나중으로 미루고 구현하는 경우는 거의 본적이 없다.

 - 히스토리 추적이 쉽고 간단해야 한다.

 - 파트 구분 없이 작업자가 직접할 수 있어야 한다. 패치는 프로그래머만 해야 하는 그런 거 없다. 모두가 하면 다 같이 편해진다.

 - 위의 항목이 가능하기 위해서는 프로세스에 점증 시스템이 반드시 필요하다.

   수많은 데이터를 관리하는 담당자에게 반복되는 패치 오류를 지적하는 것도 무책임하다. 최소한 데이터 타입, 논리적 모순, 링크 검증 등 

   빌드 단계에서 자동으로 확인해 줄 시스템은 프로그래머가 구축해야 한다. 필터링 되지 않은 문제들을 검증 시스템에 추가해나가면 데이터 

   패치 오류 문제는 정말로 많이 감소한다.


4. 서버 기능과 코드 관리

 - 큰 작업 단위로 조건부 컴파일을 이용하고 안정화되면 반드시 정리한다.

 - 기능 변경이나 서버 설정이 바뀐다고 빌드를 다시 하는 경우가 없어야 한다. 데이터 및 DB 변경으로 컨트롤할 수 있게 설계하자.

   하드코딩 좀 하지마.


5. 서버 스케일 정보

 - 서버 운용 정보 주기적 갱신 후 해당 정보를 이용하여 클라 접속을 제어한다.

 - AWS를 이용한다면 아키텍처 구상 단계에서 GameLift를 고려해본다.



그러면 최신 버전의 서버 어플리케이션에서 다양한 하위 버전을 수용하기 위해 고려할 사항으로는 무엇이 있을까? 일반적으로 데이터 드리븐방식으로 프로그램을 구현하기 때문에 데이터값이 변경되는 것은 문제가 없지만, 데이터 타입 변경이나 컬럼이 추가되는 등의 경우는 호환성 유지에 걸림돌이 될 수 있다.


[구버전 소스 코드]


    enum Color

    {

        Red,

        Yellow,

        Max

    }


    class TableHeaderFlower

    {

        string name;

        Color color;

        int count;

    }



[구버전 데이터]


 

string 

Color 

int 

 1

 Rose

Red 

10 

 2

 ...

... 

... 



위와 같이 정의된 꽃에 대한 데이터가 다음과 같이 변한다면


[신버전 소스 코드]


    enum Color

    {

        Red,

        Yellow,

        Green,

        Max

    }


    class TableHeaderFlower

    {

        string name;

        Color color;

        int count;

        string languageOfFlowers;

    }


[신버전 데이터]


 

 string

Color 

int 

string 

 1

 Rose

Red 

10 

장미 꽃 말 

 2

 녹색 꽃

Green 

녹색 꽃 말 


열거형 멤버와 데이터 컬럼이 추가되었다.

열거형 멤버만 추가될 수도 있다 여러가지 변경이 복합적으로 발생할 수도 있다.


일반적인 상황이라면 구버전 빌드는 구조가 변경된 데이터를 읽어올 때 Green이라는 열거형 멤버와 꽃말이라는 추가된 컬럼 때문에 정상적인 실행이 되지 않을 가능성이 높다.


...

내용은 앞으로 계속 추가하도록 하겠습니다...

프로그래밍 일반 2017. 11. 7. 17:07

GeoIP 자동 업데이트 빌드 프로세스에 추가하기

GeoIP가 무엇인지 알고 있다는 가정하에 매월 첫 주 목요일에 업데이트되는 해당 테이블을 자동으로 업데이트하는 방법을 알아보자.


운용 중인 프로젝트의 CI를 젠킨스로 구축했다면 서버 빌드 시 아래와 같은 Powershell 스크립트를 추가하면 된다.


참고로 프로젝트에서 GeoIP.dat를 로드해서 룩업 테이블을 만들어 사용하는 경우 .dat 파일을 자동 업데이트하는 방법이다.



#서버를 빌드할 때 자동으로 GeoIP를 업데이트한다.

if ($ENV:JobCommand -eq "ServerBuild")

#매월 첫 주 목요일에 업데이트되는 파일을 다운 받는다.

$webclient = New-Object System.Net.WebClient

$sourceUrl = "http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz"

$destinationPath = "$ENV:WORKSPACE\Server\Lib\GeoIp2\GeoIP.dat.gz"

$webclient.DownloadFile($sourceUrl, $destinationPath)


#반디집을 이용하여 압축을 푼다.

[string[]]$array = "bx", "-aoa", "$ENV:WORKSPACE\Server\Lib\GeoIp2\GeoIP.dat.gz"

start-process C:\Bandizip\Bandizip.exe $array


#압축이 풀리기 전에 원본 파일이 삭제되는 것을 방지한다.

wait-event -sourceIdentifier "Bandizip" -timeout 10


#반디집 핸들러가 원본 파일을 물고 있는 것을 풀어준다.

$bz = get-process Bandizip

stop-process -inputobject @bz -force


#원본 파일을 삭제한다.

Remove-Item $destinationPath -Recurse


#압축이 풀린 파일을 svn에 적용한다.

svn commit $ENV:WORKSPACE\Server\Lib\GeoIp2\GeoIP.dat --non-interactive --trust-server-cert --username **** --password **** -m "Build server solution from CI server"

}


설명을 첨부하자면 JobCommand 는 빌드 파라미터이고 서버 빌드를 실행했을 때 웹에서 GeoIP.dat.gz 파일을 다운로드 받아 압축을 풀고 svn으로 서밋하는 간단한 내용이다.


gz 파일은 CI 서버에 반디집을 설치하여 반디집 커맨드 명령어로 압축을 푼다.

프로그래밍 일반 2017. 8. 24. 00:30

젠킨스 삽질 정리

'svn'은(는) 내부 또는 외부 명령, 실행할 수 있는 프로그램, 또는 배치 파일이 아닙니다.

 

라는 오류 발생 시 command line client tools 설치하면 된다. 설치 시 버전에 유의하자.

 

 

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Microsoft.Common.targets(983,5): warning MSB3644: ".NETFramework,Version=v4.5" 프레임워크에 대한 참조 어셈블리를 찾을 수 없습니다. 이 문제를 해결하려면 이 프레임워크 버전에 대한 SDK 또는 타기팅 팩을 설치하거나 응용 프로그램의 대상을 SDK 또는 타기팅 팩을 설치한 프레임워크 버전으로 변경하십시오. 어셈블리는 GAC(전역 어셈블리 캐시)에서 확인되며 참조 어셈블리 대신 사용됩니다. 따라서 어셈블리의 대상이 사용자가 의도하는 프레임워크로 올바르게 지정되지 않을 수 있습니다.

 

해당 메시지가 발생하면 CI 서버의 C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework 를 확인하자.

 

경로에 필요한 버전의 .netframework가 없을 것이다.

 

필요 버전의 SDK를 설치하자. 개발 환경과 일치 시키면된다.

 

 

svn: E170013: Commit failed (details follow):

svn: E170013: Unable to connect to a repository at URL 'https://xxx.xx.x.xxx/svn/project/trunk/Tool/BatchBuilder_x64'

svn: E230001: Server SSL certificate verification failed: issuer is not trusted

 

commit 명령어 사용 시 위와 같은 오류가 발생한다면 아래와 같이 처리한다.

 

svn commit [PATH..] --non-interactive --trust-server-cert-failures="unknown-ca,cn-mismatch,expired,not-yet-valid,other" --username ARG --password ARG -m "CI server build commit" 

 

ARG는 svn 계정 정보를 입력하면 된다. 세부 항목은 svn help 를 이용하자.

프로그래밍 일반 2016. 8. 22. 14:52

FileSystemWatcher Changed 이벤트 중복 호출 버그

FileSystemWatcher 클래스는 특정 폴더나 파일 변경 모니터링이 필요할 때 사용하는데 Changed 이벤트에 버그가 있다. 


버그 해결이 까다로운데 이유는 해당 이벤트는 파일 변경이 종료되었음을 알리는게 아니기 때문에 종료 시점을 프로그래머가 꼼수를 써서 알아내야한다.


서버 환경 설정이 셋팅된 파일을 모니터링하다가 변경되면 실시간으로 반영하도록 작업되어 있는데 모니터링 파일이 커지면 Changed 이벤트가 


여러번 발생한다.


4,540 바이트 파일을 모니터링할 때 파일 복사가 종료될 때까지 LastWrite 필터는 5회, LastAccess 필터는 3회의 이벤트가 발생한다.


이벤트 중복 호출이 문제가 되는 이유는 일반적으로 변경 이벤트 발생 시 관련 핸들러에 해당 파일을 읽어 들이는 작업을 작성할텐데 파일 쓰기 


작업이 완료되지 않은 상황에 이벤트가 발생하여 파일에 동시 접근을 시도하기 때문에 변경 내용을 읽어들일 수 없게된다.


아래의 코드는 Changed 이벤트 버그를 해결할 수 있는 C#으로 작성한 간단한 예이다.  붉은색으로 작성한 부분이 중요 포인트


    /// <summary>

    /// 지정된 경로의 파일이나 폴더가 변경되면 이벤트를 발생 시킨다.

    /// </summary>

    public static class FileWatcher

    {

        /// <summary>

        /// 파일 시스템 모니터링 객체

        /// </summary>

        static FileSystemWatcher watcher = new FileSystemWatcher();



        /// <summary>

        /// 리소스 해제

        /// </summary>

        public static void Dispose()

        {

            watcher.Dispose();

        }


        /// <summary>

        /// 파일 시스템 이벤트 핸들러를 생성한다.

        /// </summary>

        /// <param name="dataDirectory">공통 csv 관리 폴더 경로</param>

        public static void CreateHandler(string dataDirectory)

        {

            if (false == watcher.EnableRaisingEvents)

            {

                watcher.Path = string.Format("{0}\\SystemConfig", dataDirectory);

                watcher.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.Size;


    // 하나의 파일만 모니터링하고 있다.

                watcher.Filter = "ContentsConfig.ini";


                watcher.Changed += new FileSystemEventHandler(OnChanged);

                

                watcher.EnableRaisingEvents = true;

                watcher.IncludeSubdirectories = false;

            }

        }


        /// <summary>

        /// 지정된 경로에서 파일이나 디렉토리가 변경될 경우 발생하는 이벤트

        /// </summary>

        /// <param name="source"></param>

        /// <param name="eventArgs"></param>

        private static void OnChanged(object source, FileSystemEventArgs eventArgs)

        {

            try

            {

                // 모니터링을 비활성화, 해당 방법은 하나의 파일만 모니터링할 때 사용 가능하다.

                watcher.EnableRaisingEvents = false;


                if (WatcherChangeTypes.Changed != eventArgs.ChangeType)

                {

                    return;

                }


                // 파일 변경 모니터링 핸들러 진입


                // 서버 컨피그 파일만 처리한다.

                if (true == string.Equals(

                    "모니터링 파일 명",

                    eventArgs.Name,

                    StringComparison.OrdinalIgnoreCase))

                {

                    // 모니터링 파일이 큰 경우 FileSystemWatcher의 변경 이벤트가 중복 호출되는 버그와

                    // 변경 대응 이벤트가 무거운 경우 파일 액세스 동시 접근 오류가 발생한다.

                    // 동시 접근 오류를 해결하기 위해 쓰기가 완료되었는지 확인할 필요가 있다.

                    string fullpath = string.Format("{0}\\{1}", watcher.Path, SystemConfigDefine.ContentsConfigFileName);


                    // 파일 쓰기 중인지 확인한다.

                    while (true)

                    {

                        try

                        {

                            using (Stream stream = File.Open(fullpath, FileMode.Open, FileAccess.Read, FileShare.None))

                            {

                                if (null != stream)

                                {

                                    break;

                                }

                            }

                        }

                        catch (Exception exception)

                        {

                            LoggerWrapper.Information("[FileWatcher] Changing {0}, {1}", eventArgs.Name, exception.Message);

                        }


                        // 메인 스레드와 다른 스레드이기 때문에 슬립을 사용해도 다른 시스템에는 영향이 없다.

                        System.Threading.Thread.Sleep(1);

                    }


                    ... 이곳에 파일을 읽어들이는 코드를 작성한다.


         // 파일 변경 완료

                }


            }

            finally

            {

     // 모니터링 활성화

                watcher.EnableRaisingEvents = true;

            }


        }


프로그래밍 일반 2015. 5. 20. 18:02

컨테이너 순환문에서는 전위 증가 연산자를 사용하자

링크 : 어셈블리 기본


int i = 0;

int iPrefix = ++i; // 전위연산

int iPostfix = i++; // 후위연산


해당 코드의 결과는 프로그래머라면 누구나 알 수 있을 것이다.


그런데 두 연산자는 속도의 차이가 있을까?


필자는 전위 연산자가 더 빠르다고 배웠기 때문에 습관적으로 전위 연산자를 사용하고 있었는데 정말로 그런지 어셈코드를 확인해 보기로 했다.





코드를 보면 알 수 있지만 대입할 때 연산자 구현 차이[각주:1]만 있을 뿐 속도차이는 없다는 것을 알 수 있다.


그렇다면 객체에서는 어떨까?




객체의 경우 전위 연산자 처리가 훨씬 단순하다는 사실을 알 수 있다. 단지 스택 메모리를 추출 후 서브루틴으로 분기한다.


그에비해 후위 연산자는 상대적으로 처리가 복잡하고 코드를 잘 보면 컨테이너 소멸자까지 호출되고 있다.


Why??


std::list 코드를 열어보면




전위 연산자는 자신을 증가 시킨 후 참조형으로 리턴하고 후위 연산자는 임시 개체에 상태를 복사한 후 자신을 증가 시키고 


복사된 임시 객체를 리턴 시키고 있다. 어셈코드가 설명이 되는 부분이다.


결론 int, float, char 등 내부 타입의 경우 성능상 차이가 없지만 객체는 전위 연산자를 사용하는게 더 빠르다.


  1. 증가 후 대입 대입 후 증가 [본문으로]
프로그래밍 일반 2014. 12. 23. 16:25

C++ 유닛테스트

게임처럼 요구사항이 수시로 변하는 프로그램에서 유닛테스트를 적용하기란 쉽지가 않다.


개인적으로는 데이터 의존성 테스트나 누락여부를 체크하는 부분에 사용하는게 적당하다고 생각한다.


예로 아이템을 추가할 때 연관 설정 파일들 (사운드, 연출, 스크립트 등..)에 누락이 없는지 확인하는지 테스트


최대한 일반화 할 수 있는 것들을 케이스로 만들어 적용하면 데이터 스크립팅과 관련된 이슈들은 QA에 올린 다음 문제를 발견하고 원인을 파악하는 수고들을 덜 수 있다.


유닛 테스트 익스플로러가 지원되는 컴파일러를 사용 중이라면 문제가 없지만 그렇지 않은 경우 가볍게 사용할 수 있는 테스트 코드를 추가한다.






테스트 결과를 처리하고 수집하는 클래스만 따로 확인하면 아래의 코드를 보면 되고


/**

@brief 테스트의 결과를 처리하고 수집

테스트 객체 트리거

*/

class CTestResult

{

private :

void addSuccess(const CContext& ctxt) { m_success.push_back(ctxt) ; }

void addFailure(const CContext& ctxt) { m_failure.push_back(ctxt) ; }

void addError  (const CContext& ctxt) { m_error.push_back(ctxt)   ; }

public  :

// Helper Method

int totalRunCount() const { return (successCount() + errorCount() + failureCount()) ; }

int successCount()  const { return static_cast<int>(m_success.size()); }

int failureCount()  const { return static_cast<int>(m_failure.size()); }

int errorCount()    const { return static_cast<int>(m_error.size())  ; }


/// 현재 테스트가 모두 성공적이었는지를 체크한다.

bool wasSuccessful() const

{

if ( 0 == errorCount() && 0 == failureCount() )

return true ;

return false ;

}


/// 테스트 결과 초기화

void clear()

{

m_timer.clear()   ;

m_success.clear() ;

m_failure.clear() ;

m_error.clear()   ;

}


/// 테스트 구동 함수. 하나의 메서드에 대해서 구동을 하게 된다.

bool run(const CMethod& method)

{

// 테스트 함수 내부 for구문 처리용

bool bResult = false;

int iIterator = 0;

int iCancelIterator = -1;


m_timer.start() ;


do {


try

{

// 넘겨받은 테스트 함수를 호출한다.

bResult = method.execute(iIterator, iCancelIterator) ; 


CContext c("OK","",method.name(),"",0) ;

addSuccess(c) ;

}

catch(CFailureException& e)

{

addFailure(e.context()) ;

}

catch(std::exception& e)

{

CContext c(e.what(),"",method.name(),"",0) ;

addError(c) ;

}

catch(...)

{

CContext c("unexpected exception is occured","",method.name(),"",0) ;

addError(c) ;

}


iCancelIterator = iIterator;


} while (false == bResult);


m_timer.stop() ;

return true ;

}


/// 파일 포인터에 테스트 결과를 출력해주는 함수

friend inline void WriteResult(const CTestResult& test, FILE* fp);


/// 테스트 결과를 trace하는 함수

friend inline void TraceResult(const CTestResult& test);



private :

CTimer m_timer ; // 시간 측정용 객체


std::vector<CContext> m_success ; // 성공한 테스트 정보 객체

std::vector<CContext> m_failure ; // 실패한 테스트 정보 객체

std::vector<CContext> m_error   ; // 에러 테스트 정보 객체

} ;


유닛 테스트는 보통 테스트 assert를 사용해서 실패하면 결과를 기록하는데 테스트 케이스 구문에서 for 루프를 도는 경우 첫번째 assert 실패 시 해당 케이스는 테스트를 종료하게 된다.

그런 문제를 해결하기 위해 실패한 곳부터 다시 처리하도록 만든 부분이 do {} while 문으로 처리한 구문이다.


테스트 케이스는 CTestCase를 상속 받으면 된다.

아래는 사운드 테스트 케이스 예제..


#pragma once


#include "CppUnitTest.h"


namespace cppunit

{

namespace testcase

{

class CTestCaseDiceSound

: public cppunit::CTestCase

{

public:

CTestCaseDiceSound() {}

CTestCaseDiceSound(const CTestCaseDiceSound& rhs)

: CTestCase(rhs)

{

_append(&CTestCaseDiceSound::TestFuncCheckDiceThrowSound);

_append(&CTestCaseDiceSound::TestFuncCheckDiceSwingSound);

}


virtual CTest* clone() const override

{

return new CTestCaseDiceSound(*this);

}


/// 사운드 매핑 테이블 체크

bool TestFuncCheckDiceThrowSound(int& output_iterator, const int iCancelIterator);


/// 

bool TestFuncCheckDiceSwingSound(int& output_iterator, const int iCancelIterator);

};

}

}


/**

@brief 사운드 매핑 테이블 체크

*/

bool cppunit::testcase::CTestCaseDiceSound::TestFuncCheckDiceSwingSound( int& output_iterator, const int iCancelIterator )

{

output_iterator = 0;


for ( ..;

               ..;

++iter, ++output_iterator)

{

if (output_iterator <= iCancelIterator) continue;

    ... 

    ...

{

const bool bIsFind = strSwing != CString();


char buff[256] = {0,};

sprintf_s(buff, sizeof(buff), "[%d] find error - swing dice sound", pTempItemInfo->m_nItemCode);


// 여기가 중요하다.

// 테스트 assert 사용!!

assertTrue(buff, bIsFind);

}

}


return true;

}



케이스는 테스트 그룹에 포함된다.


/********************************************************************

purpose: 관련 테스트 그룹

*********************************************************************/

#pragma once


#include "CppUnitTest.h"

#include "TestCaseDiceSound.h"


namespace cppunit

{

namespace testgroup

{

class CTestGroupDice

: public CTestSuite

{

public :

CTestGroup_Dice()

{

addTest(std::auto_ptr<CTest>(new testcase::CTestCaseDiceSound));

  addTest(std::auto_ptr<CTest>(new 케이스));

  ..

  .. 

}

};

}

}


테스트를 실행하는 클래스 사용 예

// trace 출력

cppunit::CTestRunner::run(std::auto_ptr<cppunit::CTest>(new cppunit::testgroup::CTestGroupDice));


// 텍스트 출력

COleDateTime currentDate(COleDateTime::GetCurrentTime());

SYSTEMTIME tempTime;

currentDate.GetAsSystemTime(tempTime);


CStringA strFileName;

strFileName.Format("UnitTestResult_%4d%2d%2d_%2d%2d%2d.txt"

, tempTime.wYear

, tempTime.wMonth

, tempTime.wDay

, tempTime.wHour

, tempTime.wMinute

, tempTime.wSecond);


FILE* fp = fopen(strFileName.GetString(), "ab");

cppunit::CTestRunner::run(std::auto_ptr<cppunit::CTest>(new cppunit::testgroup::CTestGroupDice), fp);

fclose(fp);


테스트 결과 출력 예



프로그래밍 일반 2014. 10. 24. 11:28

통합 빌드 시스템(Unity Build) 허드슨에 구축하기

예전에 통합 빌드 시스템(Unity Build)에 관한 이야기를 한적이 있었다.


링크 : 통합 빌드 시스템(Unity Build)


이번에는 통합 빌드 시스템을 허드슨 msbuild 에서 사용하는 방법에 대한 이야기해보려고 한다.


Pre-Build Event 명령어

$(SolutionDir)externals\UnityMaker\UnityMaker.exe $(ProjectPath) IndonesiaRelease UnityBuild 2


우선 빌드전 이벤트 명령어가 달라졌는데 개발 환경에 따라 유연하게 대처하기 위한 매개변수들이 늘어났을 뿐이다.


로컬에서 개발하는 경우에는 해당 명령어가 잘 동작하지만 허드슨에서는 빌드 환경 설정에 따라 문제가 발생할 수도 있는데


허드슨 MSBuild 를 사용하는 경우 VS와 매크로가 다르기 때문이다.


로컬 빌드에서는 VS Pre-Build Event 에 실행 명령어를 작업하면 되고 라이브 배포용 빌드에서는 해당 이벤트 명령어를 빌드에서 제외 시킨다.





허드슨 빌드 환경에서 프로젝트 빌드 전에 Execute windows batch command를 하나 추가한다.


build step를 하나 추가하고 'Build a Visual Studio project or solution using MSBuild' 위로 추가한 build step을 드래그해서 올린다.


@rem UnityMaker 실행

SET SOLUTION_DIR=%CD%\

SET PROJECT_XML=%CD%\src\Client.vcproj

%SOLUTION_DIR%externals\UnityMaker\UnityMaker.exe %PROJECT_XML% %_JOBS_PROJECTBUILD% UnityBuild 2

SET ERR_LEVEL=%errorlevel%


@rem UnityMaker.exe 처리 성공한다면 .vcproj 변경 커밋

IF "%ERR_LEVEL%"=="0" (

 svn commit %PROJECT_XML% -m "hudson unity build"

)


exit /b %ERR_LEVEL%


build step에 로컬빌드와 같은 의미의 명령어를 환경에 맞도록 작성해주면 허드슨 셋팅은 끝이다.


이 단계에서 고려할 사항이 혹시라도 기존에 작성한 통합 빌드 시스템 구축하기에 첨부한 소스를 참고해서 Unitymaker를 만들었다면 허드슨 빌드 환경에서 빌드에 실패할 것이다.


그 이유는 UnityMaker의 리턴 값 때문인데..기본적으로 명령 프롬프트 상에서 어떤 프로그램이나 명령어를 실행시킬 때, 프로그램이 성공적으로 끝나면 정수 0(제로)을 OS에 반환하며 끝낸다는 룰 때문이다. 


이것을 Error Level 이라고 하는데 %ERRORLEVEL% 이라는 환경 변수 안에 최근에 종료된 프로그램/명령어가 돌려준 값이 들어 있다.


BuildLog.htm 파일을 확인하면 아래와 같다.





이런 문제는 UnityMaker 프로그램이 정상적으로 종료될 때 0을 리턴하도록 수정하고 실패 리턴값을 세분화하면 된다.





프로그래밍 일반 2014. 10. 22. 18:28

enum 범위 한정 기법과 보다 안전한 문자열 표현

C++ 11 이전 버전의 열거형은 허용범위 밖에서 열거형 이름을 사용할 수 있기 때문에 이름 충돌 문제가 발생한다.


참고 : C++ 11 enum 스펙


일반적으로는 문제가 되지 않지만 통합 빌드(Unity Build) 환경에서는 골치 아픈 문제이기도 하다.


이런 문제는 namespace를 이용해서 피해갈 수 있다.


namespace EType

{

           enum Type

           {

               Null

               , Guerrilla

               , Always

               , MiniDuration

               , CubePiece

               , Discount   

           };

}

범위를 설정하고 내부적으로 열거형의 이름은 Type을 사용한다.


이렇게 사용하면 이름 중복을 피하기 위해 무리해서 이름을 작성할 필요도 없어진다.


eType_Guerrilla

, eType_Always


이름충돌을 피하기 위한 구분자_사용하는 이름, 

.....


만약 클래스 내부에서 사용하는 열거형인 경우에는 

struct EType

{

           enum Type

           {

               Null

               , Guerrilla

               , Always

               , MiniDuration

               , Max

           };

};

이런 식으로 열거형의 범위를 한정할 수가 있다.


위에서 설명한 기법들은 언리얼 엔진 프로그래밍 가이드에 나와있는 old style 표기법이다. 만약 컴파일러가 지원한다 enum class를 사용하도록 한다.


여기에 추가로 열거형을 스트링으로 표현하고 싶을 때 어떻게 하면 좋은지 설명하도록 하겠다.


일반적으로 열거형을 선언한 후 코드 어딘가에 스트링 표현식을 작성할 것이다.


... 코드 어딘가 열거형 스트링

TCHAR* szTypeString[] = {

 _T("Null")

, _T("Guerrilla")

, _T("Always")

, _T("MiniDuration")

, _T("Max")

};

 

... 코드 어딘가에서 출력

for (int i = EType::Null; i < EType::Max; ++i)

{

           출력함수(szTypeString[i]);

}

이런 방식은 열거형이 변경될 때마다 항상 스트링도 수정해야하는데 최악의 경우 프로그램을 죽이는 버그를 만들 수도 있다.


컴파일러는 위의 위험 사항을 알려주지 않는다.


디파인 함수를 이용해서 스트링으로 바꾸는 기법은 예전에 한번 설명한 적이 있는데


참고 : 열거형 스트링 표현


아래와 같은 코드는 열거형의 범위를 한정하고 문자열로 표현하는데 보다 안전하다.


#if !defined(DO_TEXT)

#define DO_TEXT(e)    L#e,

#endif


#if !defined(DO_ENUM)

#define DO_ENUM(e)    e,

#endif


namespace EType

{

#define DEF_EType(NAME) \

                     NAME(Null) \

                     NAME(Guerrilla) \

                     NAME(Always) \

                     NAME(MiniDuration) \

                     NAME(CubePiece) \

                     NAME(Discount)

 

           enum Type

           {

                     DEF_EType(DO_ENUM)

           };

 

           inline const TCHAR* GetConvertSring(EType::Type type)

           {

                    const TCHAR* szString[] = {

                                DEF_EType(DO_TEXT)

                     };

 

                     return szString[type];

           }

}


프로그래밍 일반 2014. 9. 15. 11:56

통합 빌드 시스템 구축하기 (Unity Build)


코드 종속성를 해결하기 위한 작업들은 너무 기본적인 부분이기 때문에 여기에선 논외로 치고..


프로젝트 규모가 커지면 왜 이렇게 빌드 시간이 오래 걸리는가..


빌드 명령어를 실행했을 때 컴파일러가 하는 일을 한번 들여다보자..


VS C/C++ 명령어에 /E 옵션을 추가하면 컴파일러의 전처리 과정을 확인 할 수 있는데 해당 옵션을 적용 시킨 후 간단한 hello world 프로그램을 빌드해보자.


결과를 보면..



3444 라인..


???


여기에 간단한 클래스를 추가해보자..

#pragma once


struct SBossSkill;

class CBoardProcess;


namespace skill

{

class CSkill

{

public :

CSkill(const SBossSkill* pSkillInfo = NULL) : m_pReferSkillBaseInfo(pSkillInfo) 

{

}

virtual ~CSkill() {}


// 스킬을 실행 합니다.

BOOL IsExecuteSkill(int iTargetPnum, CBoardProcess* pBoardPorcess) const;


protected:

virtual BOOL IsExecute(int iTargetPnum, CBoardProcess* pBoardPorcess, const std::vector<int>& myBlockList) const = 0;


protected:

const SBossSkill* m_pReferSkillBaseInfo;

};

}


스킬을 상속받는 모래폭풍을 추가하고..

#pragma once


#include <vector>


#include "skill.h"



namespace skill

{


class CSandStorm

: public CSkill

{

public :

CSandStorm(const  SBossSkill* pSkillInfo = NULL) : CSkill(pSkillInfo) {}

virtual ~CSandStorm() {}


protected:

virtual BOOL IsExecute(int iTargetPnum, CBoardProcess* pBoardPorcess, const std::vector<int>& myBlockList) const override;

};

}


심플하게 관리자를 하나 둔 후..

#pragma once


#include <map>

#include "noncopyable.h"


namespace EBossSkill { enum Type; }

struct SBossSkill;



class CSkillManager

: private virtual INonCopyable

{

private:

CSkillManager() {}

virtual ~CSkillManager();


public :

static CSkillManager& GetInstance() {


static CSkillManager instance;

return instance;

}


BOOL LoadData();


const SBossSkill* GetSkillInfo(EBossSkill::Type find);

const skill::CSkill* GetSkillObject(EBossSkill::Type find);


private:

typedef std::map<EBossSkill::Type, SBossSkill*> SKILL_INFO;

typedef std::map<EBossSkill::Type, skill::CSkill*> SKILL_EXE;


SKILL_INFO m_bossSkillInfo; // 스킬 정보

SKILL_EXE m_bossSkillExe; // 스킬 실행

};


빌드를 하면..



151039 라인...


스킬을 하나 더 추가하면..


164116 라인...


모듈이 헤더파일을 포함하면 포함한 만큼 컴파일러에 전처리 과정이 추가된다..


이러한 이유 때문에 프로젝트의 크기가 커져서 모듈의 수가 많아지면 많아질 수록 빌드시간은 늘어나게 되는 것이다..


모듈에서 호출되는 기본 라이브러리들은 미리컴파일된 헤더에 포함하고.. 코드 종속성도 고려해서 작업하면 어느정도 효과를 볼 수 있다.


담당중인 프로젝트에서도 기본 라이브러리들은 미리컴파일된 헤더에 포함시키고 실타래처럼 역인 코드 종속성을 완화시키는 작업을 통해 재컴파일되는 파일 수량은 기존대비 90%감소하고 리빌드 시간은 22% 감소했다.


대단히 빨라진 것 같지만.. 아직도 3분 40초대.. 프로젝트가 작아서 이정도지 만약 규모가 큰 프로젝트라면 10분에서 30분도 걸리는게 빌드 시간이라는걸 생각하면 부족한게 사실..


그러면 이쯤에서 Unity Build에 대해 알아보도록 하자..


유니티 빌드의 개념은 단순하다.


위에서 모듈이 늘어날 수록 동일한 전처리 과정이 늘어나서 빌드 시간이 길어진다는 사실을 알았기 때문에 소스가 하나라면 불필요한 전처리 과정은 사라지지 않을까..


테스트..

cpp 파일을 빌드에서 제외 시킨 후


cpp를 하나로 통합하고..

// HelloWorld.cpp : Defines the entry point for the console application.

//

#include "stdafx.h"


#include "SandHell.h"


BOOL skill::CSandHell::IsExecute( int iTargetPnum, CBoardProcess* pBoardPorcess, const std::vector<int>& myBlockList ) const 

{

return TRUE;

}



#include "SandStorm.h"


BOOL skill::CSandStorm::IsExecute( int iTargetPnum, CBoardProcess* pBoardPorcess, const std::vector<int>& myBlockList ) const 

{

return TRUE;

}



#include "skillManager.h"

#include "SandStorm.h"

#include "SandHell.h"


BOOL CSkillManager::LoadData()

{

return TRUE;

}


const SBossSkill* CSkillManager::GetSkillInfo( EBossSkill::Type find )

{

return NULL;

}


const skill::CSkill* CSkillManager::GetSkillObject( EBossSkill::Type find )

{

return NULL;

}



int _tmain(int argc, _TCHAR* argv[])

{

printf("hello world\n");

return 0;

}

빌드하면..

70838 라인...

불필요한 전처리가 많이 줄었다..


그럼 작업 중인 프로젝트의 소스파일을 어떻게 통합하느냐...


// HelloWorld.cpp : Defines the entry point for the console application.

//

#include "stdafx.h"


#include "SandHell.cpp"

#include "SandStorm.cpp"

#include "skillManager.cpp"



int _tmain(int argc, _TCHAR* argv[])

{

printf("hello world\n");

return 0;

}

cpp를 직접 포함하면 된다.


이쯤에서 중간 정리를 해보면..


수작업

  • 기존 솔루션에 통합 cpp 추가
  • 통합 cpp 빌드에서 제외
  • 통합 빌드 환경 추가 
  • 통합 빌드 환경은 미리 컴파일된 헤더를 사용하지 않는다.

 자동화

  •  stdafx.cpp 와 통합 cpp 파일을 제외한 cpp 파일들을 빌드에서 제외
  • 빌드에서 제외한 cpp 파일을 포함한 통합 cpp 파일 갱신
  • 빌드 전 이벤트로 실행



이제 vc 프로젝트 파일을 메모장으로 열어보면..


.vcproj 파일이 xml 문서라는 사실을 알 수 있는데..

자동화하는 방법은 프로젝트 파일을 파싱해서 대상 파일들을 빌드에서 제외시키고 제외된 목록을 알아오는 것으로 간단히 처리할 수 있다.


xml 파서 오픈 소스 라이브러리 : 

Markup115.zip



파일을 두개로 통합하는 예)

1. 기존 솔루션에 통합빌드 소스 추가 후 빌드에서 제외 시키기.




2. 통합 빌드 환경 추가




3. 통합 빌드 환경은 미리 컴파일된 헤더를 사용하지 않는다.




4. 대상 파일들을 빌드에서 제외하고 통합 빌드 파일에서 include 한다.






5. 자동화 프로그램 UnityMaker


// stdafx.h 

// CMakeUp 클래스가 stl을 사용하도록 셋팅

#define MARKUP_STL


메인 로직




함수들..



UnityMaker.exe 실행 배치 파일 Base.bat

@rem 경로 단순화

CD..


@rem 실행파일, 프로젝트 xml, 솔루션, 통합작업에서 제외할 필터 이름, 통합 소스 수

%CD%\externals\UnityMaker\UnityMaker.exe %CD%\src\Client.vcproj Base UnityBuild 2


pause



6. 빌드 이벤트 등록




7. 적용 결과

디버그 빌드 37초

릴리즈 빌드 30초


대략 80 ~ 90% 정도 감소를 보인다.


빌드 시간 확인하는 방법

툴 -> 옵션 -> 프로젝트 & 솔루션 -> 프로젝트 셋팅 -> Build Timing (Yes)



8. 참고 사항

- define, 지역 변수, enum 등 이름 충돌 문제는 이번 기회에 수정하자.

- 디버깅도 가능 잘된다.

- include 누락은 기존 빌드환경에서 테스트 가능하다.


9. 링크 결과물




10. 소스


UnityMaker.zip




11. 통합 빌드 시스템 허드슨에 구축하기

링크 : 통합 빌드 허드슨에 구축하기