'C# 코딩 연습'에 해당되는 글 7건

  1. 2009.11.07 C# 코딩 연습, 분할 정복과 이벤트
  2. 2009.11.01 C# 코딩 연습, 객체의 상속과 포함 (이벤트 코드 생성기의 HistoryManager 클래스의 소스 분석을 겸하여)
  3. 2009.05.20 C# 코딩 연습, 멀티 쓰레드와 이벤트
  4. 2009.05.07 C# 코딩 연습 - 크로스 스레드와 Control.Invoke 4
  5. 2008.03.14 C# 코딩 연습 – 번외, 인터페이스의 명시적 구현 3
  6. 2008.03.12 C# 코딩 연습 - 컬렉션, 序 3
  7. 2008.01.28 C# 코딩 연습 - 대리자와 이벤트 6

C# 코딩 연습, 분할 정복과 이벤트

C# 코딩 연습 2009. 11. 7. 03:35

오늘은 OOP의 이론 중의 하나인 분할 정복에 대해서 (예제를 만들어 보며) 이야기 해보겠습니다.

아시다시피 ‘분할 정복’이란 커다란(복잡한) 문제를 작은(단순한) 문제들로 나눈 후, 나뉜 작은 문제들의 해를 구한 다음에,  이를 다시 결합하여 원래 문제의 해를 구하는 방법을 말합니다.

이는 OOP에서 말하는 캡슐화의 기반이 됩니다.

즉 수 많은 작업(메서드)이 있더라도 이를 몇 개의 클래스로 묶어 클래스 내부에서 처리하도록 하면(단순해진 작은 문제의 해를 구함), 외부에서는 각 클래스가 하는 일에 대해 구체적으로 알 필요가 없이 클래스 간의 통신만 관리하는(작은 문제들의 해를 결합) 식으로 문제를 단순화시킬 수 있다는 것입니다.

여기서 중요한 것은 클래스 간의 ‘통신’(OOP 용어로는 메시지 전달) 입니다. 어떤 작업을 처리하도록 요청 받은 클래스(이하 A)는 그 결과를 요청한 클래스(이하 B)에게 알려줘야 하는데, 이에는 두 가지 방법이 있습니다.

B가 A에게 물어보는 방법과 A가 스스로 B에게 알려주는 방법이 그것인데, 전자의 경우에는 B가 A의 메서드나 속성을 호출하여 반환값(혹은 속성값 또는 메서드의 out 매개변수 등)을 얻는 것이며, 후자는 B가 A의 이벤트에 대해 이벤트 핸들러를 설정해 두면 후에 B가 A의 이벤트 핸들러를 호출하는 방법입니다.

물론 전자의 방법이 좀 더 직관적이고 사용이 간편하지만, 결정적인 한계가 있습니다. 바로 B가 얻고자 하는 값이 결정되는 시점을 A만이 알고 있는 경우가 있을 수 있다는 것입니다. 그래서 이런 경우에는 B가 A의 메서드(값을 물어보는)를 호출하는 것이 아니라, A가 B의 메서드(값을 알려주는)를 호출하는 식으로 구현이 되어야 합니다.

즉 호출의 방향이 B –> A 에서 A –> B 순으로 반대가 됩니다. (참고로, 그래서 C/C++ 에서는 ‘콜백 함수’ 라는 용어를 사용하며 이는 함수(를 가리키는) 포인터로 구현됩니다.)

정리를 하자면, 복잡한 문제를 단순화 시키기 위해서는 여러 개의 클래스로 나누어 구현을 할 필요가 있는데, 이 때 클래스 간의 통신에 이벤트가 중요한 역할을 한다는 것입니다.


말로 설명하자니 다소 추상적인 느낌이 나는데, 그럼 실제로 예제를 만들어 보면서 분할 정복의 의미에 대해서 생각을 해보지요.

이번 연습에서 만들 프로그램은 사용자 목록 관리입니다.

상단에서 각 조건을 입력하고 검색 버튼을 누르면, 가운데 그리드에 해당하는 사용자의 목록이 나타나고, 하단에는 그리드에서 선택된 사용자의 상세 항목이 나타납니다. 추가 / 수정 / 삭제 기능이 더해지면 전형적인 업무용 프로그램의 형태라고 할 수 있겠네요.

어떤 식으로 구현하면 좋을까요?

간단하니까 윈폼 클래스 하나에다 모든 코드를 추가할 수도 있겠지만, 예제처럼 간단한 형태가 아니라 아주 복잡한 폼이라면 폼 하나의 코드가 매우 길어질 수 있습니다. (제 경험으로는 윈폼 하나의 코드가 천 라인이 넘으면 코드를 장악하기가 어려웠습니다.)

대신에 검색 / 그리드 / 상세정보 라는 세 개의 클래스로 나누어 각종 로직을 분담시키는 것은 어떨까요?

검색 클래스를 예로 들자면, 생년월일 체크 박스가 체크되었을 경우에만 DateTimePicker 컨트롤들이 활성화 되는 로직이나, 나이 TextBox에는 자연수만이 입력되어야 한다는 등의 로직은 검색 클래스만이 알고 있으면 될 것입니다. 이를 그리드나 상세정보 클래스가 알 필요는 없지요. 결국 검색 클래스는 사용자가 입력한 검색 조건에 대한 유효성 검사를 한 후에, 그리드 클래스에 검색 버튼이 눌러졌다는 것을 알려주기만 하면 됩니다. 물론 이 때 검색 조건도 같이 알려줘야 하겠지요.

그렇다면, 위의 사용자 목록 폼은 검색과 관련된 로직은 가지고 있지 않아도 되어 코드가 한결 단순해집니다. 바로 분할과 정복이 일어난 것이지요. 물론 검색 클래스를 다른 폼에서 재사용활 수 있다는 추가적인 장점도 생깁니다.


지금부터는 코드를 만들어 봅시다.

사용자 목록 폼, 즉 UI를 만들기 전에 데이터 액세스 레이어를 만들어 놓는 것이 좋겠네요.

물론 여기서는 DB에서 데이터를 가지고 오는 것이 가져오는 척만 하겠습니다. (이를 이른바 mock 클래스라고 하는데, 특히 단위 테스트에서 유용하게 사용됩니다.)

먼저 Employee 엔터티를 정의합니다.

mock 클래스인 EmployeeRepository도 추가합니다.

가운데 singleton 영역의 코드는 다음과 같습니다.

Repository 패턴을 흉내내기 위해 싱글톤으로 구현한 것인데, Repository 패턴이나 싱글톤 등은 본 포스트의 주제가 아니므로 지금은 따로 언급하지 않겠습니다.

다만 Search 메서드의 경우에는, LINQ to Object 를 이용하여 (검색 조건이 0개 부터 n개 일 수 있으므로 쿼리가 정해져 있지 않다는 의미에서) 동적 쿼리를 구현하는 테크닉입니다. LINQ to Object 뿐만 아니라 LINQ to SQL 이나 Entity Framework 에서도 유용하게 사용할 수 있습니다. (LINQ의 특징 중 하나인 ‘지연된 로딩’)


이제 검색 / 그리드 / 상세정보 클래스를 각각 구현할 것인데요, 이들은 모두 사용자 정의 컨트롤(유저 컨트롤)로 구현하는 게 좋을 것 같네요.

먼저 검색 컨트롤부터 추가합니다. 이름은 EmployeeSearchControl 정도가 좋겠네요.

윈폼 디자이너에서 그림과 같이 적당히 디자인 한 후 코드 비하인드 파일에 로직을 추가합니다.

붉은 밑줄이 그인 부분이 중요합니다. 이 컨트롤이 하는 일은 EmployeeRepository의 Search 메서드를 호출하는 것이 아닙니다. 실제 Search 메서드는 이 컨트롤의 컨테이너(사용자 목록 폼)에서 할 것이며, 이 컨트롤은 검색 버튼이 눌러졌다는 메시지를 호출하면서 그때 사용자가 입력한 값들(즉 검색조건)만 전달하면 됩니다.

물론 이 통지는 이벤트를 통해 구현하면 됩니다. SearchButtonClicked 라는 이벤트를 추가하면 되는데, 직접 입력하지 말고 이벤트 코드 생성기를 사용해봅시다.

이벤트 코드 생성기를 실행하고 아래와 같이 입력을 합니다.

이벤트 매개변수에는 Search 메서드가 필요로 하는 인자들이 들어있습니다.

이벤트 코드 생성기가 생성한 코드를 EmployeeSearchControl 에 추가합니다.

그 다음에는 이 이벤트를 발생시키는 코드를 추가해야 하는데, 이는 검색 버튼이 눌러지고 검색 조건에 대한 가공이 끝나 Search 메서드를 호출할 준비가 끝난 다음이 되어야 겠지요.

필수는 아니지만 보너스로 EmployeeSearchControl  클래스에 DefaultEvent 특성을 추가해줍니다.

DefaultEvent는 디자이너에서 EmployeeSearchControl 이 더블 클릭되었을 때 디자이너가 자동으로 추가할 이벤트 핸들러를 지정하는 일을 합니다.


이제 검색 클래스는 구현이 끝났습니다. 이어서 그리드 클래스와 상세정보 클래스를 구현하여야 할 터인데, 이는 지면 관계상 자세한 설명을 생략을 하겠습니다. (구체적인 코드는 첨부 파일을 참조하십시오.)

그리드 클래스의 이름은 EmployeeListControl 인데 디자인은 다음과 같습니다.

그리고 선택된 사용자가 변경될 때 발생하는 CurrentEmployeeChanged 와 사용자가 더블 클릭될 때 발생하는 EmployeeDoubleClicked 라는 두 개의 이벤트를 추가합니다.


상세정보 클래스는 EmployeeControl 라는 이름으로 추가하고 아래와 같이 디자인을 합니다.

EmployeeControl 은 아무런 이벤트도 가지고 있지 않습니다.


자 이제 위에서 작성한 유저 컨트롤들을 조립할 차례입니다.

사용자 목록 폼을 EmployeeListForm 라는 이름으로 추가한 후 디자이너에서 세 개의 유저 컨트롤을 각각 올립니다.

붉은 글씨는 각 컨트롤의 이름입니다.

사용자 목록 폼이 하는 일은 각 유저컨트롤 간의 통신을 중개하는 일입니다. 먼저 검색 컨트롤의 SearchButtonClicked 이벤트 핸들러를 추가합니다.

검색 컨트롤의 SearchButtonClicked 의 매개변수(즉 검색 조건)을 받아 EmployeeRepository.Search를 호출하여 검색을 수행한 후 그 결과를 그리드 컨트롤에 넘겨주고 있습니다.

위 이벤트 핸들러가 실행되고 나면 그리드 컨트롤에는 검색된 사용자의 목록이 나타나게 되겠네요.


이번에는 그리드 컨트롤의 이벤트 핸들러를 추가해 봅시다.

그리드에서 현재 사용자가 변경되면 상세정보 컨트롤에 이를 알려주고, 사용자가 더블 클릭되면 사용자 폼을 띄우는 일을 하고 있습니다.

(사용자 폼에 대한 코드는 여기서 살펴보지 않겠습니다. 첨부된 프로젝트 소스를 참고하십시오.)


정리하자면 사용자 목록 폼의 코드는 세 개의 이벤트 핸들러를 구현하는 것이 전부입니다.

검색 클래스 등을 만들지 않고 사용자 목록 폼에 모든 코드를 구현하는 것에 비하면 훨씬 간략하고 구조화되어 있기 때문에 재활용성이 높아 유지보수하기가 수월합니다.

또한 각 유저 컨트롤은 다른 폼에서도 사용할 수 있습니다. 여기서는 언급하지 않았지만 상세정보 컨트롤의 경우에는 사용자 목록 폼과 사용자 폼에서 두 번 사용되었습니다.


분할 정복의 장점은 바로 이런 것입니다.

복잡한 문제가 여러 개의 단순한 문제로 나누어 지기 때문에 해결하기가 쉽고, 이 과정에서 나누어진 단순한 문제는 다른 곳에서 재사용 될 수 있습니다.

OOP에서 클래스를 모델링 한다는 것은 바로 이렇게 문제를 나누어 가는 과정이라고도 할 수 있겠습니다.


말할 필요도 없겠지만, OOP는 프로그래밍 언어사에 있어 최고의 발명 중 하나이며, 발명된 지 한 세대가 지난 지금까지도 여전히 유용한 개념입니다.

세상 많은 일들이 그런 것처럼 OOP를 익히는 데도 왕도는 없는 것 같습니다. 글을 잘 쓰기 위해서는 많이 읽고 많이 쓰고 많이 생각해야 한다고 합니다. 좋은 프로그램을 짜기 위해서도 마찬가지가 아닐까 싶습니다. 좋은 코드를 많이 읽고, 좋은 코드를 많이 연습하고, 좋은 코드에 대해 많이 생각하는 것이 바로 OOP, 나아가서는 프로그래밍의 왕도가 아닐까 싶습니다.

예제 소스 :

:

C# 코딩 연습, 객체의 상속과 포함 (이벤트 코드 생성기의 HistoryManager 클래스의 소스 분석을 겸하여)

C# 코딩 연습 2009. 11. 1. 23:41

졸작 이벤트 코드 생성기 1.4 버전에 추가된 히스토리 기능은 이전에 생성한 이벤트 이름과 매개변수 쌍을 기억하는 기능입니다.

이 기능을 어떤 식으로 구현하는 것이 좋을까요?

아마도 클래스가 있는 언어를 사용한다면 히스토리를 관리하는 클래스를 만들어 관련된 모든 로직을 캡슐화하는 것이 정석일 것입니다.

구체적으로 이 클래스가 할 일을 정리해 본다면,

  1. 히스토리를 추가한다. 다만 추가할 수 있는 갯수에는 제한이 있어 최대값에 도달하면 선입선출 식으로 제거된다.
  2. 추가된 히스토리를 삭제할 필요는 없다.
  3. 히스토리 목록을 저장소(여기에서는 파일 시스템)에 저장하고 저장소에서 불러온다.
  4. 윈폼의 BindingSource 객체의 DataSource로 지정될 수 있다. (즉 히스토리 목록을 열거할 수 있다.)
  5. 프로그램 전반에 걸쳐 단 한 개의 인스턴스가 생성된다.

각각의 요구사항에 대한 해결 방법을 (간단한 것 부터) 찾아봅시다.

5 <- 이건 그냥 싱글톤 패턴으로 바로 해결이 가능하겠네요.

4 <- ‘열거’ 라는 말에서 착상할 수 있듯이 IEnumerable<T> 인터페이스를 구현하면 역시 간단하게 해결할 수 있겠습니다.

3 <- 히스토리 목록이 직렬화가 가능하도록 하면 되겠습니다. Serializable 특성을 붙이거나 ISerializable 인터페이스를 구현하거나 직접 (역)직렬화 로직을 구현하는 방법이 있겠습니다.

여기서는 닷넷 프레임웍 3.0에 추가된 DataContract와 DataMember 특성을 사용하겠습니다.


이제 제일 중요한 1번과 2번이 남았는데요.

직관적으로 결국 이 클래스는 바로 히스토리의 목록을 조작하는 클래스라는 것을 알 수 있는데, 그렇다면 문제는 히스토리의 목록을 어떻게 표현하느냐로 좁혀집니다.

즉, 히스토리의 목록으로 어떤 자료구조를 사용하느냐, 닷넷 프레임웍의 용어로 이야기하자면 어떤 컬렉션을 선택하느냐의 문제에 다름 아닙니다.

(자료구조와 컬렉션에 대해서는 졸고를 참고하십시오.)


그런데 사실 이 문제는 별로 고민할 필요가 없는 것이, 이 프로그램처럼 윈폼의 데이터바인딩 메커니즘과 함께 사용하는 경우라면 거의 대부분의 경우 List<T> 혹은 BindingList<T>가 정답이 될 것입니다.

(드문 경우긴 하지만 IList<T>나 IBindingList<T> 인터페이스를 구현한 클래스를 직접 만들어 사용하는 경우도 있긴 합니다.)

물론 List<T>는 윈폼 뿐만 아니라 닷넷 베이스 클래스 라이브러리를 통틀어 가장 많이 사용되는 자료구조가 아닐까 싶은데요.

STL과 자바에서는 ‘벡터’라고 불리는 이 가변배열은 자료구조의 기본이면서도 정말 유용합니다.

혹시 아직도 습관적으로 배열이나 ArrayList를 사용하고 있다면 List<T>를 대신 사용할 것을 고려해 보시기 바랍니다.


히스토리의 목록으로 List<T>를 사용하기로 결정했다면 이제 마지막 문제, 오늘의 주제이기도 한 문제가 남았습니다.

List<T> 클래스를 이용하여 히스토리 목록을 구현할텐데, 어떤 방식으로 이용하느냐는 것입니다.


OOP에 있어서 한 클래스가 다른 클래스를 이용(재사용)하는 방법에는 두 가지가 있습니다.

이른 바 is-a, has-a 관계라고도 일컬어지는 상속포함(또는 합성 혹은 위임)이 그것입니다.

상속은 한 클래스가 다른 클래스의 (private 멤버를 제외한) 멤버를 그대로 물려받는 것이며

반면에 포함은 한 클래스가 다른 클래스의 인스턴스를 멤버로 가지고 있는 경우를 말합니다.

물론 상속과 포함은 각각의 장단이 있긴 때문에 상황에 따라 선택을 해야할 문제이긴 하지만, OOP 언어의 대가들은 대체로 포함 방식을 사용할 것을 권장하고 있습니다.

그 근거 중 가장 대표적인 것이 상속의 경우에는 두 클래스 간에 (보다) 강력한 결합이 발생한다는 것인데, 예를 들면 이렇습니다.

보시다시피 A 클래스의 Foo 메서드가 Foo2로 이름이 변경되면 B.Foo()를 호출하던 코드는 이제 수정을 하여야 합니다.

반면에 포함의 경우에는 이런 문제가 발생하지 않습니다.

A 클래스가 변경되더라도 B 클래스에서 A에 대한 위임 부분만 바꾸면 되므로, B 클래스를 사용하는 입장에서는 변함이 없습니다.


워밍업은 이 정도로 하고 이제부터는 본격적으로 히스토리를 관리하는 클래스를 만들어 보도록 합시다.

클래스의 이름을 HistoryManager 라고 하고, List<T>를 포함하는 형태로 한다면 아래와 같은 클래스를 선언할 수 있겠네요.

아, 아직 지네릭 타입인 T를 결정하지 않았네요.

히스토리의 목록이라고 했으니까 이 T는 히스토리 아이템 타입이 될 것입니다. 히스토리 아이템 타입은 아래와 같이 정의하겠습니다.

이벤트의 이름과 매개변수를 각각 string 형으로 가지고 있는 단순한 클래스이고요.

직렬화를 위해 DataContract 특성과 DataMember 특성을 가지고 있습니다.

이제 HistoryManager의 정의는 다음과 같이 바뀌겠네요.


프로그램 전체에 걸쳐 단 하나의 인스턴스만 생성할테니, 싱글톤 패턴을 추가해봅시다.

싱글톤 패턴은 워낙에 잘 알려져 있으니 따로 이야기하지 않아도 될테구요.

리스트에 저장할 최대 갯수인 MaxHistoryItems와 파일에 저장하기 위한 파일명을 상수로 정의한 것을 기억하여 주십시오.

IsolatedStorageHelper 클래스는 격리된 저장소에 대한 파일 입출력 작업을 담당하는 헬퍼 클래스인데 구체적인 구현은 이벤트 코드 생성기의 소스를 참조하시기 바랍니다. 여기서는 그냥 로컬 파일시스템에서 데이터를 읽어오는 코드 정도로만 생각하셔도 무방합니다.


히스토리 아이템을 추가하는 메서드는 아래와 같이 구현할 수 있습니다.

리스트의 갯수가 최대값에 도달하면 마지막 아이템을 삭제하고, 새로운 아이템을 제일 앞에 삽입하고 있습니다.

여기서 List<T>와 관련하여 한 가지 사족을 달자면, List<T>에는 원소를 추가하는 메서드가 두 개가 있습니다.

Add와 Insert가 그것인데요. Add는 내부 배열의 마지막에 원소를 덧붙이는 반면에, Insert는 배열 가운데에 원소를 삽입합니다.

(그래서 Insert에는 삽입할 위치를 나타내는 인자가 하나 더 있습니다.)

문제는 Insert를 통해 원소가 삽입되면, 내부 배열에서 인덱스가 그 이후인 모든 원소들이 한 칸식 뒤로 밀려나게 된다는 것입니다.

여기서 뒤로 밀려난다는 것은 기술적으로 말하자면, 객체의 이동(즉 메모리의 복사)을 의미하는데, 그 갯수가 많다면 이는 성능상의 오버헤드가 될 수 있습니다.

예를 들어 원소가 만 개인 리스트의 0번째에 삽입을 한다면 만 개의 객체를 이동하여야 하는 것입니다.


그리고 List<T>의 삭제의 경우에도 마지막 원소가 아닌 중간의 원소를 지우면 이후 원소들은 한 칸식 앞으로 이동하는 문제가 있습니다.


결국 위 코드에서는 Insert를 사용하나 Add를 사용하나 이론적으로는 같은 성능을 낸다고 할 수 있겠습니다.


여기서 한 가지 상속과 포함에 관하여 생각해 볼 것이 있습니다.

만일 HistoryManager 클래스가 List<T>를 상속하는 구조였다면 이 코드는 아마도 아래와 같이 작성되었을 것입니다.

Add 메서드의 시그니처가 상속받은 List<T>의 메서드에 맞추어 변경되고, (List<T>.Add가 가상 메서드가 아니기 때문에 new 키워드를 붙여줘야 합니다.)

Count 나 Insert 같은 속성이나 메서드를 가지고 있지 않으므로 상속받은 List<T>의 것을 사용하고 있습니다.

(참고로 HistoryManager를 사용하는 클래스는 Add 메서드 만을 알고 있긴 때문에, 실제로는 Add가 아니라 Insert 이지만 이름은 Add로 하였습니다.)


포함 대신 상속을 사용하면, 히스토리를 추가하는 것은 이런 식으로 가능하겠지만, 2번 요구사항인 ‘삭제를 할 수 없다’ 에서 문제가 생기게 됩니다.

포함의 경우라면, HistoryManager 매니저에 Remove 메서드를 작성하지 않는 한 당연히 외부에서는 이를 호출할 수 없습니다.

하지만 상속이라면, HistoryManager 클래스는 상속받은 List<T>의 모든 공용 속성과 메서드를 노출하므로 HistoryManager의 사용자가 List<T>의 Remove 메서드를 호출하는 것을 막을 방법이 없습니다.

그래서 다음과 같이 런타임에 예외를 발생시키는 어색한 코드라도 추가할 수 밖에 없습니다.


이번에는 히스토리 목록을 파일로 저장하는 (직력화) 하는 코드를 살펴봅시다.

IsolatedStorageHelper 클래스 덕분에 코드가 가능합니다.

여기서 한 가지 주의할 점은, 직렬화의 대상이 되는 객체는 HistoryManager 객체 자체가 아니라 그 필드인 히스토리 목록 (List<HistoryItem> _list) 객체라는 것입니다.

따라서 HistoryManager에는 DataContract 특성을 붙일 필요가 없으며, List<T>의 지네릭 타입인 HistoryItem에 이 특성을 붙여야 한다는 것입니다.

(List<T> 클래스에는 닷넷 프레임웍 내부에서 이미 이 특성이 붙어 있습니다.)


마지막으로 HistoryManager 객체를 열거가능하게 해 봅시다.

제일 위 스크린 샷에서 히스토리 콤보박스의 데이터소스로 HistoryManager 인스턴스를 지정하는 코드를 간략하게 적으면 아래와 같습니다.

물론 이 코드는 런타임에 예외를 발생시킵니다.

HistoryManager가 히스토리 목록을 열거하는 기능을 가지고 있지 않기 때문입니다.


그럼 HistoryManager에 히스토리 목록을 열거하는 기능을 어떻게 추가할 수 있을까요?

간단하게는 필드로 가지고 있는 히스토리 목록을 외부로 바로 노출하는 것입니다.

그럼 위 코드는 아래와 같이 바꾸면 됩니다.

물론 동작은 하겠지만, 이 코드는 OOP의 기본 원칙 중 하나인 캡슐화를 위배해버렸습니다.

즉 히스토리 목록이 외부로 직접 노출되었기 때문에, 이를 통해 외부에서 히스토리 목록에 아이템을 제거하거나 하는 일을 할 수 있게 된 것입니다.


히스토리 목록을 노출하는 것 보다 좋은 방법은 HistoryManager 클래스 자체를 열거 가능한 형으로 만드는 것입니다.

즉 HistoryManager가 IEnumerable<HistoryItem> 인터페이스를 구현하도록 한 후 IEnumerable<HistoryItem>의 GetEnumerator 메서드를 적절하게 구현하는 것입니다.

지네릭 버전이 아닌 IEnumerable 인터페이스는 구현하지 않고, 지네릭 버전의 IEnumerable 인터페이스만 구현하기 위해서는 위 코드 처럼 두 개의  GetEnumerator 메서드를 구현을 하여야 하는데, 둘 중 하나는 암시적으로, 나머지 하나는 명시적으로 구현을 하여야 합니다. (인터페이스의 명시적 구현에 관해서는 역시 졸고를 참고하십시오.)


그럼 이제 HistoryManager는 아래와 같이 콤보박스의 데이터소스로 바로 사용될 수 있습니다.

 


이번 C# 코딩 연습에서는 OOP의 기본 이론 중 하나인 클래스의 상속과 합성에 대해서 다루어 보았습니다.

아시다시피 C#은 (기본적으로) OOP 언어입니다. 그래서 OOP에 대한 깊이 있는 이해가 C#을 제대로 활용하기 위한 첩경이 됩니다.

OOP를 이해하는 데 이 글이 미약한 도움이나마 되셨기를 바랍니다.


우리는 모두 한국의 개발자입니다.

그래서 이 글을 읽는 분의 경쟁력이 곧 이 글을 쓴 저의 경쟁력이기도 합니다.

장인(匠人)으로서의 자부심을 가지고 피나는 용맹정진을 할 것을 (스스로에게) 다짐하고, 또 (여러분에게) 당부합니다.


연습문제

HistoryManager 클래스에 아래와 같은 상황에서 발생할 이벤트를 추가해 보시기 바랍니다.

  1. 히스토리 목록이 추가되려고 함 –> 히스토리 목록의 원소 갯수를 알려주어야 함
  2. 히스토리 목록이 추가되었음 –> 히스토리 목록의 원소 갯수를 알려주어야 함
  3. 히스토리 목록이 파일 시스템에 저장되었음 –> 저장된 파일명을 알려주어야 함

이왕이면 이벤트 코드 생성기를 사용해 보십시오.


연습문제 정답 :

:

C# 코딩 연습, 멀티 쓰레드와 이벤트

C# 코딩 연습 2009. 5. 20. 00:28

이번 C# 코딩 연습은 .NET Tip of The Day에 포스팅 된 이벤트의 올바른 호출이라는 기사에 관한 주해 형식으로 작성해 보았습니다.


위 포스트를 보면, 멀티 쓰레드 환경에서는

와 같이 이벤트를 호출해서는 안되고,

와 같이 코드를 작성해야 한다며 그 이유가 ‘간략’하게 설명이 되어 있습니다.


이번 C# 코딩 연습은 이 ‘간략’한 설명에 대한 설명입니다.

:

C# 코딩 연습 - 크로스 스레드와 Control.Invoke

C# 코딩 연습 2009. 5. 7. 14:05

명작 도서 More Effective C#: 50 Specific Ways to Improve Your C#를 읽다 보니 Control.Invoke를 캡슐화 한 ControlExtensions 이라는 클래스에 대한 이야기가 나왔습니다.

원서에는 멀티 스레딩이나 크로스 스레드 등에 대한 배경 설명 없이 달랑 몇 줄의 코드 정도만 제시되어 있는데 (이것이 바로 이 책의 매력 –반어적 의미 아님- 입니다.),

여기에 앞 뒤 설명을 붙이고, 간단한 예제를 만들어 보았습니다.


매 1초 마다 현재 시각을 표시하는 간단한 시계를 만든다고 합시다.

보통은 System.Timers.Timer 나 System.Threading.Timer, 혹은 System.Windows.Forms.Timer 를 사용하겠지만, 여기서는 Thread.Sleep 메서드를 이용하여 매 1초 마다 시간을 표시하는 방법을 사용해 보겠습니다.

실행하면, 시작하자 마자 바로 화면이 먹통이 되어버리는 걸 알 수 있습니다.

Thread.Sleep 메서드가 현재 메서드(UI가 생성된 메서드)를 잡고 있는 이른바 UI 블로킹이 일어난 것인데요. 이를 해결하기 위해서는 이 부분을 별도의 스레드에서 실행하여야 합니다.

1초를 기다린 후 라벨에 시각을 표시하는 로직이 별도의 메서드(StartNewThread)로 빠졌고, 이 메서드는 이제 메인 스레드(UI 스레드)와는 별개의 스레드에서 실행됩니다.

실행을 해 봅시다. 디버깅하지 않고 시작(CTRL + F5)을 실행하면 (운이 좋다면) 문제 없이 시계가 동작하는 걸 볼 수 있지만, 디버깅 시작(F5)을 실행하면 label1.Text = DateTime.Now.ToString();

에서 InvalidOperationException가 발생합니다.

일명 크로스 스레드 예외라고 하는데요.

label1.Text = DateTime.Now.ToString(); 에서 라벨 컨트롤에 접근을 하려고 하는데, 문제는 이 코드가 실행되는 스레드가 라벨 컨트롤이 생성된 스레드(메인 스레드, UI 스레드)가 아니라는 것입니다.


Control.Invoke를 호출하는 방법과 BackgroundWorker를 사용하는 두 가지 방법이 있을텐데, 대부분의 경우에는 BackgroundWorker 가 좋은 선택이 될 것입니다.

스레드 간에 상태를 공유하고, 진행상황을 보고하고, 작업을 중지하는 등 스레드와 관련한 대부분의 작업이 이미 구현되어 있기 때문에 편리하게 사용할 수 있습니다.

다만 BackgroundWorker에는옥의 티랄까, 한 가지 알려진 버그가 있습니다.

이 포스트의 주제가 BackgroundWorker가 아니고, 또 BackgroundWorker에는 곁다리로 슬쩍 이야기할 수 있는 수준 이상의 논점이 많으니까, 이 포스트에서는 Control.Invoke에 대해서만 이야기하도록 하겠습니다.


Control.Invoke를 호출하여 크로스 스레드 문제를 해결하는 코드는 다음과 같습니다.

먼저 라벨의 InvokeRequired 를 체크하여 Invoke가 필요한지를, 즉 컨트롤에 접근하는 스레드와 컨트롤이 생성된 스레드가  다른 스레드인지를 체크합니다.

굳이 Invoke가 필요하지 않다면 (비록 미미하더라도) 비용이 드는 Invoke를 호출할 필요가 없을 것입니다.

Control.Invoke의 시그니처는 다음과 같습니다.

첫번째 매개변수가 추상 클래스인 Delegate입니다.

따라서 label1.Invoke(new Delegate(DisplayDateTime)); 과 같이 대리자 인스턴스를 생성할 수가 없습니다.

대신 DisplayDateTimeHandler 라는 대리자 형식을 정의한 후 이 인스턴스를 전달하여야 합니다.

이제 실행(디버깅)을 하여 보면 크로스 스레드 문제 없이 잘 동작합니다.


크로스 스레드 문제가 해결되었으니 여기서 포스트가 끝나야 할 것 같지만, 이 포스트를 서야겠다고 생각한 이유는 사실은 지금 부터 시작 합니다.

Control.Invoke를 호출하는 위 코드를 Action 대리자와 확장 메서드를 사용하여 필드에서 사용할 만한 라이브러리로 만들어 봅시다.


닷넷 프레임웍 2.0에 추가된 두 가지 제네릭 대리자를 사용하면 대부분의 경우에는 대리자를 작성할 필요가 없습니다.

Func과  Action 대리자가 그것인데요. Func은 반환값이 있지만 Action은 반환값이 없다는(void) 점 외에는 동일하며, 두 대리자 모두 매개변수가 0개 ~ 4개인 오버로드가 각각 준비되어 있습니다.

(정확하게 이야기하자면, 매개변수가 0개인 Action 대리자의 형은 void Action() 이므로 제네릭 대리자는 아닙니다.)

그래서 매개 변수가 4개가 넘지 않는 시그니처를 가지는 대리자는 이 두 대리자로 표현할 수가 있는 것입니다.

위 코드에서도 DisplayDateTimeHandler 대리자를 따로 정의하지 않고 제네릭 대리자를 사용할 수 있습니다.

반환값이 없고 매개변수도 없으니까, 제네릭이 아닌 Action 대리자를 사용하면 되겠습니다.


이번에는 확장 메서드를 사용하여 위 코드를 캡슐화 해봅시다.

ControlExtensions라는 static 클래스를 만들고 아래와 같은 확장 메서드를 추가합니다.

(여기서는 매개변수가 0개 ~ 1개인 오버로드만 보이는데, 2개 ~ 4개인 오버로드도 정의해두면 편리합니다.)

이제 StartNewThread 메서드는 아래와 같이 간단해 집니다.

 

람다식이나 익명 메서드를 이용하면 DisplayDateTime 메서드도 따로 정의할 필요가 없어 코드가 좀 더 간단해 집니다.


연습 삼아 코드를 약간 고쳐 봅시다.

DisplayDateTime 메서드를 매개 변수를 가지는 형태로 다음과 같이 수정합니다.

그렇다면 이제 라벨의 Invoke 메서드를 호출할 때 현재 시각을 매개 변수로 넘겨야 합니다.

ControlExtensions.InvokeIfNeeded 오버로드 중,

가 호출되는데, 이는 다시 control.Invoke(action, arg); 를 호출합니다.

Control.Invoke의 두 번째 매개변수는 params object[] 이기 때문에, action 대리자의 매개변수의 갯수가 형에 상관없이 호출이 가능합니다.


보너스 : 제가 알렸드렸다고 소문 내지는 마시고, More Effective C# 책이 보고 싶으신 분은 이 링크를 눌러 보세요.

:

C# 코딩 연습 – 번외, 인터페이스의 명시적 구현

C# 코딩 연습 2008. 3. 14. 21:04

인터페이스의 구현에는 두 가지가 있습니다. 이른바 암시적 구현과 명시적 구현이라고 하는데요, 일반적인 경우에는 주로 암시적 구현을 사용하지만 경우에 따라서는 명시적 구현을 사용해야 하는 경우도 있습니다.

먼저 암시적 구현 부터 살펴볼까요? 두 개의 인터페이스가 있다고 합시다.

01 public interface IGunner

02 {

03 void Shoot();

04 }

05

06 public interface ISoccerPlayer

07 {

08 void Shoot();

09 }

코드 1

IGunner는 총을 쏘는(shoot)을 메소드를 정의하고 있고, ISoccerPlayer는 공을 차는(shoot) 메서드를 정의하고 있습니다. 공교롭게 메서드의 이름이 같긴 하지만, 이 두 인터페이스는 서로 전혀 연관이 없습니다. 또한 3번과 8번 라인에서 아무런 접근 지정자가 붙어있지 않은 점도 유심히 보시기 바랍니다. 클래스의 경우 멤버의 접근 지정자가 붙어있지 않다면 private이 생략된 것으로 간주하지만, 인터페이스의 경우에는 public이 생략된 것으로 간주합니다. 아니 간주하는 것이 아니라, 인터페이스의 멤버는 그 성격상public외의 접근 지정자를 가질 수가 없습니다. 따라서 항상 public이기 때문에 인터페이스의 멤버에는 아무런 접근 지정자를 붙일 필요가 없습니다. 오히려 접근 지정자를 붙이면 문법 오류입니다.

이번에는 이들 인터페이스를 각각 구현하는 총잡이와 축구 선수 클래스를 생각해봅시다.

01 internal class Gunner : IGunner

02 {

03 public void Shoot()

04 {

05 Console.WriteLine("빵빵");

06 }

07 }

08

09 internal class SoccerPlayer : ISoccerPlayer

10 {

11 public void Shoot()

12 {

13 Console.WriteLine("");

14 }

15 }

코드 2

인터페이스에 정의된 메서드의 이름이 같기 때문에 이를 구현하는 두 클래스에서의 메서드 이름도 같을 수 밖에 없지만, 그 동작은 전혀 다릅니다. 총잡이는 총을 쏘고, 축구 선수는 공을 차지요. 그리고 3번 라인과 11번 라인에서는 public 접근 지정자가 붙어 있습니다. 인터페이스를 정의할 때는 붙이지 않았던 접근 지정자를 인터페이스를 구현하는 클래스에서는 이렇게 지정하여야 합니다. 인터페이스에서의 접근 지정자가 (생략된) public 이었기 때문에, 클래스에서도 public 이외의 다른 값을 가질 수는 없습니다. (곧 살펴 볼 명시적 인터페이스에는 적용되지 않는 이야기입니다..)

총잡이와 축구 선수의 인스턴스를 만들고 각각 Shoot 메서드를 호출해보도록 합시다.

1 private static void Main(string[] args)

2 {

3 Gunner gunner = new Gunner();

4 gunner.Shoot();

5

6 SoccerPlayer soccerPlayer = new SoccerPlayer();

7 soccerPlayer.Shoot();

8 }

코드 3

빵빵

계속하려면 아무 키나 누르십시오 . . .

당연히 의도한 대로의 결과가 나왔습니다.

여기까지가 인터페이스의 암시적 구현에 관한 이야기였습니다. 지금부터는 인터페이스의 명시적 구현에 관한 내용입니다.

명시적 구현을 설명하기 위해 새로운 클래스를 하나 등장 시켜야겠습니다. 군인 클래스인데요. 군인이니까 당연히 총을 쏩니다(shoot). 그리고 군인이니까 당연히(?) 공도 잘 찹니다(shoot). 그래서 병사 클래스는 IGunner와 ISoccerPlayer를 모두 상속 구현하도록 결정을 하겠습니다.

01 internal class Soldier : IGunner, ISoccerPlayer

02 {

03 public void Shoot()

04 {

05 Console.WriteLine("빵빵");

06 }

07

08 public void Shoot()

09 {

10 Console.WriteLine("");

11 }

12 }

코드 4

한 눈에 봐도 에러입니다. Shoot 메서드가 중복이 되었지요. 위와 아래의 메서드는 각각 Gunner와 SoccerPlayer의 Shoot인데, 이를 구별하지를 못합니다. 즉 시그니처(메서드의 형을 구별하는 기준, 쉽게 말하면 메서드의 이름과 매개 변수의 개수와 각 매개 변수의 형)가 동일한 메서드를 가진 두 인터페이스를 동시에 구현 상속하지 못하는 상황을 해결하기 위해 C#의 설계자는 별도의 문법을 고안하였습니다. 그것이 바로 인터페이스의 명시적 구현입니다.

인터페이스를 명시적으로 구현하는 문법은 간단합니다. 구현하는 메서드 이름 앞에 누구의 메서드인지만 표시해주면 됩니다. 다음 코드는 코드 4를 명시적 구현으로 바꾼 코드 입니다.

01 internal class Soldier : IGunner, ISoccerPlayer

02 {

03 void IGunner.Shoot()

04 {

05 Console.WriteLine("빵빵");

06 }

07

08 void ISoccerPlayer.Shoot()

09 {

10 Console.WriteLine("");

11 }

12 }

코드 5

3번 라인과 8번 라인을 보면 Shoot 메서드 앞에 각각 인터페이스의 이름을 붙이고 있습니다. 또한 접근 지정자가 사라졌습니다. 접근 지정자가 없기 때문에 Shoot은 private 멤버입니다. 이 부분에 대해서는 잠시 후 다시 말씀 드리겠습니다.

이제 군인이 총을 쏘고 공을 차는 코드를 보겠습니다.

1 private static void Main(string[] args)

2 {

3 Soldier soldier = new Soldier();

4 soldier.Shoot(); // 쏘기

5 soldier.Shoot(); // 차기

6 }

코드 6

코드를 작성하긴 했는데 영 기분이 이상합니다. 아니나 다를까 문법 에러라서 컴파일도 되지 않습니다. 코드 5의 Shoot 메서드는 Soldier의 private 메서드라서 접근할 수 없다고 합니다. 혹 접근이 된다고 하더라도, 4번 라인에서는 총을 쏘고 5번 라인에서는 공을 차야 하는데, 뭐가 무엇을 하라는 말인지 구별이 안 갑니다. 결론적으로 인터페이스가 명시적으로 구현이 되면 Soldier 변수를 통해서는 접근할 수가 없다는 사실을 알았습니다. 그렇다면 이 군인이 총을 쏘고 공을 차게 하는 방법은 무엇일까요?

답은 Soldier가 아닌 IGunner 혹은 ISoccerPlayer를 통해서 접근을 한다는 것입니다. 즉 총을 쏠 때는 Soldier 객체를 IGunner로 형변환 하고, 공을 찰 때는 ISoccerPlayer로 형변환 한다는 것입니다.

1 private static void Main(string[] args)

2 {

3 Soldier soldier = new Soldier();

4

5 ((IGunner)soldier).Shoot(); // 쏘기

6 ((ISoccerPlayer)soldier).Shoot(); // 차기

7 }

코드 7

형변환을 하지 않는다면 Soldier 객체를 만들어 IGunner 혹은 ISoccerPlayer 변수가 그를 참조하도록 하는 방법도 있습니다.

1 private static void Main(string[] args)

2 {

3 IGunner gunner = new Soldier();

4 gunner.Shoot(); // 쏘기

5

6 ISoccerPlayer soccerPlayer = new Soldier();

7 soccerPlayer.Shoot(); // 차기

8 }

코드 8

이 경우에는 예컨데 3번 라인과 같이 생성된 Soldier 객체를 IGunner 변수에 참조시키면 IGunner 변수를 통해서는 총만 쏘지 공을 찰 수는 없습니다.

이제 코드 5를 다시 보며 두 Shoot 메서드의 접근 지정자에 대해서 생각을 해 봅시다. 코드 6에서 확인하듯이 Soldier 클래스의 입장에서 두 Shoot 메서더는 private 입니다. 하지만 코드 7에서 보듯이 IGunner와 ISoccerPlayer의 입장에서는 각각의 Shoot 메서드는 public 입니다. 결국 명시적으로 구현된 인터페이스 메서드의 접근 지정자는 상황에 따라서 서로 다른 두 개의 접근 지정자를 가지게 되는 것입니다.

이제 인터페이스의 암시적, 명시적 구현을 모두 다루었으니, 응용으로 이 둘을 섞어 놓은 형태를 보도록 합시다. 다음 코드를 보시기 바랍니다.

01 internal class Soldier : IGunner, ISoccerPlayer

02 {

03 public void Shoot()

04 {

05 Console.WriteLine("빵빵");

06 }

07

08 void ISoccerPlayer.Shoot()

09 {

10 Console.WriteLine("");

11 }

12 }

코드 9

3번 라인은 암시적으로, 8번 라인은 명시적으로 인터페이스 멤버를 구현하고 있습니다. 8번 라인이야 ISoccerPlayer의 Shoot을 구현한다고 명확히 알 수 있겠는데, 3번 라인의 경우에는 명확히 알 수가 없습니다. 이 경우 컴파일러는 Soldier가 구현하여야 한는 Shoot는 두 개가 있는데 그 중 하나는 ISoccerPlayer의 Shoot이기 때문에(8번 라인) 나머지 하나는 IGunner의 Shoot로 인식을 합니다.(3번 라인)

이를 사용하는 코드는 다음과 같습니다.

01 private static void Main(string[] args)

02 {

03 Soldier soldier = new Soldier();

04

05 ((IGunner)soldier).Shoot(); // 차기

06 ((ISoccerPlayer)soldier).Shoot(); // 차기

07

08 soldier.Shoot(); // 쏘기

09 }

코드 10

코드 7과 달라진 점은 8번 라인과 같이 Soldier 변수를 통해서도 Shoot을 호출할 수 있다는 것입니다. 이 때 Soldier의 Shoot는 IGunner의 Shoot를 구현하고 있기 때문에 결국 8번 라인은 5번 라인과 동일한 결과가 나타납니다.

빵빵

빵빵

계속하려면 아무 키나 누르십시오 . . .

인터페이스의 명시적 구현을 마무리 짓기 전에 한 가지 중요한 사실이 있습니다. 실컷 명시적 구현을 연습해 놓고 이런 말 하려니 좀 허탈하긴 한데, 바로 인터페이스의 명시적 구현은 '꼭 필요한 곳이 아니면 가급적 사용하지 말라'는 것입니다. 그 이유는 여태껏 이야기한 내용에 들어 있습니다. 예를 들어 Soldier 클래스를 사용하는 개발자는 Soldier 클래스만 봐서는 Shoot 메서드의 존재를 알기 어렵습니다. 또한 만일 Soldier가 클래스가 아닌 구조체라면, Shoot메서드를 호출하기 위해 ((IGunner)soldier).Shoot과 같이 형변환을 할 때 마다 추가적인 박싱이 일어나기도 합니다.

:

C# 코딩 연습 - 컬렉션, 序

C# 코딩 연습 2008. 3. 12. 09:57

[시리즈 안내]

C# 코딩 연습 - 대리자와 이벤트

C# 코딩 연습 - 컬렉션, 序


이 문서는 세 편으로 기획된 “C# 코딩 연습 컬렉션시리즈 중 첫 번째입니다. 이 시리즈는 닷넷 프레임웍에서 차지하는 컬렉션의 높은 비중을 이해하고, 이를 효율적으로 사용하여 코드의 품질을 높이는 방법에 대한 고민을 나누는 것을 목적으로 기획되었습니다.

현재 이 시리즈는 총 세 편으로 기획되어 있으며 각각의 주제는 다음과 같습니다.

  • : 닷넷 프레임웍의 컬렉션을 이해하기 위한 기반 기술들을 살펴봅니다. 특히 컬렉션의 내부에서 사용되는 인터페이스와 대리자와 클래스 등의 역할에 중점을 둡니다.
  • : 가장 빈번히 사용되는 동시에 다른 고급 컬렉션들의 기반이 되는 세 가지 컬렉션들을 직접 만들어 보며 컬렉션의 내부 구조와 로직을 이해합니다. 이는 컬렉션의 내부 구조와 로직을 이해함으로써 컬렉션을 효율적으로 사용하는 데 목적이 있습니다.
  • : 닷넷 베이스 클래스 라이브러리에 포함된 컬렉션의 한계를 보완하는 두 개의 공개된 컬렉션 셋을 소개합니다. 이들 컬렉션 셋은 필드에서 바로 사용할 수 있을 만큼의 높은 완성도를 가지고 있습니다.

[목차]

I. 개관

II. 컬렉션과 인터페이스

III. 컬렉션과 메서드, 그리고 대리자

IV. 순서 비교자

V. 같음 비교자

VI. 부록 : 비트 보수 연산과 정수의 표현


PDF




MS WORD

:

C# 코딩 연습 - 대리자와 이벤트

C# 코딩 연습 2008. 1. 28. 15:41

[업데이트] 이벤트 코드 조각 생성기의 버전이 1.1로 업데이트 되었습니다. 관련 기사는 여기에 있습니다.

[시리즈 안내]

C# 코딩 연습 - 대리자와 이벤트

C# 코딩 연습 - 컬렉션, 序


대리자와 이벤트를 주제로 몇 가지 생각을 적어보았습니다.

문법이나 사용법에 대한 설명보다는, 대리자와 이벤트가 왜 필요한지, 어떻게 만드는 것이 좋은 지에 대한 이야기를 주로 하였습니다.

[목차]

A.대리자

B.무명 메서드

C.이벤트

D.상속과 이벤트

  1.Template Method 패턴

  2.상속된 윈폼의 이벤트

E.이벤트 코드 조각 생성기

  1.사용방법

  2.템플릿

  3.데모

F.사용자 정의 컨트롤의 이벤트

글의 내용이 길어 파일로 첨부하였습니다.




글에서 언급하는 이벤트 코드 조각 생성기의 소스와 바이너리도 첨부파일로 첨부하였습니다.

: