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, 나아가서는 프로그래밍의 왕도가 아닐까 싶습니다.

예제 소스 :

: