참고 : 
본 글은 MSDN에 올라온 "C++: The Most Powerful Language for .NET Framework Programming"의 번역문입니다.


C++: .NET 프레임워크 프로그래밍을 위한 가장 강력한 언어
by Kenny Kerr

요점: Visual C++ 2005에서 새로이 소개되는 C++/CLI 언어의 설계와 원리에 대해 탐구해봅니다. .NET 프로그래밍을 위한 가장 강력한 언어 - C++/CLI - 를 이용하여, 파워풀한 .NET 애플리케이션을 만드는 데에 이 칼럼 내용을 도움되길 바래용~

들어가면서

Visual C++ 팀은 많은 시간을 들여가며, 사용자의 의견을 듣고, .NET과 C++로 작업을 하였으니..그 결과 Visual C++ 2005에서의 CLR에 대한 지원을 재설계하기로 맘먹었어. 이 재설계된 물건을 가리켜, C++/CLI라고 부르니, 이는 CLR 타입을 사용하고 제작하는 데 있어 좀더 자연스러운 구문(syntax)을 제공하는 데 초점을 모은 결과야. 요 칼럼에서는 이 새로운 구문을, CLR에 가장 밀접하게 관계된 언어인 C#과 Managed C++와 비교해볼 것이야. 또한, 적당한 곳에서 네이티브 C++와 유사한 개념에 대해서도 보여줄 것이야...


CLI(Common Language Infrastructure)는 MS .NET 기술을 근간으로한 명세서들을 모아논 것이니, CLR(Common Language Runtime)은 CLI를 구현해 놓은 것이야. C++/CLI 언어의 설계는 이 CLI를 자연스리 C++이 지원하는 데에 목표로 잡았고, Visual C++ 2005 컴파일러는 CLR에 대한 C++/CLI를 구현한 것이야.
여기에는 두 가지 중대한 사항이 있는데, 첫 번째는 Visual C++이 CLR을 타깃으로하는 가장 하위 수준의 언어라는 점(MSIL보다도), 두 번째는 .NET 프로그래밍이 원래의 C++ 프로그래밍 만큼이나 자연스러워져야 한다는 점이지.

이 칼럼은 C++ 프로그래머를 위해 쓰여진 것이니, C#이나 VB.NET에서 C++/CLI로 넘어오라고 말하진 않겠어. 만일 네가 C++를 사랑하고, C#의 생산성을 유지하는 동시에 C++ 고유의 능력 모두를 이용하고 싶다면, 이 칼럼은 그야말로 네게 딱!인 칼럼이야. 뿐만 아니라, 어떻게 해서 Visual C++ 2005를 통해, 좀더 우아하고도 효율적인 .NET 코드를 작성하게 만들 수 있는지에 초점을 맞출 것이야.

객체 생성

CLR은 두 가지 타입, 즉 값 타입과 참조 타입을 정의하지. 값 타입은 할당과 제어의 효율성을 위해서 설계되었지. 이 타입은 C++의 내장 타입처럼 동작하고, 네 자신만의 타입으로도 만들 수있는 것으로서, Bjarne Stroustrup은 이를 concrete 타입이라고 불러. 반면 참조 타입의 설계는 네가 객체 지향 프로그래밍에서 기대하는 모든 특징, 즉 클래스 계층체(hierarchy)와 이에 따라붙는 여러 가지의 것들(파생 클래스, 가상 함수 등)을 제공하는 데 목적을 두었지. 뿐만 아니라 이 참조 타입은 CLR을 통해, 가비지 콜렉션이라 부르는 자동 메모리 관리같은 런타임 기능도 추가로 제공하지. CLR은 이들 두 타입에 대해 세부적인 런타임 타입 정보도 함께 제공하는데, 이 기능을 가리켜 리플렉션(reflection)이라고 불러.

값 타입은 스택에 할당되고, 참조 타입은 managed 힙에 할당되는데, 이 힙은 CLR의 가비지 콜렉터(GC)가 관리해. 네가 C++로 어셈블리(assembly)를 개발한다면, 네이티브 C++ 타입을 CRT 힙에 할당할 수 있어. 마치 네가 언제나 그래왔던 것처럼. 장래에는 말야, Visual C++ 팀이 네이티브 C++ 타입을 이 managed 힙에도 할당할 수 있도록 만들 것이야. 결국, 가비지 콜렉션은 네이티브 C++에서도 똑같이 매력적인 놈이 되는 것이쥐.

네이티브 C++에서는 객체가 생성될 장소를 고를 수 있었지? 어떤 타입도 스택이나 CRT 힙에 할당될 수 있어.
// 스택에 할당된다. std::wstring stackObject; // CRT 힙에 할당된다. std::wstring* heapObject = new std::wstring;
보다시피, 객체 할당될 장소는 타입 각각에 대해서 독립적으로 지정할 수 있고, 이 선택권은 전적으로 프로그래머에게 달려있지. 게다가, 할당 구문 또한 스택과 힙의 경우가 완전히 다르쥐.

하지만, C#에서는 값 타입을 스택에다가 생성하고, 참조 타입은 힙에다가 생성해. 아래에 보면, System.DateTime 타입 작성자가 이 타입을 값 타입으로 선언하였어. 
// 스택에 할당된다. System.DateTime stackObject = new System.DateTime(2003, 1, 18); // managed 힙에 할당된다. System.IO.MemoryStream heapObject = new System.IO.MemoryStream();
보다시피, 객체가 스택과 힙 중 어디에 할당되는지 선언만 갖고는 알 길이 없어. 이 선택권은 전적으로 그 타입의 작성자와 런타임에게만 주어진 것이쥐.

C++을 위한 Managed Extensions, 짧게 불러서 Managed C++(MC++)은 네이티브 C++ 코드와 Managed 코드를 짬뽕할 수 있는 능력을 지녔어. 이 MC++은 C++ 표준 규칙을 따르면서도, CLR 구조물의 모든 기능을 지원하도록 C++을 확장한 것이쥐. 하지만 불행하게도, 확장물이 너무 많기 때문에, C++에서 Managed 코드를 작성하기란 여간 힘든 일이 아니야.
// 스택에 할당된다. DateTime stackObject(2003, 1, 18); // managed 힙에 할당된다. IO::MemoryStream __gc* heapObject = __gc new IO::MemoryStream;
이를 보면, 값 타입을 스택에 할당하는 예는 C++ 프로그래머에겐 매우 평범한 모습이야. 하지만, managed 힙의 예를 보면, 꽤나 이상스럽게 보이지. __gc는 MC++에서 추가된 키워드 중 하나야. MC++은 네가 뭘 의도하려는지를 추론해낼 수 있기 때문에, 위의 예는 아래와 같이 __gc 키워드 없이 재작성할 수 있어. 아래는 기본 규칙으로 알려진 것이야. 
// managed 힙에 할당된다. IO::MemoryStream* heapObject = new IO::MemoryStream;
위 예는 훨씬 더 네이티브 C++처럼 보이지? 하지만, 문제는 heapObject가 진짜 C++ 포인터가 아니라는 데 있어. C++ 프로그래머는 포인터를 정적인 값으로 생각할 터이지만, 가비지 콜렉터는 객체를 메모리에서 맘대로 옮기걸랑. 또 다른 문제가 하나 더 있는데, 이 코드만 봐서는 위 객체가 네이티브 힙에 할당되는지, managed 힙에 할당되는지를 알 수가 없다는 것이야. 아마도 너는 이 타입이 (어디에 할당될지를 알도록) 우짜 작성되었는지를 알아야 할거야. 이토록 C++ 포인터에 의미를 덧붙이는 것은 좋지 않은 아이디어임은 이들 말고도 무진장 많아.

C++/CLI 는 C++ 포인터와 CLR 객체 참조를 구분할 수 있도록 핸들이란 개념을 도입했어. C++ 포인터에 의미를 덧붙이지 않음으로써, 애매모호함을 유발시키는 여러 요인을 제거해버렸지. 게다가, CLR에 대한 지원도 핸들을 도입함으로써 훨씬 더 자연스러워졌어. 예를 들자면 말야, C++에서도 직접적으로 참조 타입에 대한 연산자 재정의(overload)를 할 수 있게 되었는데, 이는 핸들에 대해서도 연산자 재정의를 할 수 있게 되었기 때문이쥐. 이러한 이점은 "managed" 포인터에선 불가능한 일이었는데, 왜냐하면 C++은 포인터에 대해서는 연산자를 재정의를 금지하걸랑. 
// 스택에 할당된다. DateTime stackObject(2003, 1, 18); // managed 힙에 할당된다. IO::MemoryStream^ heapObject = gcnew IO::MemoryStream;
여기서도 마찬가지로, 값 타입의 선언에는 놀라울 것이 없어. 하지만 참조 타입의 경우에는 다르쥐. ^ 연산자는 이 값을 CLR 참조 타입에 대한 핸들로 선언해. 핸들의 값이 자동적으로 가비지 콜렉터에 의해 그 핸들이 참조하는 객체로 갱신된다는 의미인, 핸들 트랙은 메모리를 옮겨다닌다는 것이야. 게다가, 핸들은 재-바인딩(rebinding)이 가능하기 때문에, 마치 C++ 포인터처럼 다른 객체를 가리킬 수도 있어. 또하나 염두해둬야 할 점은 gcnew 연산자인데, 이놈은 new 연산자가 놓이는 위치에 사용하게되. 이놈은 객체가 managed 힙에 할당된다는 것을 명시적으로 지시하는 데 사용하지. 이제 new 연산자는 더이상 managed 타입에 대해 재정의되지 않기 때문에, 오직 CRT 힙에다만 객체를 할당할 것이야(물론 네가 너만의 new 연산자를 제공 안한다면 말야.) 너무 C++만 사랑하려고 하지 말어!

결국, 객체 생성에 대해 한마디로 말하자면, 다음과 같이 표현할 수 있어: 네이티브 C++ 포인터는 명백하게 CLR 객체 참조와는 구분되더라.

메모리 관리 vs 리소스 관리

네가 가비지 콜렉터와 같은 어떤 환경을 다루고자 할 때에, 리소스 관리와 메모리 관리를 구분짓는다면 꽤나 편리해질거야. 보통 이 가비지 콜렉터란 놈은 네 객체가 담길 메모리를 할당하고 해제하는 데에 관심이 있걸랑. 해서 이 놈은 네 객체가 갖게 될지도 모를 다른 리소스들, 예를 들어, 데이터베이스 콜렉션이라던가, 커널 객체를 가리키는 핸들에 대해서는 상관을 안하지. 이제 보게될 두 절에서는 말야, 나는 메모리 관리와 리소스 관리에 대해서 각각 썰을 풀어보겠어. 이 놈들은 반드시 이해해두어야 할 매우 중요한 주제거덩.

메모리 관리

네이티브 C++에서는 프로그래머가 메모리 관리에 대해 직접적으로 조작할 수 있어. 스택에 객체를 할당한다는 의미는, 그 객체를 위한 메모리가 특정 함수 호출이 일어났을 때에 할당되고, 그 메모리는 그 함수가 반환될 때에 해제되는 동시에 스택 또한 해제된다는 뜻이지. 한편 동적 객체 할당은 new 연산자를 이용해서 이룰 수 있어. 이 메모리는 CRT 힙에 할당되고, 그 메모리에 대한 해제는 프로그래머가 그 객체를 가리키는 포인터에 대해 delete 연산자를 사용할 때에 명시적으로 일어나. 이같은 메모리에 대한 세밀한 조작은 C++로써 극단적으로 효율적인 코드를 작성할 수 있게되는 이점이 되기도 하지만, 프로그래머가 부주의할 때엔 메모리 누출을 일으키는 요인이 되기도 하지. 물론, 메모리 누출을 피하기 위해 가비지 콜렉터에 완전 의존해서는 안되지만, 이 가비지 콜렉터에 의한 메모리 관리는 CLR이 채용한 방식일 뿐만 아니라, 매우 효과적인 방식이기도 해. 게다가 가비지 콜렉터에 의한 힙에는 이 외에도 메모리 할당 성능이 좋아진다거나 참조(reference)의 격리성 등의 여러 이점이 있지. 이러한 이점 모두는 라이브러리를 통해 C++에서도 이룰 수는 있지만, 그럼에도 불구하고 CLR을 더욱 돋보이게 하는 점은 CLR이 모든 프로그래밍 언어에 통용되는 단일 메모리 관리 프로그래밍 모델을 제공한다는 것이야. C++을 통한 COM 자동화 객체 모델에서, 데이터 타입을 상호운용하고 마샬링하기 위해 필요한 일들을 생각해봐. 이를 생각해보면 단일 메모리 관리 프로그래밍 모델이 얼마나 놀라운 물건인지를 깨닫게 될거야. 여러 프로그래밍 언어를 묶어내는 가비지 콜렉터..이를 갖게되는 일은 엄청난 것이라니깐.

CLR에서의 스택이란 개념은, 효율성에 관한 분명한 이유가 있을 때에 사용하는 값 타입이 할당되는 장소야. 하지만 CLR은 managed 힙에 객체를 할당하기 위한 newobj IL(intermediate language) 명령어도 제공하지. 이 명령어는 C#에서 참조 타입에 대해 new 연산자를 사용할 때를 위해 제공돼. 하지만 CLR에는 C++의 delete 연산자에 해당하는 함수가 없어. 이전에 할당된 메모리는 애플리케이션이 더이상 그 메모리를 참조하지 않아 가비지 콜렉터가 콜렉션을 결정내릴 때에 수거되지.

MC++ 역시, new 연산자가 참조 타입에 대해 적용될 때에 newobj 명령어를 발생시키지만, 이런 managed 또는 가비지 콜렉션이 이루어지는 포인터에 delete 연산자를 사용하는 것은 잘못된 일이야. 이는 분명 일관성을 깨뜨리는 일이기도 하고, C++ 포인터로 참조 타입을 나타내는 것이 왜 나쁜 아이디어인지를 보여주는 또다른 예이기도 해.

C++/CLI는 우리가 이 절에서 객체 생성에 대해 다뤘던 것들과는 달리, 메모리 관리에 대한 영역에서는 어떠한 새로운 것도 제공하지 않아. 하지만 리소스 관리 부분에서는 C++/CLI가 실로 뛰어나지.

리소스 관리

리소스 관리에 관한한, 그 어떤 무엇도 네이티브 C++을 따라올 수가 없어. Bjarne Stroustrup의 "리소스 획득이 초기화"란 테크닉이 정의하는 바는, 각 리소스 타입은 반드시 생성자와 그 리소스를 해제하기 위한 소멸자가 갖춰진 클래스로 모델화되어야 한다는 것이야. 그러면 리소스 타입은 스택에서 지역 객체로 쓰이게 되거나, 복합 타입의 멤버로 쓰일 수 있어. 리소스 타입의 소멸자는 갖고 있던 리소스를 자동으로 해제하는 역할을 하지. 그렇기에 Stroustrup은 C++을 가리켜 "C++은 더 적은 양의 가비지(쓰래기)를 만들어내기 때문에, 근본적으로 가비지 콜렉션에 관한한 최고의 언어이다"라고 말하걸랑.

놀랍게도 CLR에는 리소스 관리에 대해선 런타임에 명시적으로 지원하는 것이 아무 것도 없어. 다시 말해서, CLR에는 C++의 소멸자에 해당하는 개념이 없단 말이야. 그 대신 .NET 프래임워크는 리소스 관리에 대한 한 패턴을 장려하는데, 이른바 IDisposible이란 이름의 핵심 인터페이스 타입이 바로 그것이야. 이 아이디어가 의미하는 바는, 리소스를 감싸는(encapsulate) 타입은 반드시 이 인터페이스의 Dispose 메소드를 구현한다는 것인데, 이로서 호출자는 그 리소스가 더이상 필요치 않을 때에 이 Dispose 메소드를 호출하면 된다는 것이지. 말할 필요도 없이 C++ 프로그래머들은 이를 일보 후퇴한 아이디어로 여길 터인데, 왜냐하면 이들은 리소스 청소(cleanup)가 문제없이 수행되는 것이 기본인 코딩 스타일에 익숙해있걸랑.

리소스 해제 메소드를 반드시 호출해야하는 이러한 어려움으로 인해, 예외에 대해 안전한 코드를 작성하기가 어려워지지. 단순히 코드 마지막 블록에 Dispose 메소드 호출문을 넣는다고 해결되는 것이 아닌데, 예외란 어느 순간에 나타날지 모르는 일이므로 그 객체가 소유한 리소스를 누출할 위험이 여전히 있걸랑. C#에서는 이를 try-finally블록과 using문으로 해결하는데, 이는 예외 상황에 처했을 경우 Dispose 메소드를 호출하는 것이야. 하지만, 이 방법은 더 나빠진 해결법이라, 프로그래머는 이들 문장을 작성하는 것을 반드시 기억해야만 하고, 만약 이를 잊었을 경우에는 코드가 컴파일될지는 몰라도 기본적으로 잠재적인 오류를 갖게되지. try-finally 블록과 using문의 필요성은 소멸자가 없음으로 인해 파생된 귀찮은 부수물이 되는 셈이야.
using (SqlConnection connection = new SqlConnection("Database=master; Integrated Security=sspi")) { SqlCommand command = connection.CreateCommand(); command.CommandText = "sp_databases"; command.CommandType = CommandType.StoredProcedure; connection.Open(); using (SqlDataReader reader = command.ExecuteReader()) { while (reader.Read()) { Console.WriteLine(reader.GetString(0)); } } }
이 이야기는 Managed C++에서도 마찬가지야. 너는 try-finally문을 사용해야 할 터인데, 이 문장은 MS가 C++에 확장시킨 것이지. MC++에는 C#의 using문에 해당하는 놈이 없지만, 간단하게 using 템플릿 클래스를 작성함으로써 손쉽게 대처할 수 있어. 이 클래스는 GCHandle을 감싸고, 소멸자에서 managed 객체의 Dispose 메소드를 호출하지. 
Using<SqlConnection> connection(new SqlConnection (S"Database=master; Integrated Security=sspi")); SqlCommand* command = connection->CreateCommand(); command->set_CommandText(S"sp_databases"); command->set_CommandType(CommandType::StoredProcedure); connection->Open(); Using<SqlDataReader> reader(command->ExecuteReader()); while (reader->Read()) { Console::WriteLine(reader->GetString(0)); }
C++가 전통적으로 리소스 관리에 대해 강력하게 지원해왔다는 점을 고려하면, C++/CLI가 리소스를 가볍게 처리할 수 있도록 설계한 것은 당연한 일이야. 먼저, 리소스를 관리하는 클래스 작성에 대해 생각해보기로 하지. CLR을 타깃으로한 대부분의 언어가 갖는 부담은 Dispose 패턴을 정확하게 구현해야 한다는 점이야. 이를 구현하는 것은 네이티브 C++에서 소멸자를 구현하는 것 만큼 쉬운 일이 아니지. Dispose 메소드를 작성할 때에는 기반 클래스에 Dispose 메소드가 있다면, 반드시 그 메소드를 호출해야되. 게다가, 그 클래스의 Finalize 메소드를 구현하기로 맘먹었다면, 동시(concurrent) 접근에 대해서도 신경써야 하는데, 왜냐하면 Finalize 메소드는 독립된 쓰래드에서 호출되걸랑. 뿐만 아니라, managed 리소스를 해제하는 데에도 신경써줘야 하는데, 특히나 일반 애플리케이션 코드가 그러듯 Dispose 메소드가 실질적으로 Finalize 메소드에서 호출될 경우에는 말야.

C+ +/CLI라고 이런 모든 부담을 날려버리는 것은 아냐. 하지만 C++/CLI에는 이를 위해 도움이 될만한 요소을 제공하지. 이놈이 무엇을 제공하는지 살펴보기 전에, 먼저 오늘날의 C#과 MC++이 사용하는 접근법에 대해서 간단하게 복습해보기로 하자구. 이 예제는 BaseIDisposable에서 파생된다고 가정하고 있어. 만약 그렇지 않다면, Derived 클래스는 Dispose 메소드를 구현할 필요가 없을거야. 
class Derived : Base { public override void Dispose() { try { // managed 리소스와 unmanaged 리소스를 해제한다. } finally { base.Dispose(); } } ~Derived() // Object.Finalize 메소드를 구현/오버라이드한다. { // 오직 unmanaged 리소스만 해제한다. } }
Managed C++에서도 별반 다를 바가 없어. 소멸자는 실제론 Finalize 메소드처럼 보이지. 컴파일러는 try-finally 블록을 적절하게 넣고 기반 클래스의 FInalize 메소드를 호출하기 때문에, C#과 MC++에서는 상대적으로 Finalize 메소드를 작성하기가 쉽지만, 분명 훨씬 더 중요한 Dispose 메소드를 작성하는 데 있어서는 아무 도움도 주지 않아. 프로그래머는 종종 오직 리소스를 해제하는 목적만이 아닌, 어떤 스코프의 끝에서 실행될 모종의 코드를 갖기 위해, Dispose 메소드를 의사-소멸자(pseudo-destructor)로 사용하지.

C++/CLI는 Dispose 메소드를 참조 타입에 대한 지역 "소멸자"로 만듦으로써, 이 메소드를 중요히 여겨. 
ref class Derived : Base { ~Derived() // IDisposable::Dispose 메소드를 구현/오버라이드한다. { //managed 리소스와 unmanaged 리소스를 해제한다. } !Derived() //Object::Finalize 메소드를 구현/오버라이드한다. { //오직 unmanaged 리소스만 해제한다. } };
이로써 C++ 프로그래머는 좀더 자연스러움을 느끼게 될꺼야. 이제는 언제나 그래왔던 것처럼, 소멸자에서 리소스를 해제할 수 있게 되지. 컴파일러는 IDisposable::Dispose 메소드를 구현하기 위해, 이에 필요한 IL 코드를 만들 것인데, 여기에는 가비지 콜렉터가 그 객체에 해당하는 Finalize 메소드 호출을 막는 일도 포함되. 사실, C++/CLI에서 명시적으로 Dispose 메소드를 구현하는 일은 문법적으로 옳은 일이 아니야. IDisposable에서 상속받는 행동은 컴파일 오류를 유발시킬 거야. 물론, 한번 이 타입이 컴파일되면, 이를 사용할 모든 CLI 언어는, 그 타입을 구현한 언어가 어떤 방법을 사용했던지 간에, Dispose 패턴을 만나게 될거야. C#에서는 Dispose 메소드를 직접 호출할 수 있거나, 마치 그 타입이 C#에 정의되어있는 것처럼, using문을 사용할 수 있어. 하지만 C++에서는 어떨까? 어떻게 하면 객체에 기반한 힙의 소멸자를 호출할 수 있을까? 물론 delete 연산자를 이용해서야! delete 연산자를 핸들에 적용하면 그 객체의 Dispose 메소드가 호출될꺼야. 객체의 메모리는 가비지 콜렉터에 의해서 관리된다는 사실을 떠올려봐. 우리는 지금 논하는 요지는 메모리를 해제하는 데에 있는 것이 아니라, 오직 객체가 담고 있는 리소스를 해제하는 데에 있어. 
Derived^ d = gcnew Derived(); d->SomeMethod(); delete d;
해서, delete 연산자에 넘겨지는 표현식이 핸들이면, 그 객체의 Dispose 메소드가 호출되. 만약 그 참조 타입에 연결된 루트(root)가 더이상 없다면, 가비지 콜렉터는 특정 시점에 그 객체의 메모리를 모으는(콜렉션) 데에 자유로워지지. 그 표현식이 네이티브 C++ 객체라면, 메모리가 힙에 반환되기 전에 그 객체의 소멸자가 호출되.

분명 객체의 수명 관리 구문은 더욱 네이티브 C++에 가까워졌지만, delete 연산자를 호출하는 데 있어 기억해야할 오류를 유발하기 쉬운 점이 남아 있긴 하지. C++/CLI를 이용하면 참조 타입에 대해서도 스택의 의미론(semantic)을 채용할 수 있어. 이 말이 의미하는 바는, 스택에 객체를 할당하는데 이용하는 구문을 참조 타입에 대해서도 사용할 수 있다는 것이야. 컴파일러는 네가 C++에서 기대할 의미론을 제공하는 데에도 신경쓸거야. 뿐만 아니라, 컴파일러는 CLR의 요구, 즉 실질적으론 그 객체가 managed 힙에 할당되야 한다는 것도 만족시켜야 해. 
Derived d;
여기서, d가 스코프를 벋어나게 되면, dDispose 메소드가 호출되어 자신의 리소스를 해제할꺼야. 또한 이 객체는 실제론 managed 힙에 할당되기 때문에, 가비지 콜렉터는 알아서 적당한 시간에 그 리소스를 해제하겠지. 위의 ADO.NET 예제는 C++/CLI로 다시 작성하면 아래처럼 돼. 
SqlConnection connection("Database=master; Integrated Security=sspi"); SqlCommand^ command = connection.CreateCommand(); command->CommandText = "sp_databases"; command->CommandType = CommandType::StoredProcedure; connection.Open(); SqlDataReader reader(command->ExecuteReader()); while (reader.Read()) { Console::WriteLine(reader.GetString(0)); }


신고
Posted by 어쨌건간에