담당 프로젝트 2021. 11. 10. 12:29

펑키러쉬

2020년 6월 ~ 2022년 1월

 

사용 언어 및 라이브러리 : C#, OWIN Self hosting, Linq, Super socket

툴 : VS2019, IBM Cloud, MySQL, Unity 2020.3.14f1

 

5vs5 MOVA 게임 개발 및 출시

 

Jenkins와 각종 툴 연동

 - 빌드, 테이블 및 코드 생성, 패치, 배포 등 자동화

TCP/IP 엔진 제작 및 각종 백앤드 구현

 - 마스터, 런처, 로그인, 게임, 채팅, 매칭, 랭킹 서버 디자인 및 구현

 - 디비 설계 및 구현

웹서버 제작

 - 버전, 웹 스토어

디비 설계 및 구현

 

 

프로그래밍 일반 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);
        }
    }
}

참 쉽다..끝

담당 프로젝트 2018. 8. 21. 03:23

무지개나무

2018년 7 ~ 2019년 7 출시 완료

 

사용 언어 및 라이브러리: C#, Unity 2018.1, OWIN Self hosting. MS Sql, AWS.

 

스타트업 공동 창업, CTO

 

간단한 업무 내용

- 유니티를 이용한 게임 클라이언트 개발

- 자동화 툴 개발

- 운영툴 개발

- 서버 빌드 시스템 구축

- 서버 및 디비 개발

- AWS를 이용한 서비스 시스템 구축

담당 프로젝트 2018. 8. 13. 22:39

사커킹



2018년 1월 ~ 2018년 5월, 월드 사커킹 개발팀


링크 : https://itunes.apple.com/kr/app/%EC%9B%94%EB%93%9C-%EC%82%AC%EC%BB%A4-%ED%82%B9-world-soccer-king/id789094092?mt=8


사용 언어 및 라이브러리: C#, SuperSocket, MSSQL,


간단한 업무 내용

- Jenkins를 이용한 서버 개발 프로세스 구축

- 서버 빌드 및 배포, 테이블 패치 및 배포, 관리 서버 빌드 및 배포, 테이블 논리적 오류 검증 등을 Jenkins 프로세스에 추가

- Super socket을 이용한 서버 아키텍쳐 설계 및 구현

- 게임 컨텐츠 구현

- 서버 구현 후 소프트 런칭까지 할당된 작업 시간 4개월

담당 프로젝트 2018. 8. 13. 22:03

아처리킹



2017년 3월 ~ 2018년 5월, 아처리킹 개발팀


링크 : https://itunes.apple.com/kr/app/%EC%95%84%EC%B2%98%EB%A6%AC-%ED%82%B9/id1121971067?mt=8


사용 언어 및 라이브러리: C#, Proudnet, Linq to MySQL


간단한 업무 내용

- Jenkins를 이용한 개발 프로세스 구축

- 업무를 효과적으로 처리하기 위한 자동화툴 개발 및 적용, 관리 서버 런처 배포 자동화 시스템 구축

- 게임 컨텐츠 개발 및 운영

프로그래밍 일반 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 를 이용하자.

SQL 2016. 9. 21. 12:25

MySQL GUID 저장 팁

대시 기호 제거 후 UNHEX() 함수를 사용하여 16바이트 숫자로 변환 후 BINARY(16) 칼럼에 저장, 해당 값은 HEX() 함수를 이용하면 16진수로 볼 수 있다.


일반적으로 CHAR(32)에 저장했을 때보다 저장 용량이 절반으로 줄어들고 비교 속도가 빨라진다.

프로그래밍 일반 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;

            }


        }