Tag Archives: C#

C#/ Using과 IDisposable

Using과 IDisposable

C# 에서 메모리는 가비지 컬렉터가 관리해 주지만, 그 외의 열린 파일, 스트림 같은 관리되지 않는 리소스들은 인식하지 못한다. 이런 리소스들은 프로그래머가 명시적으로 해제를 해줘야 하는데 그때 사용하는게 Dispose()이다.

가비지 컬렉터가 제어할 수 없는 리소스를 제어하는 클래스가 IDisposable을 상속 받아 Dispose()를 구현하고 using을 통해 사용하면 된다.

public class Book : IDisposable
{
    public void Dispose ()
    {
        // 리소스 해제
    }
}

public class Program
{
    public void ReadBook ()
    {
        using (Book novel = new Book())
        {
            // 실행문
        }
    }
}

위와 같이 코드를 구성하면 using 블록을 빠져나갈 때 프로그램이 Dispose()를 실행시켜줘서 리소스를 자동으로 해제하게 된다. using이 네임스페이스를 지정하는 것 외에 Dispose()를 실행시키는 것으로도 쓰이는 것이다.

C#/ DLL 사용하기

C#에서 만든 DLL 사용하기

C#에서 만들어진 DLL 사용하는 방법은 매우 쉽다. using 키워드로 해당 DLL 을 불러온 후에 일반 클래스 사용하듯이 사용하면 된다.

using MyDLL;

static void Main (string[] args)
{
    // MyDLL에 정의된 클래스로 객체 정의하기
    Calcurate cal = new Calcurate();

    int resultAdd = cal.add(1, 2);
    int resultMinus = cal.minus(5, 3);
    int resultMultiply = cal.multiply(2, 4);
    int resultDivide = cal.divide(6, 3);
}

C, C++에서 만든 DLL 사용하기

C나 C++로 만든 DLL은 System.Runtime.InteropServices을 using 한 후에, DLLImport라는 애트리뷰트와 extern이라는 키워드를 이용해서 사용해야 한다.

using System.Runtime.InteropServices;

public class SqliteDatabase
{
    [DllImport("sqlite3", EntryPoint = "sqlite3_open")]
    private static extern int sqlite3_open (string filename, out IntPtr db);

    ...

    void Open (string path)
    {
        IntPtr _connection;

        if (sqlite3_open (path, out _connection) != 0) 
        {
            throw new SqliteException ("Could not open database file: " + path);
        }
    }
}

extern은 해당 메서드의 구현을 바깥 –여기서는 DLL– 에 맡긴다는 의미다. extern 한정자가 붙은 메서드는 static으로 선언되어야 한다.

extern 키워드가 들어간 함수 이름과 매개변수는 DLL 코드 안에 정의된 것과 같아야 한다. 이렇게 정의된 메서드는 일반 메서드처럼 사용할 수 있다.

C#/ String과 StringBuilder

String과 StringBuilder

string은 char[]로서 immutable 타입이다. 이것이 무슨 말인고 하니 string에 ‘+’ 연산을 하면, 현재 string에 새로운 string에 더해지는게 아니라, 현재 string과 새로운 string을 더한 새로운 string을 만들어 낸다는 이야기다.

아래와 같은 코드는 loop를 돌 때마다 추가적으로 string을 만들어 낸다. string이 char[]임을 생각해보면 사실 당연한 결과다.

string num = "";

for (int i = 0; i < 10; i++)
{
    num += i.ToString();
}

이는 자원 낭비이므로 위와 같이 string을 반복적으로 연산할 일이 있을 때는 StringBuilder를 사용하도록 권장된다. StringBuilder는 string과는 사용법이 약간 다른데, 변수보다는 메서드를 사용하는 방식에 가깝다.

StringBuilder num = new StringBuilder();

for (int i = 0; i < 10; i++)
{
    num.Append(i.ToString());
}

StringBuilder는 ToString() 으로 string 타입으로 변환시킬 수 있다.

C#/ C#의 메모리 구조

C#의 메모리는 3가지 영역으로 구분된다.

데이터

데이터를 보관하는 영역, static 등이 해당된다. static 으로 선언된 데이터는 프로그램이 시작될 때 메모리에 올라왔다가 프로그램이 종료될 때까지 메모리에서 해제되지 않는다.

스택

값형 변수들이 올라오는 영역. 메서드가 종료되면 스택 메모리는 해제된다. 메모리의 스택 영역은 컴파일시에 결정된다.

참조형 변수들이 올라오는 영역. 엄밀히 말하면 참조형 변수들은 스택과 힙을 모두 사용하는데, 힙에는 데이터가 올라오고, 스택에는 힙의 메모리 주소가 올라온다. 메모리의 힙 영역은 런타임시에 결정된다.

복수의 스택이 힙 메모리의 주소만 갖고 있으면 하나의 데이터를 여러 곳에서 사용할 수 있는 형식이기 때문에 전체 메모리 관리에 좋다.

스택과 달리 메서드가 종료되도 사라지지 않는데 –물론 주소를 들고 있는 스택 메모리는 사라짐– 만일 힙 메모리의 주소를 갖고 있는 스택이 없으면 가비지 컬렉터가 수거해 간다.

뇌를 자극하는 C# 4.0 프로그래밍/ 가비지 컬렉션

가비지 컬렉터를 아시나요

  • C와 C++의 메모리 관리 문제점
    • 객체를 할당하기 위해 일일이 메모리 공간을 확보해야 하며, 객체를 할당한 후에는 힙을 가리키는 포인터를 잘 유지하고 있다가 객체를 다 사용하고 나면 해당 포인터가 가리키고 있는 메모리를 해제해줘야 한다. 이 때 프로그래머들이 실수로 객체의 메모리를 해제하는 것을 잊으면 메모리 누수가 발생한다.
    • 한편 메모리를 제대로 해제 했는데, 해제한 줄 모르고 그 포인터에 접근해서 코드를 실행하는 실수가 발생하는 경우도 많다. 그 포인터가 가리키는 메모리가 비어있을 수도 있지만 엉뚱한 코드가 그 자리를 대신 차지하고 있을 수도 있기 때문에 어떤 일이 벌어질지 예측하기가 어렵다.
  • 한편 프로그래머의 실수와는 별도로 C와 C++은 힙에 객체를 할당하기 위해 비싼 비용을 치뤄야 한다는 문제도 있다.
    • C, C++ 기반의 프로그램을 실행하는 C-런타임은 객체를 담기 위한 메모리를 여러 개의 블록으로 나눈 뒤, 이 블록을 링크드 리스트로 묶어서 관리한다. 가령 어떤 객체를 힙에 할당하는 코드가 실행되면 C-런타임은 메모리 링크드 리스트를 순차적으로 탐색하면서 해당 객체를 담을 만한 여유가 있는 메모리 블록을 찾느나. 적절한 크기의 메모리 블록을 만나면 프로그램은 이 메모리 블록을 쪼개서 객체를 할당하고 메모리 블록의 링크드 리스트를 재조정한다.
    • 정리하자면 단순히 메모리 공간에 데이터를 집어 넣는 것이 아니라 공간을 ‘탐색’하고 ‘분할’하고 ‘재조정’하는 오버헤드가 발생한다는 것
  • 반면 C#은 CLR이 자동 메모리 관리를 해주기 때문에 이런 문제들로부터 자유롭다. 이 자동 메모리 관리 기능의 중심에는 가비지 컬렉션이 있다. 가비지 컬렉션은 프로그래머가 무한한 메모리를 갖고 있는 것처럼 간주하고 코드를 작성할 수 있도록 한다.
  • 가비지 컬렉션을 담당하는 것을 가비지 컬렉터라고 하는데, 프로그래머가 객체를 할당해서 일을 하면 가비지 컬렉터는 객체 중에 쓰레기인 것과 쓰레기가 아닌 것을 완벽하게 분리해서 쓰레기들만 수거해 간다.
  • 가비지 컬렉터가 똑똑하게 일을 하지만 가비지 컬렉터 자체도 소프트웨어이기 때문에 CPU와 메모리 같은 컴퓨팅 자원을 소모한다. 우리가 작성한 코드도 사용해야 하는 그 자원을 가비지 컬렉터도 같이 사용해야 한다는 이야기. 만약 가비지 컬렉터가 최소한으로 이 자원을 사용하게 만들 수 있다면 프로그램의 성능을 아낀 자원의 양만큼 끌어올릴 수 있게 된다.
  • 참고) 기본적으로 C#으로 작성된 모든 코드는 CLR에게 관리되는 관리형 코드(Managed Code)에 속한다. 한편 비관리형 코드(Unmanaged Code)도 작성할 수 있는데, unsafe 키워드를 사용하면 된다. 대신 이 경우에는 가비지 컬렉션을 포함하여 CLR이 제공하는 서비스를 받을 수 없다.

개처럼 할당하고 정승처럼 수거하라

  • C#으로 작성한 소스 코드를 컴파일해서 실행파일을 만들고 이 실행 파일을 실행하면 CLR은 이 프로그램을 위한 일정 크기의 메모리를 확보한다. C-런타임처럼 메모리를 쪼개는 일은 하지 않고, 넓은 메모리 공간을 통째로 확보해서 하나의 관리되는 힙(Managed Heap)을 마련한다. 그리고 CLR은 이렇게 확보한 관리되는 힙 메모리의 첫 번째 주소에 ‘다음 객체를 할당할 메모리의 포인터’를 위치시킨다.

  • 비어 있는 힙에 첫 번째 객체를 할당하면 –object A = new object()– CLR은 ‘다음 객체를 할당할 메모리의 포인터’가 가리키는 주소에 A 객체를 할당하고 포인터를 A 객체가 차지하고 있는 공간 바로 뒤로 이동시킨다.

  • 여기서 객체를 또 할당하면 첫 번째 객체의 바로 뒤에 새로운 객체가 할당된다.

  • CLR은 객체가 위치할 메모리를 할당하기 위해 메모리 공간을 쪼개 링크드 리스트를 탐색하는 시간도 소요하지 않으며, 그 공간을 다시 나눈 뒤에 리스트를 재조정하는 작업도 필요로 하지 않는다. C-런타임에 비해 객체 할당 메커니즘이 단순한데다 효율적이다.
  • 값 형식 객체는 스택에 할당 되었다가 자신이 태어난 코드 블록이 끝나면 메모리로부터 바로 사라지고 참조 형식 객체들만이 힙에 할당되어 코드 블록과 관계없이 살아남는다. 만일 참조 객체를 만들면 객체의 내용물은 힙에 할당되지만 스택에 힙 메모리 주소의 참조가 생성된다.

  • 위 그림과 같은 상황에서 객체가 담긴 코드 블록이 끝나서 스택이 사라지면 힙에는 더는 접근할 수 없어서 사용할 수 없는 메모리가 남게 된다.

  • 객체를 잃은 채 힙에 남아 있는 객체는 쓰레기가 되고 이 쓰레기는 가비지 컬렉터가 집어가게 된다.
  • 한편 사라져 버린 객체처럼 할당된 메모리의 위치를 참조하는 객체를 루트(Root)라고 부르는데 루트는 스택에 생성될 수도 있고 정적 필드처럼 힙에 생성될 수도 있다. .NET 응용 프로그램이 실행되면 JIT 컴파일러가 이 루트들을 목록으로 만들고, CLR은 이 루트 목록을 관리하며 상태를 갱신한다. 이 루트가 중요한 이유는 가비지 컬렉터가 CLR이 관리하고 있던 루트 목록을 참조해서 쓰레기 수집을 하기 때문이다.

  • 가비지 컬렉터가 루트 목록을 이용해서 쓰레기 객체를 정리하는 과정은 다음과 같다.
    1. 작업을 시작하기 전에 가비지 컬렉터는 모든 객체가 쓰레기라고 가정한다. 즉, 루트 목록 내의 어떤 루트도 메모리를 가리키지 않는다고 가정한다.
    2. 루트 목록을 순회하면서 각 루트가 참조하고 있는 힙 객체와의 관계 여부를 조사한다. 막약 루트가 참조하고 있는 힙의 객체가 또 다른 힙 객체를 참조하고 있다면 이 역시도 해당 루트와 관계가 있는 것으로 판단한다. 이때 어떤 루트와도 관계가 없는 힙의 객체들은 쓰레기로 간주된다.
    3. 쓰레기 객체가 차지하고 있던 메모리는 ‘비어 있는 공간’이 된다.
    4. 루트 목록에 대한 조사가 끝나면 가비지 컬렉터는 이제 힙을 순회하면서 쓰레기가 차지하고 있던 ‘비어 있는 공간’에 쓰레기의 인접 객체들을 이동시켜 차곡차곡 채워 넣는다. 모든 깨체의 이동이 끝나면 깨끗한 상태의 메모리를 얻게 된다.

세대별 가비지 컬렉션

  • 버스에서 빨리 내리려는 승객은 출입구 쪽에 있고 늦게 내리려는 승객은 출입구 반대편에 있는 것처럼 CLR의 메모리도 구역을 나누어 메모리에서 빨리 해제될 객체와 오래 살아남을 것 같은 객체들을 따로 담아 관리한다.
  • CLR은 메모리를 0, 1, 2의 3개 세대로 나누고 0세대에는 빨리 사라질 것으로 예상되는 객체들을, 2세대에는 오랫동안 살아남을 것으로 예상되는 객체들을 위치시킨다.
  • CLR은 객체의 나이가 어릴수록 메모리에서 빨리 사라지고, 나이가 많을수록 메모리에서 오래 살아 남는다고 간주한다. 여기서 나이는 가비지 컬렉션을 겪은 횟수를 의미하는데, 따라서 0세대에서는 가비지 컬렉션을 한 번도 겪지 않은 ‘갓 생성된’ 객체들이 위치하고 2세대에는 최소 2회에서 수차례 가비지 컬렉션을 겪고도 살아남은 산전 수전 다 겪은 객체들이 위치한다.
  • .NET 응용 프로그램이 시작되면 CLR은 다음과 같은 (비어 있는) 관리되는 힙을 확보한다. 이 힙에는 아직 어떤 객체도 할당되지 않은 상태이다.

  • 응용 프로그램이 일을 시작함에 따라 할당된 객체들로 힙이 차오른다.

  • 할당된 객체들의 총 크기가 0세대 가비지 컬렉션 임계치에 도달하면 가비지 컬렉터는 0세대에 대해 가비지 컬렉션을 수행하고 여기서 살아남은 객체들을 1세대로 옮긴다. 이로서 0세대는 깨끗하게 비워지며, 2세대는 아직 깨끗한 상태로 남아 있게 된다.

  • 응용 프로그램은 여전히 객체를 생성해서 일을 하고 새로 생성된 객체들은 0세대에 할당된다.

  • 다시 0세대 객체의 용량이 0세대 가비지 컬렉션 임계치를 넘어서면 가비지 컬렉터가 다시 가비지 컬렉션을 수행한다.

  • 0세대는 깨끗히 비워졌지만 또다시 응용 프로그램에 의해 새로운 객체들이 할당된다. 이번에는 1세대의 임계치가 초과됐기 때문에 1세대에 대해 가비지 컬렉션을 수행한다. 이때 가비지 컬렉터는 하위 세대에 대해서도 가비지 컬렉션을 수행하기 때문에 0세대와 1세대에 대한 가비지 컬렉션이 수행된다. 여기서 살아남은 0세대 객체들은 1세대로, 2세대에서 살아남은 객체들은 2세대로 옮겨 간다.

  • 다시 응용 프로그램이 일을 수행하고 0세대가 객체들로 차오른다. 각 세대의 메모리 임계치에 따라 가비지 컬렉션이 수행되고, 가비지 컬렉션이 반복됨에 따라 0세대의 객체들은 1세대로, 1세대의 객체들은 2세대로 계속 이동한다. 2세대로 옮겨간 객체들은 더는 다른 곳으로 옮겨가지 않고 그곳에 정착한다.
  • 2세대도 포화되어 2세대에 대한 가비지 컬렉션이 수행되면 가비지 컬렉터는 동시에 1세대와 0세대에 대해서도 가비지 컬렉션을 수행한다. 그래서 2세대 가비지 컬렉션을 전체 가비지 컬렉션이라고 부르기도 한다.

  • 2세대의 힙이 가득차게 되면 CLR은 응용 프로그램의 실행을 ‘잠시 멈추고’ 전체 가비지 컬렉션을 실행하여 여유 메모리를 확보하려고 하는데, 이때 응용 프로그램이 차지하고 있던 메모리가 크면 클수록 Full GC 시간이 길어지므로 응용 프로그램이 정지하는 시간도 그만큼 늘어나게 된다.

가비지 컬렉션을 이해했습니다. 우리는 뭘 해야 하죠?

객체를 너무 많이 할당하지 마세요

  • CLR의 객체 할당 속도가 빠르긴 하지만 너무 많은 수의 객체가 관리되는 힙의 각 세대애 대해 메모리 포화를 초래하고 이는 빈번한 가비지 컬렉션을 부르는 결과를 낳는다.

너무 큰 객체 할당을 피하세요

  • CLR은 보통 크기의 객체를 할당하는 힙과는 별도로 85kb 이상의 대형 객체를 할당하기 위해 ‘대형 객체 힙(Large Object Heap, LOH)’을 따로 유지한다. 커다란 객체를 소형 객체 힙에 할당하면 0세대가 빠르게 차오르므로 이는 좋은 방법이다.
  • 그런데 이 대형 객체 힙은 메모리를 0 바이트의 낭비도 없이 사용하는 소형 객체 힙(Small Object Heap, SOH)과 달리 큰 공간을 군데군데 낭비하게 된다. 또한 CLR이 LOH를 2세대 힙으로 간주하기 때문에 LOH에 있는 쓰레기 객체가 수거되려면 2세대에 대한 가비지 컬렉션이 수행되어야 한다. 2세대에 대한 가비지 컬렉션은 전 세대에 대한 가비지 컬렉션을 뜻하기 때문에 조심해야 한다.

너무 복잡한 참조 관계는 만들지 마세요

  • 위 그림과 같은 클래스간 복잡한 참조 관계는 가독성을 떨어뜨리기 때문에 지양해야 한다.
  • 한편 이렇게 참조 관계가 많은 객체는 가비지 컬렉션 후 살아 남았을 때 문제가 된다. 가비지 컬렉터는 가비지 컬렉션 후에 살아 남은 객체의 세대를 옮기기 위해 메모리 복사를 수행하는데, 이때 참조 관계가 복잡한 객체의 경우 단순히 메모리 복사를 하는데 끝나지 않고 객체를 구성하고 있는 각 필드 객체 간의 참조 관계를 일일이 조사해서 참조하고 있는 메모리 주소를 전부 수정하게 된다. 클래스 구조를 간단하게 만들었으면 메모리 복사만으로 끝났을 일이 탐색과 수정까지 하게 되는 것.
  • 또한 복잡한 참조 관계에서 쓰기 장벽을 통해 가비지 컬렉션을 당하면 안되는 객체를 보호하는 일을 하게 된느데 이 때 쓰기 장벽을 생성하는데 드는 오버 헤드가 크기 때문에 참조 관계는 최소한으로 만드는 것이 좋다.

루트를 너무 많이 만들지 마세요

  • 가비지 컬렉터는 루트 목록을 돌면서 쓰레기를 찾아내는데, 루트 목록이 적어지면 그만큼 가비지 컬렉터가 검사를 수행하는 횟수가 줄어드므로 더 빠릴 가비지 컬렉션을 끝낼 수 있다.

뇌를 자극하는 C# 4.0 프로그래밍/ 네트워크 프로그래밍

네트워크 프로그래밍에 앞서 알아 두어야 할 기초

인터넷의 유래

  • 천공 카드로 입, 출력을 하던 시기를 지나 프로그램을 하나만 실행할 수 있었던 컴퓨터는 동시에 여러 가지 프로그램을 실행할 수 있었다.
  • 이 시기의 컴퓨터 사용 방식은 아래 그림처럼 중앙 컴퓨터에 더미 터미널 여러개를 연결하는 것이었다.
  • 더미 터미널은 연산 능력은 전혀 없는 대신 입력과 출력을 할 수 있는 기능만 있었다. 이 더미 터미널은 아무 지능이 없었지만 큰 의미가 있는 기능을 갖추고 있었는데 그것이 바로 중앙 컴퓨터와 데이터를 주고 받는 기능이었다.

  • 1957년 소련이 미국보다 먼저 우주에 위성을 쏘는데 성공하자 이에 충격을 받은 미국은 과학 기술에 많은 투자를 하게 되었는데 그 중 하나가 1958년 설립된 DARPA(Defence Advanced Research Project Agency) 였다.
  • DARPA는 막대한 예산을 지원 받으며 미군을 위한 연구를 수행했는데 DARPA의 본부는 알링톤에 있었지만 DARPA의 프로젝트 상당수가 대학과 외부의 연구소에서 진행되어 DARPA가 외부 자료에 접근할 수 있는 방법은 ‘택배’ 밖에 없었다.
  • DARPA는 이 문제를 위해 새로운 구상을 했는데, 그것은 네트워크와 네트워크를 연결하는 시스템이었다. 이른바 DARPANET 이라고 불리던 이 네트워크의 네트워크는 더 많은 대학과 연구 기관으로 또한 세계의 연구 기관과 민간으로 연결되기 시작하더니 1980년 말에 이르러 인터넷이라고 하는 국제 통신망을 형성하게 되었다.

TCP/IP 스택

  • 컴퓨터끼리 데이터를 주고 받기 위해서는 그 네트워크에서 통용되는 ‘프로토콜(Protocol)’이라는 통신 규약을 따라야 한다.
  • 인터넷은 전세계에서 가장 거대한 네트워크긴 하지만 유일한 네트워크는 아니다. 또한 프로토콜도 굉장히 다양한 종류가 많다. 다행히 인터넷이 사실상 전세계 컴퓨터 네트워크의 표준이라고 할 수 있을 정도로 자리를 잡았고 인터넷의 통신 프로토콜로 사용되는 TCP/IP도 실질적인 인터넷 표준 프로토콜로 자리 잡았다.
  • TCP/IP는 표준 프로토콜로써 인터넷에서 데이터를 주고 받는데 필요한 일련의 프로토콜 모음(Suite)이다. TCP/IP는 아래 그림과 같이 4개의 계층으로 구성되어 있으며, 한 계층 위에 다른 계층이 포개져 있는 형태 때문에 TCP/IP 스택이라고 부르기도 한다.

링크 계층

  • 링크 계층은 물리 계층(Physical Layer), 네트워크 접속 계층(Network Interface Layer), 미디어 접근 계층(Media Access Layer) 등으로 불리기도 한다.
  • TCP/IP는 네트워크의 물리적인 구성으로부터 독립적인 프로토콜로 컴퓨터가 네트워크에 전화선에 모뎀으로 연결되어 있든, LAN에 이더넷 케이블로 연결되어 있든, Wi-Fi에 연결되어 있든 전혀 신경 쓰지 않는다. 이는 링크 계층에서 네트워크의 물리적인 연결 매체를 통해 패킷을 주고 받는 작업을 담당해 주기 때문에 가능한 일이다.
    • 네트워크를 통해 전송되는 데이터를 패킷이라고 부르는 까닭은 데이터가 소포처럼 포장지를 필요로 하기 때문이다. 이 포장지는 데이터를 싸서 보호하고 데이터가 어디에서 어디로 가는지가 기록 된다.
    • 네트워크 패킷은 여러 겹의 포장지로 포장되는데 어플레이케이션 계층, 전송 계층, 인터넷 계층, 링크 계층이 모두 패킷의 포장지가 된다. 데이터를 보낼 때는 어플레이케이션 계층부터 시작해서 링크 계층까지 포장을 하고, 데이터를 받을 때는 링크 계층부터 어플리케이션 계층까지 포장을 뜯어 내용물을 꺼낸다.
  • 어떤 패킷이 네트워크를 통해 컴퓨터에 들어오면 제일 먼저 링크 계층이 맞이한다. 이 링크 계층은 이 패킷에서 물리적 데이터 전송에 사용되던 부분을 제거하고 인터넷 계층에 넘긴다. 이렇게 함으로써 인터넷 계층에서는 패킷이 전파를 타고 넘어 왔든 광케이블을 타고 넘어왔든 간에 상관 없이 자신의 일을 처리할 수 있게 된다.

인터넷 계층

  • 인터넷 계층은 패킷을 수신해야 할 상대의 주소를 지정하고, 나가는 패킷에 대해서는 적절한 크기로 분할하며 들어오는 패킷에 대해서는 재조립을 수행한다. 이 계층에서 사용되는 규약이 바로 인터넷 프로토콜(Internet Protocol, IP)이다. TCP/IP에서의 IP가 바로 이것.
  • IP는 우체국 일반우편과 비슷한 특징이 있는데, 내보낸 패킷을 상대방이 잘 수령했는지에 대해 전혀 보장하지 않기 때문. 배달 중에 문제가 생겨서 패킷이 손상되거나 분실된다 해도 전혀 책임을 지지 않는다.
  • 일단 IP는 상대방이 패킷을 잘 수령했는지의 여부를 파악하는 기능 자체가 없다. 그저 전송 계층에서 내려온 패킷에 주소를 붙여 링크 계층으로 보내기만 할 뿐. 따라서 여러 개의 패킷을 전송했을 때 순서대로 도착하지 않아도 이상할 것이 전혀 없다.
    • 여기서 사용하는 주소 체계가 바로 IP 주소이다.

전송 계층

  • 전송 계층에는 이름 그대로 패킷의 운송을 담당하는 프로토콜들이 정의되어 있다. 이 중에서도 전송 제어 프로토콜(Transmission Control Protocol, TCP)은 송신측과 수신측 간의 연결성을 제공하며, 신뢰할 수 있는 패킷 전송 서비스를 제공한다.
  • 여러 개의 패킷을 송신하는 경우 패킷 사이의 순서를 보장하며, 패킷이 유실되기라도 하면 재전송 해주기도 한다. TCP/IP 프로토콜에서 TCP가 바로 이 프로토콜을 가리키는 것이며, TCP는 IP가 제공하지 않는 연결성, 신뢰성을 제공한다.
    • 웹 문서를 전달하는 기능을 하는 HTTP를 비롯한 수많은 응용 프로토콜들이 바로 이 TCP와 IP 프로토콜 위에서 동장한다.
  • 한편 TCP는 IP가 제공하지 않는 연결성과 신뢰성을 제공하느라 성능에서 손실을 본다. 데이터가 큰 경우에는 여러 개의 패킷에 나눠 담아 순서대로 보내야 하지만, 데이터가 충분히 작은 경우에는 하나의 패킷에 담아 보내도 된다. 이런 상황에서는 TCP가 제공하는 패킷의 순서 보장성이 필요가 없다. 또한 받아도 그만, 안 받아도 그만인 패킷의 경우에는 굳이 재전송을 할 필요가 없다. 다시 말해 충분히 작고 전송 신뢰성을 요구하지 않는 데이터는 TCP의 장점이 단점이 되는데, 이를 위한 대안으로 전송 계층에는 UDP(User Datagram Protocol)이라는 프로토콜이 정의되어 있다.
    • 이 프로토콜은 연결성과 신뢰성을 제공하지 않지만 성능이 TCP에 비해 우수하기 때문에 전송 제어를 직접 처리하는 응용 프로그램 수준에서 채용되는 경우가 많다.

어플리케이션 계층

  • 이 계층은 각 응용 프로그램 나름의 프로토콜들이 정의되는 곳이다. 웹 문서를 주고 받기 위한 HTTP(Hyper Text Transfer Protocol), 파일 교환을 위한 FTP(File Transfer Protocol), 네트워크 관리를 위한 SNMP(Simple Network Management Protocol) 등이 어플리케이션 계층에서 정의된 프로토콜의 대표적인 예이다.
  • 어플리케이션 계층의 프로토콜들은 전송 계층의 프로토콜 중 TCP에 기반할 수도 있고, UDP에 기반할 수도 있다. HTTP와 FTP는 상대적으로 큰 데이터를 처리해야 하기 때문에 TCP에 기반하고 SNMP는 단순한 정보만 다루는데다 패킷을 일부 유실해도 임무에 지장을 주지 않기 때문에 비용이 저렴한 UDP에 의존한다.
  • 한편 HTTP나 FTP처럼 표준화된 프로토콜이 아니더라도 이 계층에서는 우리 나름대로의 프로토콜을 정의해서 사용할 수 있습니다. 예컨대 인스턴트 메신저를 개발하는데 필요한 프로토콜을 정의하면 그 프로토콜은 이 어플리케이션 계층에 속한다.

TCP/IP의 주소 체계: IP 주소

  • 우편물을 전달하기 위해서는 ‘주소’가 필요하듯이 인터넷에서도 패킷을 전달하려면 이것을 어디에서 보냈으며 어디로 보낼지에 대한 정보가 필요하다. 그리고 인터넷에서 사용하는 이 주소를 IP 주소(Address)라고 부른다.

포트

  • 큰 빌딩은 대개 출입구가 여러개 있게 마련인데, 컴퓨터도 네트워크 패킷이 드나들려면 출입문이 있어야 한다. 이 출입문을 가리켜 ‘포트(Port)’라고 부른다.
  • 포트는 부호가 없는 16비트 정수로써 0~65535 사이의 값을 이용한다. 예컨대 HTTP는 80번 포트를 사용하고 FTP는 21번 Telnet은 23번을 사용한다.
    • HTTP나 FTP, Telnet과 같은 표준 프로토콜이 사용하고 있는 포트 번호는 전세계적으로 합의된 값이다. 이러한 포트 번호를 일컬어 ‘잘 알려진 포트 번호(Well Known Port Number)’라고 부른다. 잘 알려진 포트 번호는 1-1023 사이의 수를 사용하므로 새로운 응용 프로그램 프로토콜을 정의할 때는 이 범위를 피해서 정하는 것이 좋다.
      • HTTP: 80
      • HTTPS: 443
      • FTP: 21
      • Telnet: 23
      • SMTP: 25
      • IRC: 194
      • IIOP: 535
  • 건물의 보안이 출입구를 얼마나 잘 단속하느냐에 달려 있듯 네트워크의 보안도 이런 포트 단속이 큰 비중을 차지 한다. 꼭 필요한 포트만 열어 놓는 것이 최선이다.

TCP/IP의 동작 과정

  • TCP/IP는 서버/ 클라이언트 방식으로 동작한다. 통신을 수행하는 양단 중 한 쪽에서는 다른 한 쪽에게 서비스를 제공한다는 뜻.
  • TCP/IP 통신을 위해서는 먼저 서버가 이 서비스를 시작해야 한다. 클라이언트가 접속해 올 수 있도록 준비를 하는 것. 서버가 준비되고 나면 클라이언트는 서버에 접속을 시도하고 서버가 이 접속을 수락하면 서버와 클라이언트는 동등한 입장에서 데이터를 주고 받을 수 있게 된다.

TcpListener와 TcpClient

  • TcpListener 클래스는 서버 응용 프로그램에서 사용되며, 클라이언트의 연결 요청을 기다리는 역할을 하며, TcpClient는 서버 응용 프로그램과 클라이언트 응용 프로그램 양쪽에서 사용된다. 클라이언트에서는 TcpClient가 서버에 연결 요청을 하는 역할을 수행하며, 서버에서는 클라이언트의 요청을 수락하면 클라이언트와의 통신에 사용할 수 있는 TcpClient의 인스턴스가 반환된다.
  • 서버와 클라리언트 각각이 갖고 있는 TcpClient는 GetStream()이라는 메소드를 갖고 있어서 양쪽의 응용 프로그램은 이 메소드가 반환하는 NetworkStream 객체를 통해 데이터를 주고 받는다. 데이터를 보낼 떄는 NetworkStream.Write()를, 데이터를 읽을 때는 NetworkStream.Read()를 호출한다. 데이터를 주고 받는 일을 마치고 서버와 클라이언트의 연결을 종료할 때는 NetworkStream 객체와 TcpClient 객체 모두의 Close() 메소드를 호출한다.

클래스 메소드 설명
TcpListener Start() 연결 요청 수신 대기를 시작한다.
TcpListener AcceptTcpClient() 클라이언트의 연결 요청을 수락한다. 이 메소드는 TcpClient 객체를 반환한다.
TcpListener Stop() 연결 요청 수신 대기를 종료한다.
TcpClient Connect() 서버에 연결을 요청한다.
TcpClient GetStream() 데이터를 주고 받는데 사용하는 매개체인 NetworkStream을 가져온다.
TcpClient Close() 연결을 닫는다.
/* 서버의 TcpListener 코드 예시 */

// IPEndPoint는 IP 통신에 필요한 IP 주소와 출입구(포트)를 나타낸다.
IPEndPoint localAddress = new IPEndPoint(IPAddress.Parse("192.168.100.17"), 5425);

TcpListener server = new TcpListener(localAddress);

// server 객체는 클라이언트가 TcpClient.Connect()를 호출하여 연결을 요청해 오기를 기다리기 시작한다.
server.Start();


/* 클라이언트의 TcpListener 코드 예시 */

// 포트를 0으로 지정하면 OS에서 임의의 번호로 포트를 할당해 준다.
IPEndPoint clientAddress = new IPEndPoint(IPAddress.Parse("192.168.100.18"), 0);

TcpClient client = new TcpClient(clientAddress);

IPEndPoint serverAddress = new IPEndPoint(IPAddress.Parse("192.168.100.17"), 5425);

// 서버가 수신 댁하고 있는 IP 주소와 포트 번호를 향해 연결 요청을 수행한다.
client.Connect(serverAddress);


/* 서버 코드 예시. 서버에서 AcceptTcpClient()를 호출하면 코드는 블록되어 그 자리에서 이 메소드가 반환할 때까지 진행하지 않는다. 기다리던 연결 요청이 오면 이 메소드는 클라이언트와 통신을 수행할 수 있도록 TcpClient 형식의 객체를 반환한다. */

TcpClient client = server.AcceptTcpClient();


/* TcpClient 객체가 NetworkStream 객체를 반환하고 NetworkStream 객체를 이용하여 데이터를 읽고 쓰는 예제 */

// TcpClient를 통해 NetworkStream 객체를 얻는다.
NetworkStream stream = client.GetStream();

int length;
string data = null
byte[] bytes = new byte[256];

// NetworkStream.Read() 메소드는 상대바잉 보내 온 데이터를 읽어 들인다.
// 한편, 상대와의 연결이 끊어지면 이 메소드는 0을 반환한다. 즉, 이 루프는 연결이 끊어지기 전까지는 계속된다.
while ( (length = stream.Read(bytes, 0, bytes.Length)) != 0 )
{
    data = Encoding.Default.GetString(bytes, 0, length);
    Console.WriteLine(String.Format("수신: {0}", data));

    byte[] msg = Encoding.Default.GetByte(data);

    stream.Write(msg, 0, msg.Length);
    Console.WriteLine(String.Format("송신: {0}", data));
}
  • 참고) 127.0.0.1은 컴퓨터의 네트워크 입출력 기능을 시험하기 위해 가상으로 할당한 주소이다. 네트워크 출력에 데이터를 기록하면 실제로 패킷이 링크 계층을 거쳐 바깥으로 나가야 하지만 127.0.0.1을 향해 데이터를 기록하면 링크 계층을 거치지 않고 다시 자기 자신에게로 패킷을 보내게 된다. 이렇게 되돌아 오는 입출력 기능 때문에 루프백(Loopback) 주소라고 부르기도 한다.

흐르는 패킷

  • TCP를 처음 접하면서 가장 흔히 하게 되는 오해 중의 하나는 ‘송신측에서 Write()를 할 때마다 하나의 메시지가 만들어지며 이 메시지를 수신측에서 Read()를 통해 하나씩 읽어온다’는 것.
  • TCP는 연결 지향, 흐름 지향 프로토콜로 TCP 프로토콜의 데이터 전달 과정은 편지보다는 오히려 전기가 전달되는 모습과 더 닮았다고 할 수 있다. 전기는 전선으로 ‘연결’ 되어 있는 상태에서 전기를 갖고 있는 쪽이 전기를 받아야 하는 쪽으로 전기를 ‘흘려’ 보낸다. TCP 프로토콜도 전기처럼 양쪽이 연결되어 있어야 하고 보내는 쪽에서 받는 쪽으로 패킷을 흘려 보낸다. 둘이 다른 점이 있다면 TCP 프로토콜은 흐름 속에서 각 개별 패킷의 경계를 구분한다는 점. 시작이 어디고 끝이 어딘지를 파악해야 한다는 것이다.
  • TCP 통신 응용 프로그램도 댐과 같은 역할을 하는 버퍼(Buffer)를 갖고 있다. 응용 프로그램에서 네트워크를 향해 내보내는 데이터나 들어오는 데이터나 모두 이 버퍼를 거친다.

  • 예컨대 두 응용 프로그램이 TCP 연결을 맺고 있고, 송신 응용 프로그램이 메모리에 들고 있는 ‘a’, ‘b’, ‘c’를 수신 응용 프로그램에 보내려 한다고 가정한다. –a, b, c는 wBuffer라는 이름의 바이트 배열에 담겨 있다.

  • 송신측 응용 프로그램에서 writer.Write(wBuffer, 0, 3)을 호출하면 데이터는 아래 그림과 같이 응용 프로그램의 메모리에서 송신 버퍼로 이동한다.

  • 운영체제는 송신 버퍼에 있는 내용을 연결을 맺고 있는 수신측으로 보내기 싲가한다. 이 때 네트워크 대역폭이 넓고 품질도 좋다면 많은 데이터가 빠른 속도로 수신측으로 이동할 것이고 그렇지 않다면 다음과 같이 아주 조금씩 데이터가 이동할 수도 있다.

  • 한편 수신측 응용 프로그램에서는 데이터를 담기 위핸 rBuffer를 선언하고 reader.Read(rBuffer, 0, 16)을 호출한다. 이 코드는 16바이트를 읽어오려고 시도하지만 실제 수신 버퍼에는 a 하나 밖에 없으므로 rBuffer에는 ‘a’가 담기고 Read() 메소드는 실제로 읽은 바이트 수 1을 반환한다. 한편, 그러는 동안 수신 버퍼에는 송신측에서 보낸 b, c가 도착했다.

  • 이번에도 수신측은 reader.Read(rBuffer, 0, 16)을 호출했는데, 이번에는 b, c가 rBuffer에 담기고 Read() 메소드는 읽은 바이트 수 2를 반환한다. 이렇게 해서 송신측의 프로그램 메모리에 있던 a, b, c가 모두 수신측의 메모리로 전달된다.

  • (이하 프로토콜 설계하는 내용 생략)

뇌를 자극하는 C# 4.0 프로그래밍/ 스레드와 태스크

프로세스와 스레드

  • 프로세스(Process)란 실행 파일이 실행되어 메모리에 적재된 인스턴스를 말한다. 예컨대 word.exe가 실행 파일이라면, 이 실행 파일을 실행한 것이 프로세스가 된다.
  • 프로세스는 반드시 1개 이상의 스레드(Thread)로 구성된다. 스레드는 운영체제가 CPU 시간을 할당하는 기본 단위인데, 프로세스가 밧줄이라면 스레드는 밧줄을 이루는 실이라고 할 수 있다. 위 그림은 프로세스와 프로세스를 구성하는 스레드의 관계를 표현하고 있다.

  • 멀티 스레드의 장점
    1. 사용자 대화형 프로그램에서 응답성을 높일 수 있다. 예컨대 프로그램 파일 복사를 하는데 파일 용량이 커서 소요 시간이 30분 정도 걸리는 경우, 단일 스레드에서는 취소 조차 할 수 없어서 –운영체제 단에서 강제 종료를 하지 않는 이상– 파일 복사가 완료될 때까지 기다리고 있어야 한다.
    2. 멀티 프로세스 방식에 비해 멀티 스레드 방식이 자원 공유가 쉽다. 멀티 스레드 방식에서는 스레드끼리 코드 내 변수를 같이 사용하는 것만으로도 데이터 교환을 할 수 있다.
    3. 멀티 프로세스 방식에 비해 경제성이 높다. 프로세스를 띄우기 위해 메모리와 자원을 할당하는 작업은 비용이 비싼데, 스레드를 띄울 때는 이미 프로세스에 할당된 메모리와 자원을 그대로 사용하므로 메모리와 자원을 할당하는 비용을 지불하지 않아도 된다.

  • 멀티 스레드 방식의 단점
    1. 구현이 어렵다.
    2. 테스트도 어렵다.
    3. 멀티 프로세스 방식에서는 자식 프로세스 중 하나에 문제가 생겨도 그 자식 프로세스 하나만 죽는 것 이상으로 영향이 확산되지 않지만, 멀티 스레드에서는 자식 스레드 중 하나만 문제가 생겨도 전체 프로세스에 영향이 미친다.
    4. 스레드를 너무 많이 사용하면 오히려 성능이 저하된다. 스레드가 CPU를 사용하기 위해서는 작업 간 전환(Context Switching)을 해야 하는데, 이 작업 간 전환 비용이 꽤 비싸다. 많은 스레드가 너무 자주 작업 간 전환을 수행하다 보면 응용 프로그램이 실제로 일을 하는 시간에 비해 작업 간 전환에 사용하는 시간이 커지기 때문에 성능이 저하된다.

스레드 시작하기

// 스레드 사용 예시
static void DoSomething ()
{
    for ( int i = 0 ; i < 5 ; i++ )
    {
        Console.WriteLine("DoSomething: {0}", i);
    }
}

static void Main(string[] args)
{
    Thread t1 = new Thread(new ThreadStart(DoSomething)); // Thread 인스턴스 생성

    t1.Start(); // 스레드를 시작한다.

    t1.Join(); // 스레드의 종료를 대기한다.
}
  • .NET 프레임워크에서는 System.Threading.Thread 클래스를 통해 스레드를 사용한다. 그 사용 방법은 다음과 같다.
    1. Thread의 인스턴스를 생성한다. 이때 생성자의 매개변수로 스레드가 실행할 메소드를 넘긴다.
    2. Thread.Start() 메소드를 호출하여 스레드를 시작한다.
    3. Thread.Join() 메소드를 호출하여 스레드가 끝날 때까지 기다린다.

스레드 임의로 종료 시키기

// 스레드 종료 예시
static void DoSomething ()
{
    try
    {
        for ( int i = 0 ; i < 5 ; i++ )
        {
            Console.WriteLine("DoSomething: {0}", i);
        }
    }
    catch ( ThreadAbortedException e )
    {
        // ...
    }
    finally
    {
        // ...
    }
}

static void Main(string[] args)
{
    Thread t1 = new Thread(new ThreadStart(DoSomething)); // Thread 인스턴스 생성

    t1.Start();

    t1.Abort(); // 스레드 취소

    t1.Join();
}
  • 살아 있는 스레드를 죽이려면 Thread 객체의 Abort() 메소드를 호출하면 된다.
  • 여기서 주의할 점은 Abort() 메소드를 호출한다고 동작하던 스레드가 즉시 종료된다는 보장이 없다는 것이다. Thread 객체의 Abort() 메소드를 호출하면 CLR은 해당 스레드가 실행 중이던 코드에 ThreadAbortException을 호출한다. 이 때 이 예외를 catch 하는 코드가 있으면 이 예외를 처리한 다음, finally 블록까지 실행한 후에야 해당 스레드는 완전히 종료가 된다.
  • (Thread.Abort()는 사용하지 않는 편이 좋다. 마치 goto와 같은 식)

스레드의 일생과 상태 변화

스레드 상태 설명
Unstarted 스레드 객체를 생성한 후 Thread.Start() 메소드가 호출되기 전의 상태
Running 스레드가 싲가하여 동작 중인 상태. Unstarted 상태의 스레드를 Thread.Start() 메소드를 통해 이 상태로 만들 수 있다.
Suspende 스레드의 일시 중단 상태. 스레드를 Thread.Suspend() 메소드를 통해 이 상태로 만들 수 있으며, Suspended 상태인 스레드는 Thread.Resume() 메소드를 통해 다시 Running 상태로 만들 수 있다.
WaitSleepJoin 스레드가 블록(block)된 상태
Aborted 스레드가 취소된 상태. Aborted 상태가 된 스레드는 다시 Stopped 상태로 전환되어 완전히 중지된다.
Stopped 중지된 스레드의 상태. Abort() 메소드를 호출하거나 스레드가 실행 중인 메소드가 종료되면 이 상태가 된다.
Background 스레드가 백그라운드로 동작하고 있는 상태. foreground 스레드는 하나라도 살아 있는 한 프로세스가 죽지 않지만, 백그라운드는 프로세스가 죽고 사는 것에 영향을 미치지 않는다. 다만 프로세스가 죽으면 백그라운드 스레드들도 모두 죽는다. Thread.IsBackground 속성에 true 값을 입력함으로써 이 상태를 바꿀 수 있다.
  • 스레드의 상태 변화에는 규칙이 있는데, 예컨대 Aborted 상태의 스레드는 절대 Running 상태로 옮겨가지 못하고, Running 상태의 스레드는 Unstarted 상태로 바뀔 수 없다. 자세한 내용은 아래 그림 참조.

  • ThreadState 열거형의 특징은 ThreadState가 Flags 애트리뷰트를 갖고 있다는 점이다. ThreadStage는 2가지 이상의 상태를 동시에 가질 수 있는 Flags를 통해 자신의 상태를 표현한다.
  • ThreadState의 열거형은 2n의 형태로 값을 갖는데 이유는 비트 연산을 통해 여러 상태를 동시에 가질 수 있는 ThreadState가 어떤 상태인지 쉽게 알 수 있게 하기 위함이다.
상태 10진수 2진수
Running 0 000000000
StopRequested 1 000000001
SuspendRequested 2 000000010
Background 4 000000100
Unstarted 8 000001000
Stopped 16 000010000
WaitSleepJoin 32 000100000
Suspended 64 001000000
AbortedRequest 128 010000000
Aborted 256 100000000
// Thread 객체의 ThreadState 필드의 값을 확인하는 예제
if ( t1.ThreadState & ThreadState.Aborted == ThreadState.Aborted )
{
    // ...
}
else if ( t1.ThreadState & ThreadState.Stopped == ThreadState.Stopped )
{
    // ...
}

스레드를 임의로 멈추는 또 하나의 방법: 인터럽트

// 스레드 중단 예시
static void DoSomething ()
{
    try
    {
        for ( int i = 0 ; i < 5 ; i++ )
        {
            Console.WriteLine("DoSomething: {0}", i);
        }
    }
    catch ( ThreadInterruptedException e )
    {
        // ...
    }
    finally
    {
        // ...
    }
}

static void Main(string[] args)
{
    Thread t1 = new Thread(new ThreadStart(DoSomething)); // Thread 인스턴스 생성

    t1.Start();

    t1.Interrupt(); // 스레드 중단

    t1.Join();
}
  • 불가피하게 스레드를 중단해야 한다면 Thread.Interrupt() 메소드를 이용하여 중단 시키는 것이 좋다. Thread.Interrupt() 메소드는 스레드가 한참 동작 중인 상태 –Running 상태– 를 피해서 WaitJoinSleep 상태에 들어갔을 때 ThreadInterruptedException 예외를 던져 스레드를 중지시킨다.
    • 이런 특징 때문에 프로그래머는 최소한 코드가 ‘절대로 중단 되면 안 되는’ 작업을 하고 있을 때는 중단 되지 않는다는 보장을 받을 수 있다.

스레드 간의 동기화

  • 스레드는 이기적이라 남의 상황 따위는 안중에 두지 않는다. 다른 스레드가 어떤 자원을 잡고 사용하고 있는 중인데 갑자기 끼어들어 자기 멋대로 사용해 버리는 경우가 다반사이다.
  • 스레드들이 순서를 갖춰 자원을 사용하게 하는 것을 일컬어 ‘동기화(Synchronization)’이라고 하는데, 이를 제대로 하는 것이 멀티 스레드 프로그래밍을 완벽하게 하는 길이라고 할 수 있다.
  • 스레드 동기화에서 가장 중요한 사명은 ‘자원을 한 번에 하나의 스레드가 사용하도록 보장’하는 것이다. 프로그래머가 이 사명을 달성할 수 있도록 .NET 프레임워크가 제공하는 도구로는 lock 키워드와 Monitor 클래스가 있다.
    • lock 키워드가 사용하기는 더 쉬운데 Monitor 클래스는 보다 섬세한 동기화 제어 기능을 제공한다.

lock 키워드로 동기화하기

// 스레드 동기화 예시 코드
class Counter
{
    public int count = 0;
    public void Increase()
    {
        count = count + 1;
    }
}

MyClass obj = new MyClass();
Thread t1 = new Thread(new ThreadStart(obj.DoSomething);
Thread t2 = new Thread(new ThreadStart(obj.DoSomething);
Thread t3 = new Thread(new ThreadStart(obj.DoSomething);

t1.Start();
t2.Start();
t3.Start();

t1.Join();
t2.Join();
t3.Join();

Console.WriteLine(obj.count);
  • 위 예시 코드에서 obj.count는 최소 1에서 최대 3까지의 결과를 가질 수 있는데, 이유는 아래 그림과 같다.
  • count + 1은 여러 단계의 하위 연산으로 나눠지는 코드로 CPU 입장에서는 count = count + 1이 꽤 긴 코드가 된다. 그리하여 프로그래머 입장에서는 위쪽 그림처럼 코드가 동작하길 바라지만, 실제로는 t1스레드가 연산을 마치기 전체 t2 스레드가 같은 코드를 실행하고, t3 스레드가 다시 같은 코드를 실행하여 최종적으로 1이 나올 수가 있다. 더 문제는 실행할 때마다 그 결과가 다를 수 있다는 것.
  • 이 문제를 해결학 위해 count = count + 1 코드를 한 스레드가 실행하고 있을 때 다른 스레드는 실행하지 못하도록 하는 장치가 필요하다. 그 장치가 바로 크리티컬 섹션
    • 크리티컬 섹션(Critical Section)이란 한 번에 한 스레드만 사용할 수 있는 코드 영역을 의미한다. C#에서는 lock 키워드로 감싸 주기만 해도 평범한 코드를 크리티컬 섹션으로 바꿀 수 있다.

// lock 키워드 사용 예시
public viod Increase ()
{
    // lock 키워드와 중괄호로 둘러싸인 이 부분은 크리티컬 섹션이 된다.
    // 한 스레드가 이 코드를 실행하다 lock 블록이 끝나는 괄호를 만나기 전까지는 다른 스레드는 절대 이 코드를 실행할 수 없다.
    lock ( thisLock )
    {
        count = count + 1;
    }
}
  • 스레드는 lock 을 얻어야 크리티컬 섹션을 생성하므로 누군가가 lock을 받아 크리티컬 섹션을 생성하였다면 다른 스레드들은 대기를 하는 상황이 발생한다. 이런 경우 소프트웨어의 성능이 크게 떨어지므로 동기화를 설계할 때는 반드시 필요한 곳에서만 크리티컬 섹션을 사용하도록 해야 한다.
  • 한편 lock 키워드의 매개 변수로 사용하는 객체는 참조형이면 어느 것이든 쓸 수 이지만 public 키워드 등을 통해 외부 코드에서도 접근할 수 있는 다음 3가지는 문법적으로는 문제가 없지만 절대 사용하지 않기를 권장된다.
    1. this: 클래스의 인스턴스는 클래스 내부 뿐만 아니라 외부에서도 자주 사용된다. lock(this)는 매우 나쁜 버릇이다.
    2. Type 형식: typeof 연산자나 object 클래스로부터 물려받은 GetType() 메소드는 Type 형식의 인스턴스를 반환한다. 즉 코드의 어느 곳에서나 특정 형식에 대한 Type 객체를 얻을 수 있다. lock( typeof(SomeClass) )나 lock( obj.GetType() )은 피할 것.
    3. string 형식: 절대 string 객체로 lock을 하지 말 것. “abc”는 어떤 코드에서든 읽을 수 있는 string 객체이다.

Monitor 클래스로 동기화하기

// Monitor.Enter()와 Monitor.Exit()를 이용한 크리티컬 섹션 생성과 삭제
public void Increase ()
{
    int loopCount = 1000;
    while (loopCount-- > 0)
    {
        Monitor.Enter(thisLock);

        try
        {
            count++;
        }
        finally
        {
            Monitor.Exit(thisLock);
        }
    }
}
  • Monitor.Enter()는 크리티컬 섹션을 생성하며 Monitor.Exit()은 크리티컬 섹션을 제거한다.
  • lock 키워드는 Monitor.Enter(), Monitor.Exit()를 바탕으로 구현되어 있기 때문에 이 둘은 완전히 동일한 기능을 갖는다. lock이 사용하기 더 간편하므로 lock을 사용하는 편이 낫다.

Monitor.Wait()와 Monitor.Pulse()로 하는 저수준 동기화

  • Monitor.Wait()와 Monitor.Pulse()는 lock 키워드보다 섬세한 멀티 스레드 동기화를 제공한다.
  • 이 두 메소드는 반드시 lock 블록 안에서 호출해야 하는데 만일 lock을 걸어 놓지 않은 상태에서 이 두 메소드를 호출하면 CLR이 Synchronization LockException 예외를 던지는 광경을 봐야 한다.
  • Wiat() 메소드는 스레드를 WaitSleepJoin 상태로 만든다. 이렇게 WaitSleepJoin 상태에 들어간 스레드는 동기화를 위해 갖고 있던 lock을 내려 놓은 뒤 Waiting Queue라고 하는 큐에 입력되고 다른 스레드가 lock을 얻어 작업을 수행한다.
  • 작업을 수행하던 스레드가 일을 마친 뒤 Pulse() 메소드를 호출하면 CLR은 Waiting Queue의 가장 첫 요소 스레드를 꺼낸 뒤 Ready Queue에 입력 시킨다. Ready Queue에 입력된 스레드는 입력된 차례에 따라 lock을 얻어 Running 상태에 들어간다.
    • 아래 그림은 이 과정을 설명한다.
  • 한편 Thread.Sleep() 메소드도 스레드를 WaitSleepJoin 상태로 만들기는 하지만 Monitor.Pulse() 메소드에 의해 깨어날 수는 없다. 다시 Running 상태로 돌아오려면 매개 변수로 입력된 시간이 경과되거나 인터럽트 예외를 받아야 깨어난다. 반면 Monitor.Wait() 메소드는 Monitor.Pulse() 메소드가 호출되면 바로 깨어날 수 있다. 이 때문에 멀티 스레드 응용 프로그램의 성능 향상을 위해 Monitor.Wait()와 Monitor.Pulse()를 사용한다.
  • (예시 코드 생략)

Task와 Task<TResult>, 그리고 Parallel

  • CPU의 발전이 클럭을 높이는 방향으로는 한계에 이르자, 인텔을 비롯한 CPU 업체들은 하나의 CPU 안에 여러 개의 코어를 집적하는 방향으로 제품을 향상시켰고 MS는 개발자들이 이러한 하드웨어 변화를 최대로 활용할 수 있는 방법을 연구하는데 많은 투자를 하였다.
  • .NET 프레임워크 4.0에 도입된 System.Threading.Tasks 네임 스페이스도 그 연구 성과의 일부로 이 네임스페이스는 병행성 코드나 비동기 코드를 개발자들이 손쉽게 작성할 수 있도록 돕는 클래스들이 들어 있다.
  • System.Threading.Task 네임스페이스의 클래스들은 하나의 작업을 쪼갠 뒤 쪼개진 작업들을 동시에 처리하는 코드와 비동기 코드를 위해 설계되었다.반면 Thread 클래스는 여러 개의 작업을 나누지 않고 각각 처리해야 하는 코드에 적합하다.

System.Threading.Tasks.Task 클래스

  • 동기(Synchronous) 코드는 검사에 비유할 수 있는데 상대를 찌른 후에 다시 뽑아야 다시 칼을 쓸 수 있는 것처럼 메소드를 호출한 뒤에 이 메소드의 실행이 완전히 종료되어야만 다음 메소드를 호출할 수 있다.
  • 반면 비동기 코드는 궁수에 비유할 수 있는데 궁수가 화살을 쏘고 바로 다음 화살을 쏠 수 있는 것 –Shoot and Forget– 처럼 메소드를 호출한 뒤에 메소드의 종료를 기다리지 않고 바로 다음 코드를 실행한다.
  • C#에는 어떤 메소드를 동기/ 비동기로 실행하도록 하는 키워드 같은 것은 없다. 대신 Task와 Task<TResult> 클래스를 제공해서 비동기 코드를 수행할 수 있도록 도울 뿐이다.
  • Task 클래스는 인스턴스를 생성할 때 Action 델리게이트를 넘겨 받는다. 다시 말해 반환형을 갖지 않는 메소드와 익명 메소드, 무명 함수 등을 넘겨 받는다는 것.

Task 사용 예시

// task의 인스턴스는 Start() 메소드를 통해 Action 델리게이트를 비동기로 실행한다.
Action> someAction  = () = >
{
    Thread.Sleep(1000);
    Console.WriteLine("Printed Asynchronously");
}

Task myTask = new Task(someAction);
myTask.Start();

Console.WriteLine("Printed Synchronously");

myTask.Wait();


// 보다 일반적인 Task 사용법. Task.Factory를 이용한다.
var myTask = Task.Factory.StartNew(
    () =>
    {
        Thread.Sleep(1000);
        Console.WriteLine("Printed Asynchronously");
    }
);

Console.WriteLine("Printed Synchronously");

myTask.Wait();

코드의 비동기 실행 결과를 주는 Task<TResult> 클래스

var myTask = Task<List<int>>.Factory.StartNew(
    () =>
    {
        Thread.Sleep(1000);

        List<int> list = new List<int>();
        list.Add(3);
        list.Add(4);
        list.Add(5);

        return list;
    }
);

var myList = new List<int>();
myList.Add(0);
myList.Add(1);
myList.Add(2);

myTask.Wait();
myList.AddRange(myTask.Result.ToArray());
  • Task<TResult>는 코드의 비동기 실행 결과를 손쉽게 취합할 수 있도록 도와준다.
  • Task가 비동기로 수행할 코드를 Action 델리게이트로 받는 대신 Func 델리게이트로 받아서 결과를 반환한다는 점이 다르다.

손쉬운 병렬 처리를 가능케 하는 Parallel 클래스

void SomeMethod(int i)
{
    Console.WriteLine(i);
}

// Parallel.For()는 SomeMethod를 병렬로 호출하면서 0부터 100사이의 정수를 메소드의 매개 변수로 넘긴다.
// SomeMethod를 병렬로 호출할 때 몇 개의 스레드를 사용할지는 Parallel 클래스가 내부적으로 판단하여 최적화 한다.
Parallel.For(0, 100, SomeMethod);
  • MS는 System.Threading.Tasks.Parallel 클래스를 제공하여 For(), Foreach() 등의 메소드를 이용하여 Task<TResult>를 이용해서 직접 구현했던 병렬처리를 더 쉽게 구현할 수 있게 해준다.

뇌를 자극하는 C# 4.0 프로그래밍/ 파일 다루기

파일 정보와 디렉토리 정보 다루기

  • 파일(File)이란 컴퓨터 저장 매체에 기록되는 데이터의 묶음.
  • 디렉토리(Directory)는 파일이 위치하는 주소로서 파일을 폴더(Folder)라고도 한다.
  • 아래는 System.IO 네임스페이스에 있는 파일과 디렉토리를 다룰 수 있는 클래스들.
클래스 설명
File 파일의 생성, 복사, 삭제, 이동, 조회를 처리하는 정적 메소드
FileInfo File 클래스와 동일하지만 인스턴스 메소드를 제공한다는 차이가 있다.
Directory 디렉토리의 생성, 삭제, 이동, 조회를 처리하는 정적 메소드
DirectoryInfo Directory 클래스와 동일하지만 인스턴스 메소드를 제공한다는 차이가 있다.

 

  • File, FileInfo, Directory, DirectoryInfo 클래스에서 제공하는 주요 메소드와 프로퍼티
기능 File FileInfo Directory DirectoryInfo
생성 Create() Create() CreateDirectory() Create()
복사 Copy() CopyTo()
삭제 Delete() Delete() Delete() Delete()
이동 Move() MoveTo() Move() MoveTo()
존재 여부 확인 Exists() Exists Exists() Exists
속성 조회 GetAttributes() Attributes GetAttributes() Attributes
하위 디렉토리 조회 GetDirectories() GetDirectories()
하위 파일 조회 GetFiles() GetFiles()

파일을 읽고 쓰기 위해 알아야 할 것들

  • 메모리에서 하드 디스크 –DVD, 플래시 메모리 등 모두 마찬가지– 로 데이터를 옮길 때 먼저 스트림(Stream)을 만들어 둘 사이를 연결한 뒤에 메모리에 있는 데이터를 바이트 단위로 하드 디스크로 옮겨 넣는다. 하드 디스크에서 메모리로 데이터를 옮길 때도 마찬가지다.

  • 스트림은 데이터의 흐름이기 때문에 스트림을 이용하여 파일을 다룰 때는 처음부터 끝까지 순서대로 읽고 쓰는 것이 보통이다. 이것을 순차 접근(Sequential Access) 방식이라고 한다.
  • 이러한 스트림의 구조는 네트워크나 데이터 백업 장치의 데이터 입/출력 구조와도 통하기 때문에 스트림을 이용하면 파일이 아닌 네트워크를 향해서 데이터를 흘려 보낼 수 있고 테이프 백업 장치를 통해 데이터를 기록하거나 읽을 수도 있다.

  • 하드 디스크는 암과 헤드를 움직여 디스크의 어떤 위치에 기록된 데이터라도 즉시 찾아갈 수 있다. 가령 1MB 크기의 파일에서 768kbyte번째 위치한 데이터를 읽고 싶을 때 하드 디스크는 앞의 767kbyte를 읽지 않아도 곧장 원하는 위치로 이동 할 수 있다. 이런 방식을 임의 접근(Random Access) 방식이라고 한다.
  • Stream 클래스는 그 자체로 입력 스트림, 출력 스트림의 역할을 모두 할 수 있으며 파일을 읽고 쓰는 방식 역시 순차 접근 방식과 임의 접근 방식을 모두 지원한다.
  • 단, Stream 클래스는 추상 클래스이므로 이 클래스로부터 파생된 클래스를 이용해야 한다. 가령 저장 장치와 데이터를 주고 받으려면 FileStream 클래스를 이용하고 네트워크를 통해 데이터를 주고 받으려면 NetworkStream 클래스를 이용해야 한다.

/*
 * FileStream 인스턴스 생성 방식
 */

// 새 파일 생성
Stream stream1 = new FileStream("a.dat", FileMode.Create);

// 파일 열기
![](https://drive.google.com/uc?id=)파일을 열거나 파일이 없으면 생성
![](https://drive.google.com/uc?id=;)    // 파일을 비워서 열기
Stream stream4 = new FileStream("a.dat", FileMode.Truncate);

// 파일을 덧붙이기 모드로 열기
Stream stream5 = new FileStream("a.dat", FileMode.Append);


/*
 * FileStream을 이용해서 데이터를 파일에 기록하는 방식
 */
 long someValue = 0x1123456789ABCDEF0;

 // 파일 스트림 생성
 Stream outStream = new FileStream("a.dat", FileMode.Create);

 // someValue를 byte 배열로 변환. 
 // FileStream 클래스의 Read/ Write는 오로지 byte[]만 매개 변수로 받으므로 BitConverter를 이용해서 데이터를 변환해 주어야 한다.
 byte[] wBytes = BitConverter.GetBytes(someValue);

 // 변환한 byte 배열을 파일 스트림을 통해 파일에 기록
 outStream.Write(wBytes, 0, wBytes.Length);

 // 파일 스트림 닫기
 outStream.Close();


/*
 * FileStream을 이용해서 파일에서 데이터를 읽어오는 방식
 * 기본 내용은 동일하므로 상세한 내용은 생략한다.
 */
 byte[] rBytes = new byte[8];

 ![](https://drive.google.com/uc?id=)(rBytes, 0, rBytes.Length);

 long readValue = BitConverter.ToInt64(rBytes, 0);

 inStream.Close();

  • Stream 클래스에는 Position이라는 프로퍼티가 있는데, 이 Position 프로퍼티는 현재 스트림의 읽는 위치 또는 쓰는 위치를 나타낸다. 만일 Position이 3이라면 파일의 3번째 바이트에서 읽거나 쓸 준비가 되어 있는 상태라는 뜻이다.
  • 위 그림을 보면 FileStream 객체를 생성할 때 Position이 0이 되고, WriteByte() 메소드를 호출할 때마다 데이터를 기록한 후 자동으로 Position이 1씩 증가되는 것을 알 수 있다.
  • 따라서 여러 개의 데이터를 기록하는 일은 그냥 Write() –또는 WriteByte()– 메소드를 차례차례 호출하는 것으로 충분하다. 이렇게 파일을 순차적으로 쓰거나 읽는 방식을 “순차 접근(Sequential Access)” 라고 한다.
  • 한편 파일 내의 임이의 위치에 Position이 위치하도록 할 수도 있는데 –임의 접근(Random Access) 방식– Seek() 메소드를 호출하거나 Position 프로퍼티에 직접 원하는 값을 대입하면 지정한 위치로 점프하여 읽기/ 쓰기를 할 수 있다.
  • (코드 생략)

이진 데이터 처리를 위한 BinaryWriter/ BinaryReader

// BinaryWriter 를 이용한 파일 쓰기 예시
BinaryWriter bw = new BinaryWriter( new FileStream("a.dat", FileMode.Create) );

bw.Write(32);
bw.Write("Good Morning");
bw.Write(3.14);

bw.Close();


// BinaryReader 를 이용한 파일 읽기 예시
![](https://drive.google.com/uc?id=);)a = br.ReadInt32();
string s br.ReadString();
double c = br.ReadDouble();

br.Close();
  • FileStream 클래스는 데이터를 읽고 쓸 때 byte[] 형식으로 변환해 줘야 한다는 점에서 대단히 불편하다. 이런 불편함을 해결하기 위해 .NET 프레임워크는 BinaryWriter, BinaryReader 클래스를 만들었다. 클래스 사용 방식의 위의 예시 코드를 보면 된다.
  • (BinaryWriter와 BinaryReader가 만든 바이너리 파일에 대한 내용 생략)

텍스트 파일 처리를 위한 StreamWriter/ StreamReader

// StreamWriter 를 이용한 파일 쓰기 예시
StreamWriter sw = new StreamWriter( new FileStream("a.txt", FileMode.Create) );

sw.Write(32);
sw.Write("Good Morning");
sw.Write(3.14);

sw.Close();


// StreamReader 를 이용한 파일 읽기 예시
![](https://drive.google.com/uc?id=);)// EndOfStream 프로퍼티는 스트림의 끝에 도달했는지를 알려준다.
while ( sr.EndOfStream == false )
{
    Console.WriteLine(sr.ReadLine());
}

sr.Close();
  • StreamWriter, StreamReader 클래스를 이용하면 텍스트 파일을 읽고 쓸 수 있다.

객체 직렬화 하기

  • BinaryWriter/Reader, StreamWriter/Reader는 기본 데이터 형식을 스트림에 쓰고 읽을 수 있도록 메소드들을 제공하지만, 프로그래머가 직접 정의한 클래스나 구조체 같은 복합 데이터 형식은 지원하지 않는다.
  • BinaryWriter/Reader, StreamWriter/Reader로 복합 데이터 형식을 기록하고 읽으려면 그 형식이 갖고 있는 필드의 값을 저장할 순서를 정한 후, 이 순서대로 저장하고 읽는 코드를 작성해야 한다.
  • 이 문제를 위해 C#은 복합 데이터 형식에 대해 쉽게 스트림에 쓰기/ 읽기를 할 수 있게 하는 직렬화(Serialization)라는 메커니즘을 제공한다.
    • 직렬화란 객체의 상태를 메모리나 영구 저장 장치에 저장이 가능한 0과 1의 순서로 바꾸는 것을 말한다.
    • .NET 에서는 이진(Binary) 형식의 직렬화도 지원하지만, JSON이나 XML 같은 텍스트 형식으로의 직렬화도 지원한다. (C# 완전 짱짱맨)
  • C#에서는 객체를 직렬화 할 수 있는 아주 간단한 방법을 제공하는데, 클래스 선언부 앞에 [Serializable] 애트리뷰트를 붙여주기만 하면 된다.

직렬화 예시

// 직렬화 하려면 클래스 위에 `[Serializable]` 애트리뷰트를 붙여주면 된다.
[Serializable]
class MyClass
{
    public int myField1;
    public int myField2;

    [NonSerialized]
    public int myField3; // myField3 을 제외한 나머지 필드만 직렬화 된다.

    public int myField4;
}

class MainApp
{
    void WriteFile ()
    {
        Stream ws = new FileStream("a.dat", FileMode.Create);
        BinaryFormatter serializer = new BinaryFormatter();

        MyClass obj = new MyClass();
        // obj 필드에 값 저장...

        serializer.Serialize(ws, obj); // 직렬화
        ws.Close();
    }

    void ReadFile ()
    {
        ![](https://drive.google.com/uc?id=)new BinaryFormatter();

        MyClass obj = (MyClass)deserializer.Deserialize(rs);
        rs.Close();
    }
}
  • BinaryFormmater는 직렬화하거나 역질렬화 하는 역할을 한다.
  • List를 비롯한 컬렉션들도 직렬화를 지원한다. 예컨대 List<MyClass> 형식의 객체도 직렬화를 통해 파일에 저장해 뒀다가 이를 역직렬화를 통해 –(List<MyClass>)deserializer.Deserialize(obj)– 컬렉션으로 불러들일 수 있다.

뇌를 자극하는 C# 4.0 프로그래밍/ dynamic 형식

dynamic 형식 소개

// dynamic 예시
class MyClass
{
    public void MethodA()
    {
        // ...
    }
}

class MainApp
{
    static void Main (string[] args)
    {
        dynamic obj = new MyClass();
        obj.MethodA();
        obj.MethodB(); // dynamic으로 선언되어 있기 때문에 컴파일 에러가 발생하지 않는다.
    }
}
  • dynamic 형식은 int, string과 달리 형식 검사가 실행시에 이루어진다는 특징이 있다.

오리 타이핑

“오리처럼 걷고 오리처럼 헤엄치며 오리처럼 꽉꽉거리는 새를 봤을 때, 나는 그 새를 오리라고 부른다.” –제임스 위트콤 라일리

  • dynamic은 오리 타이핑을 구현하기 좋은 형식이다. 어느 형식으로 상속 받는지는 따지지 않고 오리처럼 걷고, 오리처럼 헤엄치고, 오리처럼 꽉꽉거리기만 하면 오리라고 인정할 수 있다.
  • (오리 타이핑과 관련한 구현 코드 생략. 그냥 인터페이스 상속 받아 쓰는 게 나아 보임. 아래 내용을 보면 dynamic 형식은 다른 언어와의 상호 운용성을 위해서만 사용하는게 좋아 보인다.)

COM과 .NET 사이의 상호 운용성을 위한 dynamic 형식

  • COM이란 Component Object Model의 약자로 MS의 소프트웨어 컴포넌트 규격을 뜻한다. OLE, ActiveX, COM+와 같은 파생 규격들이 모두 COM을 바탕으로 만들어졌다.
  • C#을 비롯한 .NET 언어들은 RCW(Runtime Callable Wrapper)를 통해 COM 컴포넌트를 사용할 수 있다.
  • 그러나 RCW가 있어도 C#은 COM과 잘 어울리지 못했는데 그 이유는 다음과 같다.
    1. COM은 메소드가 결과를 반환할 때 실제 형식이 아닌 object 형식으로 반환한다. 이 때문에 C# 코드에서는 이 결과를 실제 형식으로 변환을 해줘야 하는 번거로움이 발생하였다.
    2. COM은 오버로딩을 지원하지 않는다. 대신 메소드의 선택적 매개변수와 기본 값 매개변수를 지원한다.
  • C# 4.0에 이르러서야 dynamic 형식의 도입으로 1번 문제를 해결 했고, 메소드의 선택적 매개 변수와 기본 값 매개 변수 도임을 통해 2번 문제를 해결하여 C#은 비주얼 베이직처럼 COM 친화적인 언어가 되었다.
  • (COM 컴포넌트 붙이는 부분 생략)

동적 언어와의 상호 운용성을 위한 dynamic 형식

  • CLR은 IL로 컴파일 할 수 있는 언어들은 지원하지만, 파이썬이나 루비처럼 실행시에 코드를 해석해서 실행하는 방식의 동적 언어는 지원할 수 없었다. 그래서 MS는 동적 언어를 실행할 수 있도록 해주는 플랫폼인 DLR(Dynamic Language Runtime)을 만들었다.
  • DLR은 CLR 위에서 동작하며, 파이썬이나 루비 같은 동적 언어를 실행할 수 있다. DLR의 장점은 그저 동적 언어를 .NET 플랫폼에서 실행할 수 있다는 정도가 아니라 C#이나 VB 같은 정적 언어의 코드에서 파이썬이나 루비 같은 동적 언어의 코드에서 만들어진 객체에 접근할 수 있게 해준다. 쉽게 말해 C# 코드에서 직접 파이썬이나 루비 코드를 실행하고 그 결과를 받아 볼 수 있다는 이야기.
  • CLR 입장에서 보면 DLR API를 기반으로 구현된 동적 언어라도 호스팅 할 수 있다는 장점이 있다. 가령 파이썬을 줄곧 사용해 오다가 파이썬에는 없는 라이브러리가 루비에 있는 경우, C# 프로그래머는 별도의 학습 없이 바로 루비 라이브러리를 이용하는 코드를 호스팅 할 수 있다.
  • COM과 .NET의 상호 운용성 문제에 사용했던 dynamic을 CLR과 DLR 사이의 상호 운용성 문제를 해결하는데도 사용할 수 있다.

파이썬 코드를 C#에서 실행하는 예제

// 1. 파이썬 소스 코드 파일을 C#에서 실행하는 예제
ScriptRuntime runtime = Python.CreateRuntime();
dynamic result = runtime.ExecuteFile("namecard.py");

// 2. 문자열에 담긴 파이썬 코드를 실행하는 예제
// ScriptEngine, ScriptScope, ScriptSource 클래스를 사용한다.
ScriptEngine engine = Python.CreateEngine();
ScriptScope scope = engine.CreateScope();

scope.SetVariable("n", "박수영");
scope.SetVariable("p", "010-1234-5678");

ScriptSource source = engine.CreateScriptSourceFromString(
    @"
    // 이하는 파이썬 코드, 파이썬 코드 전체를 string으로 넘기고 있다.
    class NameCard :
        name = ''
        phone = ''

        def __init__(self, name, phone) :
            self.name = name
            self.phone = phone

        def printNameCard(self) :
            print self.name + ', ' + self.phone

    NameCard(n, p)
");

dynamic result = source.Execute(scope);
result.printNameCard();

Console.WriteLine("{0}, {1}", result.name, result,phone);
  • dynamic 형식은 ScriptRuntime을 이용해서 소스 파일에 담긴 코드를 실행하든, ScriptEngine, ScriptScope, ScriptSource를 이용해서 문자열에 담긴 코드를 그 자리에서 실행하든 C# 코드가 호스팅하고 있는 파이썬 코드 안에서 만들어진 객체를 그대로 받아낸다. 이렇게 받아낸 파이썬 출신의 객체는 C# 코드에서 직접 메소드를 호출할 수도 있고 필드에 접근하는 것도 가능하다.

뇌를 자극하는 C# 4.0 프로그래밍/ 리플렉션과 애트리뷰트

리플렉션

  • 리플렉션은 객체의 형식 정보를 들여다보는 기능으로 이 기능을 이용하면 프로그램 실행 중에 객체의 형식 이름부터, 프로퍼티 목록, 필드, 이벤트 목록까지 모두 열어 볼 수 있다.
  • 형식의 이름만 있다면 동적으로 인스턴스를 만들 수도 있고, 그 인스턴스의 메소드를 호출할 수도 있다. 또한 새로운 데이터 형식을 동적으로 만들 수도 있다.
  • .NET 팀은 모든 형식을 들여다 볼 수 있는 장치를 설계했다. 모든 데이터 형식의 조상인 Object 형식에 GetType() 메소드를 만들어 놓은 것이 그것이다.

Object.GetType() 메소드와 Type 클래스

// GetType() 메소드와 Type 형식을 사용하는 방법 예시
int a = 0;

Type type1 = a.GetType();
FieldInfo[] fields = type1.GetFields();

foreach (FieldInfo field in fields)
{
    Console.WriteLine("Type: {0}, Name:{1}", field.FieldType.Name, field.Name);
}

// GetFields(), GetMethods() 에 검색 옵션 지정하는 예시
// 옵션은 System.Reflection.BindingFlags 열거형으로 구성된다.
Type type2 = a.GetType();

// Public 인스턴스 필드 조회
var fields1 = type.GetFields( BindingFlags.Public | BindingFlags.Intance );

// 비 Public 인스턴스 필드 조회
var fields1 = type.GetFields( BindingFlags.NonPublic | BindingFlags.Intance );

// Public 정적 필드 조회
var fields1 = type.GetFields( BindingFlags.Public | BindingFlags.Static );

// 비 Public 정적 필드 조회
var fields1 = type.GetFields( BindingFlags.NonPublic | BindingFlags.Static );
Type 형식의 메소드 반환 형식 설명
GetConstructors() ConstructorInfo[] 해당 형식의 모든 생성자 목록을 반환
GetEvents() EventInfo[] 해당 형식의 이벤트 목록을 반환
GetFields() FieldInfo[] 해당 형식의 필드 목록을 반환
GetGenericArguments() Type[] 해당 형식의 형식 매개 변수 목록을 반환
GetInterfaces() Type[] 해당 형식이 상속하는 인터페이스 목록을 반환
GetMembers() MemberInfo[] 해당 형식의 멤버 목록을 반환
GetMethods() MethodInfo[] 해당 형식의 메소드 목록을 반환
GetNestedTypes() Type[] 해당 형식의 내장 형식 목록을 반환
GetProperties() PropertyInfo[] 해당 형식의 프로퍼티 목록을 반환

리플렉션을 이용해서 객체 생성하고 이용하기

// 인스턴스를 동적으로 만드는 예제
class Profile
{
    public Name { get; set; }
    public Phone { get; set; }
}

static void Main ()
{
    Type type = typeof(Profile);

    // Activator.CreateInstance()는 입력받은 형식의 인스턴스를 생성하여 반환한다.
    Object profile = Activator.CreateInstance(type);

    // PropertyInfo 객체는 SetValue(), GetValue()를 통해 값을 할당하고 읽어올 수 있다.
    PropertyInfo name = type.PropertyInfo("Name");
    PropertyInfo phone = type.PropertyInfo("Phone");

    // SetValue(), GetValue()의 마지막 매개 변수는 인덱서의 인덱스를 위해 사용된다. 
    // 프로퍼티는 인덱서가 필요 없으므로 null을 할당.
    name.SetValue(profile, 박찬호", null);
    phone.SetValue(profile, "010-1234-5678", null);

    Console.WriteLine("{0}, {1}", name.GetValue(profile, null), phone.GetValue(profile, null));
}

형식 내보내기

  • 리플렉션을 이용하면 런타임에 원하는 형식의 정보를 읽어 낼 수 있고, 그 형식의 인스턴스도 만들 수 있으며, 프로퍼티나 필드에 값을 할당하고 메소드를 호출할 수도 있다.
  • C#은 여기에 한 술 떠서 프로그램 실행 중에 새로운 형식을 만들어 낼 수도 있다.
  • (이하 내용은 어셈블리 단의 내용을 다루는데, 개인적으로는 흥미가 없으므로 생략)

애트리뷰트

애트리뷰트 사용하기

// 애트리뷰트 사용 방법
[ 애트리뷰트이름 (애트리뷰트매개변수) ]
public void MyMethod ()
{
    // ...
}

// 애트리뷰트 사용 예시
class MyClass
{
    [Obsolete("OldMethod()는 폐기되었습니다. NewMethod()를 이용하세요")]
    public void OldMethod ()
    {
        // ...
    }

    public void NewMethod ()
    {
        // ...
    }
}

내가 만드는 애트리뷰트

// 애트리뷰트 만드는 방법
class History : System.Attribute
{
    private string programmer;

    public double Version { get; set; }
    public string Changes { get; set; }

    // 생성자
    public History (string programmer)
    {
        this.programmer = programmer;
        Version = 1.0;
        Changes = "First Release";
    }

    public string Programmer
    {
        get { return programmer; }
    }
}

// 만든 애트리뷰트 사용 방법
[History("Sean", Version = 0.1, Changes = "2010-11-01 Created class stub")]
class MyClass
{
    // ...
}
  • 모든 애트리뷰트는 System.Attribute 클래스의 상속을 받아 만들어진다. 따라서 System.Attribute를 상속 받는 것만으로도 애트리뷰트 하나를 만든 셈이 된다.
  • 사용할 때는 [와 ] 안에 애트리뷰트 이름을 넣어 사용하면 된다.

애트리뷰트의 애트리뷰트

// System.AttributeUsage 사용 예시
[System.AttributeUsage(System.AttributeTarget.Class | System.AttributeTarget.Method, AllowMultiple = true)]
class History : System.Attribute
{
    // ...
}
  • System.AttributeUsage는 애트리뷰트의 애트리뷰트이다. 선언하는 애트리뷰트 위에 위와 같이 System.AttributeUsage 애트리뷰트를 사용하면 애트리뷰트에 대한 설정을 할 수 있다.
Attribute Target 설명
All 이 표의 모든 요소
Assembly 어셈블리
Module 모듈
Interface 인터페이스
Class 클래스
Struct 구조체
ClassMembers 클래스 안에 선언된 클래스나 구조체를 포함한 클래스의 모든 멤버
Constructor 생성자
Delegate 델리게이트
Enum 열거형
Event 이벤트
Field 필드
Property 프로퍼티
Method 메소드
Parameter 메소드의 매개 변수
ReturnValue 메소드의 반환값