Tag Archives: 프로그래밍

전문가를 위한 C++/ C++와 표준 라이브러리 초단기 속성 코스

(전체가 아니라 C#과 차이가 있는 부분을 중심으로 요약 정리)

C++의 기초

프로그래밍 언어의 공식 예제 ‘Hello, World!’

주석

(생략)

전처리 지시자

C++로 작성된 소스 코드를 프로그램으로 만드는 빌드(build) 작업은 세 단계를 거친다.

  1. 전처리(preprocess) 단계에서는 소스 코드에 담긴 메타 정보를 처리한다.
  2. 컴파일(compile) 단계에서는 소스 코드를 머신이 읽을 수 있는 오브젝트(object) 파일로 변환한다.
  3. 링크(link) 단계에서는 앞에서 변환한 여러 오브젝트 파일을 애플리케이션으로 엮는다.

지시자(directive)는 전처리기에 전달할 사항을 표현하며 #include 처럼 # 문자로 시작한다.

main() 함수

프로그램은 항상 main() 함수에서 싲가한다. main() 함수는 int 타입의 값을 리턴하는데, 이 값으로 프로그램의 실행 결과에 대한 상태를 표시한다. main() 함수 안에서는 리턴 문장을 생략해도 되는데 그러면 자동으로 0을 리턴한다. (개인적으로 마음에 드는 디자인은 아니다. 이런 식으로 예외를 허용해주면 일관성이 깨지기 때문)

main() 함수는 매개변수를 받지 않거나 다음과 같이 2개를 받도록 작성할 수 있다.

int main(int argc, char* argv[])

argc는 프로그램에 전달할 인수 개수를 지정하고, argv는 전달할 인수의 값을 담는다. argv[0]에는 프로그램 이름이 담기는데, 공백 스트링으로 지정될 수 있어서 프로그램 이름을 참조하려면 이 값보다는 플랫폼에서 제공하는 기능을 사용하는 것이 좋다. 여기서 주의할 점은 실제 매개변수는 인덱스 1부터 시작한다는 것이다.

I/O 스트림

I/O 스트림은 13장에서 자세히 다루는데, 기본 원리는 굉장히 간단하다. 출력 스트림은 데이터를 나르는 컨베이어 벨트에 비유할 수 있다. 성격에 맞는 컨베이어 벨트에 데이터를 올려두기만 하면 그대로 출력된다.

데이터를 컨베이어 벨트에 올리는 작업은 << 연산자로 표현한다.

std::cout << "There are " << 219 << " ways I love you." << std::endl;

std::endl은 문장이 끝났다는 것을 의미하는데, 출력 스트림에서 std::endl이 나타나면 지금까지 전달한 내용을 모두 출력하고 다음 줄로 넘어간다. 문장의 끝은 \n 자로 표현할 수도 있다.

(이하 네임스페이스/ 리터럴/ 변수/ 연산자/ 타입/ 조건문/ 함수/ 배열/ std::vector/ 구조적 바인딩/ 반복문/ 이니셜라이저 리스트 생략)

C++의 고급기능

C++의 스트링

(생략)

포인터와 동적 메모리

스택과 힙

(생략)

포인터 사용법

메모리 공간을 적당히 할당하기만 하면 어떤 값도 힙에 저장할 수 있다. 

int* myIntegerPointer;

int 타입 뒤에 붙은 별표(*)는 이 변수가 정수 타입에 대한 메모리 공간을 가리킨다는 것을 의미한다. 이때 포인터는 동적으로 할당된 힙 메모리를 가리키는 화살표와 같다.

아직 값을 할당하지 않았기 때문에 포인터가 구체적으로 가리키는 대상은 없는데, 이를 초기화되지 않은 변수라 부른다. 포인터 변수는 초기화하지 않으면 어느 메모리를 기리키는지 알 수 없기 때문에 거의 대부분 프로그램이 뻗어 버린다. 그래서 포인터 변수는 선언하자마자 초기화한다. 포인터 변수에 메모리를 당장할당하고 싶지 않다면 널 포인터(nullptr)로 초기화한다.

int* myIntegerPointer = nullptr;

널 포인터란 정상적인 포인터라면 절대로 가자지 않을 특수한 값이며, 부울 표현식에서는 false로 표현한다.

포인터 변수에 메모리를 동적으로 할당할 때는 new 연산자를 사용한다.

myIntergerPointer = new int;

이렇게 하면 정숫값 하나에 대한 메모리 주소를 가리키며, 이 포인터가 가리키는 값에 접근하려면 포인터를 역참조(dereference) 해야 한다. 역참조란 포인터가 힙에 있는 실젯값을 가리키는 화살표를 따라간다는 뜻이다. 앞서 힙에 새로 할당한 공간에 정숫값을 넣으러면 다음과 같이 작성한다.

*myIntergerPointer = 8;

이 문장은 myIntegerPointer = 8; 과 전혀 다르다. 이 문장에서 변경하는 값은 포인터(메모리 주소)가 아니라 이 포인터가 가리키는 메모리에 있는 값이다.

동적으로 할당한 메모리를 다 쓰고 나면 delete 연산자로 그 공간을 해제해야 한다. 메모리를 해제한 포인터를 다시 사용하지 않도록 곧바로 포인터 변수의 값을 nullptr로 초기화 하는 것이 좋다.

delete myIntegerPointer;
myIntegerPointer = nullptr;

포인터는 힙뿐만 아니라 스택과 같은 다른 종류의 메모리를 가리킬 수도 있다. 원하는 변수의 포인터값을 알고 싶다면 주소 참조 연산자인 &를 사용한다.

int i = 8;
int* myIntegerPointer = &i; // 8이란 값을 가진 변수 i의 주소를 가리키는 포인터

C++은 구조체의 포인터를 다루는 부분을 조금 다르게 표현한다. 다시 말해 먼저 * 연산자로 역참조해서 구조체 자체(시작 지점)에 접근한 뒤 필드에 접근할 때는 . 연산자로 표기한다. 예컨대 다음 코드와 같다. 여기서는 getEmployee() 함수가 Employee 구조체를 리턴한다고 가정한다.

Employee* anEmployee = getEmployee();
cout << (*anEmployee).salary << endl;

위 코드가 복잡해 보이는데, 좀 더 간결하게 표현하고 싶다면 -> (화살표) 연산자로 다음과 같이 표기해도 된다. (화살표 연산자는 *와 . 를 하나로 합친 것)

Employee* anEmployee = getEmployee();
cout << anEmployee->salary << endl;

포인터를 다룰 때 단락 논리를 적용하면 잘못된 포인터에 접근하지 않게 할 수 있다.

bool isValidSalary = (anEmployee != nullptr && anEmployee->salary > 0);

동적으로 배열 할당하기

Note) C에서 사용하던 malloc()이나 free()는 사용하지 말고, new와 delete, new[], delete[]를 사용하라

널 포인터 상수

C++ 11 이전에는 NULL 이란 상수로 널 포인터를 표현했는데, NULL은 실제로 상수 0과 같아서 문제가 발생할 수 있다. 따라서 정식 널 포인터 상수인 nullptr을  사용하라

스마트 포인터

스마트 포인터를 사용하면 메모리와 관련하여 흔히 발생하는 문제를 방지할 수 있기 때문에 스마트 포인터를 사용하는 것이 권장된다. (아무리 메모리 해제를 꼼꼼히 해도 문제가 생길 수 있는 이유가, 실행 중 예외가 발생하는 경우 메모리 해제 코드까지 도달하지 못하기 때문)

C++에서 가장 중요한 스마트 포인터 타입은 다음 두 가지 이다.

  • std::unique_ptr
  • std::shared_ptr

unique_ptr은 포인터로 가리키는 대상이 스코프를 벗어나거나 삭제될 때 할당된 메모리나 리소스도 자동으로 삭제된다는 점을 제외하면 일반 포인터와 같다. 그러나 unique_ptr이 가리키는 객체를 일반 포인터로는 가리킬 수 없다.

unique_ptr은 return 문이 실행되거나 exception이 발생하더라도 항상 할당된 메모리나 리소스를 해제할 수 있다.

unique_ptr을 생성할 때는 반드시 std::make_unique<>()를 사용해야 한다.

auto anEmployee = make_unique<Employee>();

unique_ptr은 제네릭 스마트 포인트라서 어떠한 종류의 메모리도 가리킬 수 있다. 그래서 템플릿으로 만든 것이다.

make_unique()는 C++ 14부터 추가된 것이기 때문에 C++ 14를 지원하지 않는 컴파일러를 사용한다면 다음과 같은 방법으로 unique_ptr을 만든다.

unique_ptr<Employee> anEmployee(new Employee);

스마트 포인터로 지정한 anEmployee의 사용법은 일반 포인터와 같다.

cout << "Salary: " << anEmployee->salary <<< endl;

unique_ptr은 C 스타일 배열을 저장하는데도 활용할 수 있다. 다음 예는 열 개의 Employee 인스턴스로 구성된 배열을 생성하여 이를 unique_ptr에 저장하고 배열에 담긴 원소를 접근하는 방법을 보여주고 있다.

auto employees = make_unique<Employee[]>(10);
cout << "Salary: " << employees[0].salary << endl;

shared_ptr을 사용하면 데이터를 공유할 수 있다. shared_ptr에 대한 대입 연산이 발생할 때마다 레퍼런스 카운트가 하나씩 증가한다. 그래서 shared_ptr가 가리키는 데이터를 레퍼런스 카운트만큼 사용하고 있다는 것을 표현한다. shared_ptr가 스코프를 벗어나면 레퍼런스 카운트가 감소한다. 그러다 레퍼런스 카운트가 0이 되면 그 데이터를 아무도 가지고 있지 않기 때문에 포인터로 가리키던 객체를 해제한다.

shared_ptr는 std::make_shared<>()로 생성한다.

auto anEmployee = make_shared<Employee>();

if (anEmployee)
{
cout << "Salary: " << anEmployee->salary << endl;
}

C++ 17부터 shared_ptr에 배열도 저장할 수 있다. 배열을 저장하는 shared_ptr을 생성할 때는 make_shared<>()를 사용할 수 없고 다음과 같이 작성해야 한다.

shared_ptr<Employee[]> employees(new Employee[10]);
cout << "Salary: " << employees[0].salary << endl;

const의 다양한 용도

const 상수

(생략)

const 매개변수

C++에서는 non-const 변수를 const 변수로 캐스팅할 수 있다. 이렇게 하면 다른 코드에서 변수를 변경하지 않도록 어느 정도 보호할 수 있다.

다음 코드는 mysteryFunction()을 호출할 때 string*을 const string*으로 자동으로 캐스팅한다. 이때 mysteryFunction() 안에서 매개변수로 전달된 스트링의 값을 변경하면 컴파일 에러가 발생한다.

void mysteryFunction(const std::string* someString)
{
*someString = "Test"; // 컴파일 에러
}

int main()
{
std::string myString = "The string";
mysteryFunction(&myString);
return 0;
}

레퍼런스

C++에서 제공하는 레퍼런스를 사용하면 기존 변수에 새 이름을 지정할 수 있다.

int x = 42;
int& xReference = x;

변수의 타입 뒤에 &를 붙이면 그 변수는 레퍼런스가 된다. 코드에서 다루는 방법은 일반 변수와 같지만 내부적으로는 원본 변수에 대한 포인터로 취급한다. 위의 예에서 나온 일반 변수 x와 레퍼런스 변수 xReference는 모두 같은 값을 가리키며, 둘 중 한 변수에서 값을 변경하면 그 결과가 다른 변수에도 반영된다.

레퍼런스 전달 방식

일반적으로 함수에 전달한 변수는 값 전달 방식(pass by value)으로 처리한다. 예컨대 함수의 매개변수에 정수를 전달하면 함수 안에는 그 정수의 복제본이 전다로딘다. 따라서 함수 안에서 원본 변수의 값을 변경할 수 있다.

C에서는 스택 변수에 대한 포인터를 자주 사용했는데, 이런 방식을 사용하면 다른 스택 프레임에 있는 원본 변수를 수정할 수 있다. 이러한 포인터를 역참조하면 그 포인터가 현재 스택 프레임을 가리키지 않더라도 함수 안에서 그 변수가 가리키는 메모리의 값을 수정할 수 있다. 그런데 이 방식은 포인터 연산이 많아져서 간단한 작업이라도 코드가 복잡해진다.

C++에서는 값 전달방식보다 뛰어난 레퍼런스 전달 방식(pass by reference)을 제공한다. 이 방식을 이용하면 매개변수가 포인터값이 아닌 레퍼런스로 전달된다.

예컨대 addOne() 함수를 두 가지 방식으로 구현한 코드를 살펴보자. 첫 번째 함수는 매개변수가 값으로 전달되 함수 안에서는 그 값의 복제본을 조작하기 때문에 원본 변수는 값이 변하지 않는다. 두 번째 함수는 레퍼런스로 전달되기 때문에 원본 변수의 값도 변경된다.

void addOne(int i)
{
i++;
}

void addOne(int& i)
{
i++;
}

레퍼런스를 받는 함수를 호출하는 문장을 작성하는 방식은 일반 함수를 호출할 때와 같다.

복제하는데 부담스러울 정도로 큰 구조체나 클래스를 리턴하는 함수를 구현할 때는 구조체나 클래스를 non-const 레퍼런스로 받아서 원하는 작업을 수행한 뒤 그 결과를 직접 리턴하지 않고 내부에서 곧바로 수정하는 방식을 많이 사용한다. 

하지만 C++ 11부터 추가된 이동 의미론(move semantics) 덕분에 복제하지 않고도 구조체나 클래스를 직접 리턴할 수 있다.

Note) 위 두 함수는 미묘한 차이가 있는데, 값으로 전달하는 버전은 매개변수로 리터럴을 넣어도 문제 없지만, 레퍼런스를 전달하는 버전은 리터럴을 넣을 경우 컴파일 에러가 발생한다.

const 레퍼런스 전달 방식

함수의 매개변수를 const 레퍼런스로 전달하는 코드를 자주 볼 수 있는데, 얼핏 보면 모순되는 표현처럼 보인다. 레퍼런스 매개변수를 사용하면 변수의 값을 수정할 수 있는데 const로 지정하면 그렇게 할 수 없기 때문이다.

const 레퍼런스의 가장 큰 장점은 성능이다. 함수에 매개변수를 값으로 전달하면 그 값 전체가 복제된다. 그러나 레퍼런스로 전달하면 원본에 대한 포인터만 전달되기 때문에 원본 전체를 복제할 필요가 없다. 또한 const로 지정하면 원본 변수가 변경되지도 않는다.

const 레퍼런스는 특히 객체를 다룰 때 유용하다. 객체는 대체로 커서 복제하는 동안 의도하지 않은 효과가 발생할 수 있기 때문이다.

void printString(const std::string& myString)
{
std::cout << myString << std::endl;
}

int main()
{
std::string someString = "Hello World";
printString(someString);
printString("Hello World"); // 리터럴을 전달해도 된다.
return 0;
}

익셉션

C++은 유연성은 뛰어나지만 안정성은 좋지 않다. 메모리 공간을 무작위로 접근하거나 0으로 나누는 연산을 수행하더라도 컴파일러는 가만히 내버려둔다. 

(이하 생략)

타입 추론

auto 키워드

auto 키워드는 다음과 같이 다양한 상황에서 사용한다.

  • 함수의 리턴 타입을 추론한다.
  • 구조적 바인딩에 사용한다.
  • 표현식의 타입을 추론하는데 사용한다.
  • 비타입(non-type, 타입이 아닌) 템플릿 매개변수의 타입을 추론하는데 사용한다.
  • decltype(auto)에서 사용한다.
  • 함수에 대한 또 다른 문법으로 사용한다.
  • 제네릭 람다 표현식에서 사용한다.

auto 키워드를 지정하면 그 변수의 타입은 컴파일 시간에 자동으로 추론해서 결정된다.

auto x = 123; // x는 int 타입으로 결정된다.

auto로 표현식의 타입을 추론하면 함수에 지정된 레퍼런스나 const 한정자가 제거된다.

const std::string message = "Test";

const std::string& foo()
{
return message;
}

foo() 함수의 결과를 auto 타입으로 저장하면 다음과 같다.

auto f1 = foo();

auto를 지정하면 레퍼런스와 const 한정자가 사라지기 때문에 f1은 string 타입이 된다. 따라서 값이 복제되어 버린다. const 타입으로 지정하려면 다음과 같이 auto 키워드 앞뒤에 레퍼런스 타입과 const 키워드를 붙인다.

const auto& f2 = foo();

decltype 키워드

decltype 키워드는 인수로 지정한 표현식의 타입을 알아낸다.

int x = 123;
decltype(x) = 456;

이렇게 작성하면 컴파일러는 y의 타입이 x의 타입인 int라고 추론한다.

decltype은 레퍼런스나 const 지정자를 삭제하지 않는다는 점에서 auto와 다르다. 여기서 string을 가리키는 const 레퍼런스를 리턴하는 함수 foo()를 살펴보자. f2를 다음과 같이 decltype으로 정의하면 const string& 타입이 돼 복제 방식으로 처리하지 않는다.

decltype(foo()) f2 = foo();

얼핏보면 decltype을 사용한다고 특별히 나아질 게 없다고 생각할 수 있지만 템플릿을 사용할 때 상당히 강력한 효과를 발휘한다.

(이하 1장 생략)

C# 6.0 완벽 가이드/ Roslyn 컴파일러

  • C# 6.0에는 완전히 C#으로 작성된 새로운 컴파일러가 있다. 새 컴파일러는 모듈식으로 구성되어 있어서, 소스 코드를 실행 파일이나 라이브러리로 컴파일하는 것 말고도 그 기능들을 다양한 방식으로 활용할 수 있다. Roslyn(로즐린)이라는 이름의 컴파일러 덕분에 정적 코드 분석 도구나 리팩터링 도구, 구문 강조 기능과 코드 완성 기능을 갖춘 편집기, 그리고 C# 코드를 이해하는 Visual Studio 플러그인을 만들기가 좀 더 쉬워졌다.
    • Roslyn 라이브러리들은 NuGet에서 내려받을 수 있다. C#용 패키지뿐만 아니라 VB용 패키지도 있다. 두 언어는 일부 구조를 공유하므로, 의존하는 라이브러리들도 일부 겹친다. C# 컴파일러 라이브러리들의 NuGet 패키지 ID는 Microfost.CodeAnalysis.CSharp이다.
    • Roslyn의 소스 코드는 Apache 2 오픈소스 사용권 하에 공개되어 있다. 이 소스 코드는 또 다른 가능성을 열어주는데, 예컨대 C#을 커스텀 언어 또는 영역 국한 언어(domain-specific language)로 바꾸는 것도 가능하다. 소스코드는 GitHub의 Roslyn 페이지(https://github.com/dotnet/roslyn)에서 내려받을 수 있다.
    • GitHub의 Roslyn 페이지에는 문서화와 예제들 그리고 코드 분석과 리팩터링 방법을 보여주는 단계별 튜토리얼들이 있다.
  • Roslyn C# 컴파일러 라이브러리를 구성하는 어셈블리들은 다음과 같다.
    • Microsoft.CodeAnalysis.dll
    • Microsoft.CodeAnalysis.CSharp.dll
    • System.Collections.Immutable.dll
    • System.Reflection.Metadata.dll

Roslyn의 구조

  • Roslyn은 컴파일 과정은 다음 세 단계로 나누어서 진행한다.
    1. 코드를 구문 트리로 파싱한다. 이 단계는 구문층(syntatic layer)에 해당한다.
    2. 식별자들을 기호(symbol)들에 묶는다(바인딩). 이 단계는 의미층(semantic layer)에 해당한다.
    3. IL 코드를 산출한다.
  • 첫 단계에서 파서는 C# 코드를 읽어서 구문 트리(syntax tree)를 출력한다. 구문 트리는 소스 코드의 구조와 내용을 트리 형태로 구성한 DOM(Document Object Model; 문서 객체 모형)이다.
  • 둘째 단계에서는 C#의 정적 바인딩(static binding)이 일어난다. 이 단계에서 컴파일러는 어셈블리 참조 정보를 마련해서, 이를테면 ‘Console’ 이라는 식별자가 mscorlib.dll의 System.Console을 지칭한다는 사실을 파악한다. 중복적재 해소와 형식 추론도 이 단계에서 일어난다.
  • 셋째 단계는 출력 어셈블리를 만들어 낸다. 독자가 코드 분석이나 리팩터링을 위해 Roslyn을 사용할 계획이라면 이 셋째 단계는 필요하지 않을 것이다.

Continue reading

C# 6.0 완벽 가이드/ 정규 표현식

  • 정규 표현식(regular expression) 줄여서 정규식(regex)은 문자 패턴을 식별하는 수단이다.
    • 정규식을 지원하는 .NET 형식들은 Perl 5의 정규 표현식 문법을 따르며, 패턴을 찾는 기능뿐만 아니라 찾아 바꾸는 기능도 지원한다.
  • 정규 표현식은 이를테면 다음과 같은 과제에 쓰인다.
    • 패스워드나 전화번호 같은 텍스트 입력의 유효성 점검(ASP.NET은 이 용도만을 위해 ReularExpressionValidator라는 컨트롤을 제공한다)
    • 텍스트 자료를 좀 더 구조화된 형태로 파싱(이를테면 HTML 페이지에서 자료를 추출해서 데이터베이스에 저장하는 등)
    • 문서에 있는 특정 패턴의 텍스트를 치환

정규 표현식의 기초

  • 정규 표현식에는 여러 연산자가 있는데, 그중 한정사(quantifier; 양화사)라고 부르는 연산자들이 특히나 많이 쓰인다.
    • 한정사 중 하나인 ?는 그 앞의 항목이 0회 또는 1회 나와야 한다는 뜻이다. 다른 말로 하면 ?는 그 앞의 항목이 선택적(optional)임을 뜻한다.
    • 예컨대 “colou?r”라는 정규 표현식은 color와도 부합하고 colour와도 부합하지만, colouur와는 부합하지 않는다.
Console.WriteLine(Regex.Match("color", @"colou?r").Success);  // True
Console.WriteLine(Regex.Match("colour", @"colou?r").Success);  // True
Console.WriteLine(Regex.Match("colouur", @"colou?r").Success);  // False
  • Regex.Match는 주어진 문자열에서 주어진 패턴과 부합하는 부분 문자열을 찾는다.
    • 이 메서드가 돌려주는 객체에는 패턴과 부합하는 부분 문자열의 시작 색인을 담은 Index 속성과 길이를 담은 Length 속성, 그리고 부함 문자열 자체를 담은 Value 속성이 있다.
Match m = Regex.Match("any colour you like", @"colou?r");

Console.WriteLine(m.Success);  // True
Console.WriteLine(m.Index);  // 4
Console.WriteLine(m.Length);  // 6
Console.WriteLine(m.Value);  // colour
Console.WriteLine(m.ToString());  // colour
  • Regex.Match를 string의 IndexOf 메서드의 좀 더 강력한 버전이라고 생각해도 될 것이다. 차이점은 Regex.Match는 주어진 문자열을 곧이곧대로 검색하는 것이 아니라 패턴을 검색한다는 것이다.
  • IsMatch 메서드는 Match 호출 후 Success 속성을 판정하는 과정을 하나로 엮은 단축 메서드이다.
  • 정규 표현식 엔진은 기본적으로 왼쪽에서 오른쪽으로 패턴을 점검하므로, Match는 가장 왼쪽의 부함만을 돌려준다. 더 많은 부합을 얻으려면 NextMatch 메서드를 사용해야 한다.
Match m1 = Regex.Match("One color? Threre are two colours in my head!", @"colou?rs?");
Match m2 = n1.NextMatch();
Console.WriteLine(m1);  // color
Console.WriteLine(m2);  // colour
  • Matches 메서드는 모든 부합을 배열에 담아 돌려준다.
foreach (Match m in Regex.Match("One color? Threre are two colours in my head!", @"colou?rs?"))
  Console.WriteLine(m);
  • 흔히 쓰이는 또 다른 정규 표현식 연산자로 대안 선택자(alternator)가 있다. 대안 선택자는 수직선 기호 |로 표시한다. 대안 선택자는 말 그대로 선택할 수 있는 대안들을 표현한다.
    • 예컨대 다음은 “Jen”이나 “Jenny”, “Jennifer”와 부합한다.
Console.WriteLine(Regex.IsMatch("Jenny", "Jen(ny|nifer)?"));  // True
  • 대안 선택자를 감싸는 괄호는 대안들을 정규식의 나머지 부분과 구분하는 역할을 한다.
  • .NET Framework 4.5 부터는 정규 표현식 부합 메서드 호출 시 만료 시간을 지정할 수 있다.
    • TimeSpan 객체로 주어진 시간이 다 지나도 부합 연산이 완료되지 않으면 RegexMatchTimeoutException 예외가 발생한다.
    • 임의의 정규 표현식(이를테면 고급 검색 대화상자에 사용자가 입력한 정규식)을 처리하는 프로그램이라면 잘못된 또는 악의적인 정규 표현식 때문에 프로그램이 무한히 멈추는 일을 방지하기 위해 이러한 시간 만료 기능을 활용하는 것이 바람직하다.

Continue reading

C# 6.0 완벽 가이드/ 상호운용성

네이티브 DLL 호출

  • Platform Invocation Services(플랫폼 호출 서비스)를 줄인 P/Invoke는 .NET 응용 프로그램에서 비관리(unmanaged; .NET이 관리하지 않는) DLL에 있는 함수나 구조체, 콜백에 접근하는데 사용하는 기술이다.
  • 예컨대 Windows DLL user32.dll에 있는 MessageBox 함수를 생각해 보자. 이 C함수는 다음과 같이 선언되어 있다.
int MessageBox(HWND hWnd, LPCTSTR lpText, LPCTStr lpCaption, UINT uType);
  • .NET 응용 프로그램에서 이 함수를 직접 호출하는 것은 생각보다 쉽다. 같은 이름의 정적 메서드를 선언하되 extern 키워드를 적용하고 DllImport 특성을 부여하면 된다.
using System;
using System.Runtime.IneropServices;

class MsgBoxTest
{
  [DllImport("user32.dll")]
  static extern int MessageBox(IntPtr hWnd, string text, string caption, int type);

  public static void Main()
  {
    MessageBox(IntPtr.Zero, "Please do not press this again.", "Attention", 0);
  }
}
  • 실제로 System.Windows 이름공간과 System.Windows.Forms 이름공간에 있는 MessageBox 클래스들이 이와 비슷한 비관리 메서드들을 이런 식으로 호출한다.
  • CLR에는 .NET 형식들과 비관리 형식들 사이에서 매개변수들과 반환 값들을 변환하는 방법을 아는 인도기(marshaler)가 있다.
    • 지금 예에서는 int 매개변수는 함수가 기대하는 4바이트 정수로 직접 대응되며, 문자열 매개변수는 2바이트 유니코드 문자들의 널 종료(null-terminated) 배열로 변환된다.
    • IntPtr은 비관리 핸들을 캡슐화 하도록 만들어진 하나의 구조체로, 그 너비는 32비트 플랫폼에서는 32비트이고 64비트 플랫폼에서는 64비트이다.

Continue reading

C# 6.0 완벽 가이드/ 응용 프로그램 도메인

  • 응용 프로그램 도메인(appication domain)은 .NET 프로그램이 실행되는 하나의 실행 시점 격리 단위(unit of isolation)이다.
  • 응용 프로그램 도메인은 관리되는 메모리 경계로 작용하며, 적재된 어셈블리들과 응용 프로그램 구성 설정들을 담는 컨테이너이기도 하다. 또한 분산 응용 프로그램의 경우 통신의 경계를 나타내기도 한다.
  • 일반적으로 하나의 .NET 프로세스는 하나의 응용 프로그램 도메인을 수용한다. 프로세스 시동시 CLR이 자동으로 생성한 기본(default) 도메인이 바로 그것이다.
    • 그러나 한 프로세스가 응용 프로그램 도메인들을 더 생성하는 것이 가능하며, 종종 유용하다.
    • 응용 프로그램 도메인을 추가로 생성하면 개별 프로세스들을 둘 때 발생하는 통신상의 복잡한 문제를 피하면서도 코드 실행 단위들을 서로 격리할 수 있다.
    • 이러한 접근방식은 부하 검사나 응용 프로그램 부분 갱신(patching) 같은 시나리오에 유용하며 안정적인 오류 복구 메너티즘을 구현할 때에도 유용하다.
  • 이번 장은 Windows 스토어 앱이나 CoreCLR 앱과는 무관하다. 그런 앱에서는 오직 하나의 응용 프로그램 도메인만 사용할 수 있다.

응용 프로그램 도메인의 구조

  • 아래 그림은 단일 도메인, 다중 도메인, 그리고 전형적인 분산 클라이언트/서버 응용 프로그램 도메인 구조를 나타낸 것이다. 대부분의 경우, 응용 프로그램 도메인을 수용하는 프로세스들은 운영체제가 암묵적으로 생성한다. (예컨대 사용자가 .NET 실행 파일을 더블클릭하거나 Windows 서비스가 시작될 때)
    • 그러나 IIS 같은 다른 프로세스가 응용 프로그램 도메인을 가지거나 SQL Server가 CLR 통합을 통해서 응용 프로그램 도메인을 가지기도 한다.
  • 단순한 실행 파일에서 비롯된 프로세스는 기본 응용 프로그램 도메인의 실행이 끝나면 함께 끝난다. 그러나 IIS나 SQL Server 같은 호스트에서는 프로세스가 그 수명을 제어한다.
    • 즉 필요에 따라 .NET 응용 프로그램 도메인을 생성하고 파괴한다.

Continue reading

C# 6.0 완벽 가이드/ 병렬 프로그래밍

  • 이번 장에서는 다중 코어 프로세서의 활용을 목적으로 하는 다음과 같은 다중 스레드 API 들과 구축 요소들을 살펴본다.
    • PLINQ(Parallel LINQ; 병렬 LINQ)
    • Parallel 클래스
    • 작업 병렬성 구축 요소
    • 동시적 컬렉션(concurrent collection)
  • 이들은 모두 .NET Framework 4.0에서 도입되었다. 이들을 통틀어 PFX(Parallel Framework; 병렬 프레임워크)라고 부르기도한다.
    • 그리고 Parallel 클래스와 작업 병렬성 요소들을 합해서 TPL(Task Parallel Library; 작업 병렬 라이브러리)이라고 부른다.
  • 이번 장을 이해하려면 14장에서 말한 기본 개념들에 익숙해야 한다. 특히 잠근, 스레드 안전성, Task 클래스를 숙지할 필요가 있다.

PFX가 왜 필요한가?

  • 지난 10여년 사이에 CPU 제조사들은 단일 코어 프로세서에서 다중 코어 프로세서로 초점을 옮겼다. 이떄문에 예전처럼 그냥 CPU만 빠랄지면 단일 스레드 코드도 저절로 빨라지는 현상은 더는 기대할 수 없게 되었다. 이제 성능 향상을 위해서는 여러 개의 코어(core)를 제대로 활용해야 한다.
  • 서버 응용 프로그램들은 대부분 각 클라이언트 요청을 개별 스레드에서 처리하는 형태이므로 여러 코어를 활용하는 것이 어렵지 않다. 그러나 데스크톱 응용 프로그램은 그렇지 않다. 데스크톱 응용 프로그램에서 다중 코어를 활용하려면 프로그램 중 처리량이 많은 코드의 구조를 다음과 같은 형태로 개선해야 한다.
    1. 처리할 일거리를 더 작은 덩어리들로 분할(partitioning)한다.
    2. 각 덩어리를 다중 스레드 기법을 이용해서 병렬로 처리한다.
    3. 처리가 끝난 스레드들의 결과를 스레드에 안전한, 그리고 성능 효율적인 방식으로 취합(collating) 한다.
  • 이러한 개선을 고전적인 다중 스레드 적용 수단들을 이용해서 독자가 직접 수행할 수도 있지만, 그리 쉬운 일은 아니다. 특히 분할과 취합 단계가 까다롭다.
    • 게다가 다수의 스레드가 같은 자료를 동시에 다루는 경우 스레드 안전성 확보에 흔히 쓰이는 잠금 전략들을 그대로 적용하면 경합이 심해져서 성능이 떨어진다.
    • PFX 라이브러리들은 바로 이런 상황에 도움이 되도록 설계되었다.
  • 다중 코어 또는 다중 프로세서를 활용하는 프로그래밍을 병렬 프로그래밍(parallel programming)이라고 부른다. 병렬 프로그래밍은 그보다 더 넓은 개념인 다중 스레드 적용(multithreading)의 일부이다.

Continue reading

C# 6.0 완벽 가이드/ 고급 스레드 기법

동기화 개요

  • 동기화(synchronization)란 동시에 실행되는 작업들이 예측 가능한 최종 결과를 내도록 그 작동을 조율하는 것을 말한다. 동기화는 여러 스레드가 같은 자료에 접근할 때 특히나 중요하다. 그런 코드를 작성할 때는 뭔가를 빼먹거나 잘못 구현하기가 놀랄만큼 쉽다
  • 가장 간단하고 유용한 동기화 도구는 14장에서 설명한 연속(continuation) 기능과 작업 조합기(task combinator)일 것이다. 동시적 프로그램을 다수의 비동기 연산들이 연속 작업 객체들과 조합기들로 연결된 구조로 만들면 잠금과 신호 전달의 필요성이 줄어든다.
    • 그렇지만 저수준 수단들을 동원해야 하는 경우도 여전히 존재한다.
  • 동기화 수단들은 크게 다음 세 부류로 나뉜다.
    • 독점 잠금
        • 독점 잠금(exclusive locking)은 한 번에 단 하나의 스레드만 어떠한 활동을 수행하거나 코드의 한 부분을 실행하게 만드는 수단이다. 독점 잠금은 여러 스레드가 서로 간섭하지 않고 공유 상태에 접근해서 상태를 변경할 수 있게 하는데 주로 쓰인다.
      • C#의 독점 잠금 수단으로는 lock과 Mutex, SpinLock이 있다.
    • 비독점 잠금
      • 비독점 잠금(nonexclusive locking)은 동시성을 제한하는 수단이다. 비독점 잠금 수단으로는 Semaphore(Slim)과 ReaderWriterLock(Slim)이 있다.
    • 신호 전달
      • 신호 전달(signaling)은 다른 스레드로부터 하나 또는 여러 개의 통지를 받을 때까지 한 스레드의 실행을 차단하는 수단이다.
      • 신호 전달 수단으로는 ManualResetEvents(Slim), AutoResetEvent, CountdownEvent, Barrier가 있다. 처음 셋을 이벤트 대기 핸들(event wait handles)이라고 부른다.
  • 비차단 동기화(nonblocking synchronization) 수단들을 이용해서 잠금 없이 공유 상태에 대한 동시적 연산을 수행하는 것도 가능하다.(까다롭긴 하지만)
    • 비차단 동기화 수단으로는 THread.MemoryBarrier, Thread.VolatileRead, Thread.VolatileWrite.volatile 키워드, Interlocked 클래스가 있다.

Continue reading

C# 6.0 완벽 가이드/ 보안

  • .NET에서 권한(permission)은 운영체제가 강제하는 것과는 독립적인 하나의 보안 계층을 제공한다. .NET Framework에서 권한의 주된 용도는 다음 두 가지 이다.
    • 모래상자 적용
      • 부분적으로 신뢰된 .NET 어셈블리가 수행할 수 있는 연산 종류를 제한한다.
    • 권한 부여(인가)
      • 누가 무엇을 할 수 있는지를 제한한다.
  • .NET의 암, 복호화(cryptography) 기능은 고가 자료의 저장 및 교환, 정보 유출 방지, 메시지 위변조 검출, 패스워드 저장을 위한 단방향 해시 생성, 디지털 서명 생성 등에 쓰인다.

권한

  • .NET Framework는 권한 기능을 모래상자와 권한 부여(authorization; 인가)에 사용한다.
    • .NET Framework에서 하나의 권한은 어떤 코드의 실행을 조건에 따라 금지하는 일종의 관문으로 작용한다.
    • 모래상자 적용에는 코드 접근 권한들이 쓰이고, 권한 부여에는 신원(identity) 권한들과 역할(role) 권한들이 쓰인다.
  • 이들은 모두 비슷한 모형을 따르지만 사용해보면 그 느낌이 상당히 다르다. 부분적인 이유는 관점이 정반대라는 것이다.
    • 일반적으로 코드 접근 보안에서는 독자의 코드가 신뢰받지 않는 쪽, 즉 불신의 대상(untrusted party)이다.
    • 그러나 신원 및 역할 보안에서는 독자의 코드가 다른 어떤 코드를 신뢰하지 않는 쪽, 즉 불신의 주체(unstrustig party)이다.
    • 대부분의 경우 코드 접근 보안은 CLR이나 호스팅 환경(ASP.NET이나 Internet Explorer)이 독자의 코드에 강제하지만, 권한 부여 관련 보안은 독자의 코드가 다른 어떤 단위에 강제한다(독자의 코드에 함부로 접근하지 못하도록)
  • 권한이 제한된 환경에서 실행될 어셈블리를 작성하는 응용 프로그램 개발자라면 누구나 코드 접근 보안(code access security, CAS)을 숙지할 필요가 있다.
    • 예컨대 어떤 구성요소 라이브러리를 작성해서 판매하는 경우, 고객이 독자의 라이브러리를 SQL Server CLR 호스트 같은 모래상자 안의 환경에서 호출할 수도 있음을 간과한다면 좋은 평가를 받기 어려울 것이다.
  • 다른 어셈블리들을 모래상자 안에서 실행하는 호스팅 환경을 직접 만들 때도 CAS를 숙지할 필요가 있다.
    • 예컨대 서드파티 개발자들이 작성한 플러그인 구성요소를 실행할 수 있는 응용 프로그램을 만든다고 하자. 그런 플러그인들을 권한이 제한된 응용 프로그램 도메인에서 실행하면 잘못된 플러그인 때문에 응용 프로그램이 불안정해지거나 보안이 손상될 확률이 줄어든다.
  • 신원/역할 보안은 주로 중간층(middle-tier) 서버나 웹 응용 프로그램 서버를 작성할 떄 쓰인다. 그런 경우 흔히 일단의 역할들을 정해 두고, 서버가 노출하는 메서드마다 그 메서드를 어떤 역할의 구성원들이 호출할 수 있는지 설정한다.

Continue reading

C# 6.0 완벽 가이드/ 동적 프로그래밍

DLR(동적 언어 런타임)

    • C#은 DLR(dynamic language runtime; 동적 언어 런타임)에 의존해서 동적 바인딩(dynamic binding)을 수행한다.
    • 이름이 주는 느낌과는 달리 DLR은 CLR의 동적 버전이 아니다. DLR은 System.Xml.Dll 같은 다른 모든 라이브러리와 마찬가지로 그냥 CLR 위에 놓인 하나의 라이브러리이다. DLR의 주된 역할은 정적 형식 언어와 동적 형식 언어 모두에서 동적 프로그래밍의 통합을 위한 실행시점 서비스들을 제공하는 것이다.
      • DLR 덕분에 C#이나 VB, IronPython, IronRuby 같은 여러 언어는 동적으로 함수를 호출할 때 동일한 규약을 따른다.
      • 결과적으로 이 언어들은 같은 라이브러리를 공유할 수 있으며, 다른 언어로 작성된 코드를 호출할 수 있다.
    • 또한 .NET에서 새로운 동적 언어를 작성하기가 비교적 쉬운 것도 DLR 덕분이다. 동적 언어를 작성할 때 IR 코드를 직접 산출하는 코드를 작성하는 대신, 표현식 트리를 다루는 코드를 작성하면 된다.
    • 더 나아가서 DLR은 모든 소비자가 호출 사이트 캐싱(call-site caching)의 혜택을 받게 한다. 호출 사이트 캐싱은 동적 바인딩 과정에서 잠재적으로 비싼 멤버 환원 결정을 되풀이 하지 않기 위해 DLR이 사용하는 하나의 최적화 기법이다.
  • DLR은 .NET Framework 4.0에서 처음으로 .NET Framework 자체에 포함되었다. 그 전에는 Codeplex에서 따로 내려받아야 하는 라이브러리였다. 지금도 Codeplex 사이트에는 동적 언어 작성자에 유용한 몇 가지 추가 자원이 있다.

Continue reading

C# 6.0 완벽 가이드/ 반영과 메타자료

  • 앞장에서 보았듯이, C# 프로그램 소스 코드를 컴파일하면 어셈블리가 만들어진다. 어셈블리는 컴파일된 코드와 메타자료(metadata) 그리고 기타 자원들로 구성된다. 컴파일된 코드와 메타자료를 실행시점에서 조사하는 것을 가리켜 반영(reflection)이라고 한다.
  • 어셈블리 안에 담긴 컴파일된 코드에는 원 소스 코드의 내용이 거의 다 들어 있다. 지역변수 이름이나 주석, 전처리기 지시문 등의 일부 정보는 컴파일 과정에서 사리지지만 그 나머지의 상당 부분은 반영 기능을 이용해서 접근할 수 있다.
    • 실제로 반영 기능을 이용해서 역컴파일러(decompiler)를 작성하는 것도 가능하다.
  • .NET Framework가 제공하는 그리고 C#을 통해서 노출되는 여러 서비스(동적 바인딩, 직렬화, 자료 바인딩, Remoting 등)는 메타자료가 있어야 작동한다.
    • 독자가 작성하는 프로그램 역시 메타자료를 활용할 수 있으며 심지어는 커스텀 특성을 이용해서 메타자료에 새로운 정보를 추가할 수도 있다.
    • 반영 API는 System.Reflection 이름공간에 들어 있다. System.Reflection.Emit 이름공간에 있는 클래스들을 이용하면 새 메타자료와 실행 가능한 IL(Intermediate Language; 중간 언어) 코드를 동적으로 생성하는 것도 가능하다.
  • 이번 장에서 뭔가를 ‘동적으로’ 수행한다는 것은 형식 안전성이 오직 실행시점에서만 강제되는 어떤 작업을 반영 긴으을 이용해서 수행하는 것을 뜻한다.
    • 구체적인 메커니즘과 기능성은 다르지만 원칙적으로 이는 C#의 dynamic 키워드를 통한 동적 바인딩과 비슷하다.
    • 둘을 비교하자면 동적 바인딩이 반영 기능보다 훨씬 사용하기 쉽다. 그리고 동적 바인딩은 동적 언어 상호운용성을 위해 DLR(Dynamic Language Runtime)을 활용한다.
    • 반영은 사용하기가 비교적 번거롭고 오직 CLR만 고려한다. 그러나 CLR로 할 수 있는 것의 관점에서 본다면 좀 더 유연하다.
    • 예컨대 반영 기능을 이용해서 형식들과 멤버들의 목록을 얻을 수 있고, 형식의 이름을 문자열로 지정해서 그 인스턴스를 생성할 수 있으며, 즉석에서 어셈블리를 구축할 수 있다.

Continue reading