이번에 간단히 직렬 포트serial port 로 데이터를 전송하는 툴을 만들고 있습니다.
회사 내부에서 사용할 목적인데, 비교적 간단한 프로그램임에도 리팩토링을 마음먹고 하면서 진행하다보니 시간이 좀 걸리는군요.
3일 정도해서 대부분 개발이 끝났고, 현재는 오류 처리 부분만 조금 남겨 둔 상태입니다.
기능은 뭐 정말 간단합니다. 전송할 파일 선택하고, 내보 낼 직렬 포트 선택하고, 포트 속도 등 설정한 다음에 Send 버튼 눌러주면 착착착~ 데이터 보내면서 진행 상황 보여주는 정도입니다.
굳이 이 녀석을 끌고 와서 포스팅을 하는 이유는... 근래에 이런 작은 프로젝트는 정말 오랜만에 하게 되었는데, 큰 프로젝트에서는 쉽게 꺼내어 보기 힘들 수 있는 객체-지향object-oriented 에 대한 기본 개념들에 대해 다시 느낄 수 있는 기회가 되었기 때문입니다. 그래서 그에 대한 고찰을 통해 한번 더 곱씹어 보고자 합니다. ^^
전체적인 구조는 그림에서 보는 정도로 작은 프로그램입니다.
만만한 MVC 패턴을 적용해서 개발했구요. 그림에서
주황색 계열이 View,
녹색은 Controller,
파란색 계열이 Model 로 보시면 되겠습니다.
GUI 쪽은
wxWidgets 이용했구요, 그렇다고 멀티-플랫폼 지원까지는 아니고... 용도가 용도인지라 그냥 Windows 용입니다.
podo... 이 녀석들은 제가 만들어서 쓰는 자잘한 프레임워크입니다.
podoFile 은 디스크 상의 파일을 가리키는 클래스로, open/close 및 read/write 가상 메쏘드가 마련되어 있구요.
podoFile 을 상속한 podoCommPort는 podoFile과 같은 인터페이스로 Serial/Parallel 포트를 나타냅니다. 즉, podoFile 객체 하나는 파일 하나를 가리키고, podoCommPort 객체 하나는 역시 직/병렬 포트 하나를 가리킵니다.
이렇게
객체 하나가 물리적 혹은 논리적인 장치나 실체 하나를 의미하게 하는 것이 바로 객체-지향object-oriented 의 기본이라고 하겠습니다.
다만 podoFile 에 대한 설계가 완벽치 않은 부분이 있습니다.
예를 들어 podoFile::size() 메쏘드는 파일의 크기를 나타내는데, podoCommPort::size() 메쏘드는 어떤 것을 나타내야 하는지 그 의미가 불분명해집니다. 파일 포인터의 위치를 옮기는 seek() 메쏘드도 마찬가지 상황을 겪게 됩니다.
간단히 생각한다면, '직/병렬 포트에 대해서는 size와 seek가 의미가 없으니 그런 짓을 하지 않으면 되고, 또 그럴 일도 없다' 라고 간단하게 생각할 수도 있습니다만, 이것은 podoFile 로 표현된 객체가 사실은 podoCommPort 라는 것을 알고 있어야만 가능한 이야기입니다.
podoFile 을 다루는 다른 객체가 podoCommPort 에 대해서도 알고 있어야만 한다는 건 분명 문제입니다. 객체와 메쏘드 내부에서 어떤 처리가 일어나든지 간에 이를 사용하는 다른 객체 입장에서는 입력과 출력이 언제나 규정된 대로 동일해야 합니다. 이게 바로 캡슐화encapsulation 입니다.
캡슐화를 통해 좀더 나은 설계를 하려고 한다면, podoData 정도의 클래스를 만들어 podoFile 의 기능을 그리로 옮기고, size/seek 메쏘드는 podoFile 로 보낸 다음, podoCommPort 는 podoFile 이 아닌 podoData 를 상속하는 형태로 바꾸어야 할 것입니다.
다만, 이러한 리팩토링을 실행하지는 않았는데, 그것이 아직까지는 쓸데없이 설계를 복잡하게 만든다고 생각했기 때문입니다. 물론 필요하다고 느끼는 순간이 온다면 과감하게 이를 수정할 것입니다. ^^
FileTransferer 는 podoFile 에서 podoFile 로의 데이터 전송만을 담당합니다.
이번 프로젝트에서는 FileToCommPort를 통해 podoFile -> podoCommPort 연결이 이용되었고, 다음에 다른 연결이 필요해지면 FileTransferer를 재사용할 수 있겠죠. ^^
이거 써 놓고 보니 별 거 아닌 것 같은데... 실전 현장에서 '코드의 재사용'이 일어나는 가장 흔한 경우가 이런 클래스의 재사용이 아닌가 싶습니다. 일종의 부품화로 볼 수 있는데, 이렇게 입력과 출력을 추상화한 처리 클래스는 재사용의 가능성이 무척 높습니다. 콘센트에 모양만 맞으면 어디든 꽂을 수 있는 것처럼...
이번엔 View 쪽을 살펴 보겠습니다.
FileSelector는 파일 선택 다이얼로그를 띄워서 파일을 선택하고, 선택한 파일의 경로를 알려주는 역할을 합니다.
FileSelector는 FileType을 이용하여 선택할 파일에 대한 필터링을 수행합니다.
이름에서 유추할 수 있듯이, FileType 은 파일의 종류를 의미하며, 확장자와 그에 대한 설명을 기본으로 이루어져 있습니다. 확장자와 설명은 모두 문자열string 로, FileType 의 궁극적 형태는 문자열 짝string pair 입니다.
이렇게 짝을 이루는 데이터를 처리하기 위해 아래와 같은 템플릿template 을 이용합니다. daval... 시리즈 역시 제가 만들어서 사용하는 자료 구조 관련 템플릿 클래스들입니다. tA와 tB 가 모두 string 이 되면 문자열 짝을 의미하게 됩니다.
FileType은 아래와 같은 구조입니다.
그리고 FileSelector는 이렇게 생겼습니다. podoStr은 문자열이고, davalList 는 이중-연결 리스트를 구현하는 템플릿 클래스입니다.
이제 마지막으로... FileSelector는 아래와 같이 사용하면 됩니다.
부분적인 코드들과 클래스를 정리해보면,
- FileSelector 는 파일 필터링을 위해 FileType 을 이용하고, 필요한 만큼의 FileType 을 추가(append)하여 다중 필터링합니다.
- FileSelector 가 사용하는 FileType 은 여러 개가 될 수 있고, 그 개수를 한정하기 어려우므로 연결 리스트를 이용합니다. (davalList)
- FileType 는 파일의 와일드카드wildcard 및 설명remark 를 가지는 문자열 짝입니다. (davalPairString)
- FileSelector::select() 메쏘드 내부에는 FileType 리스트를 Win32 API인 GetOpenFileName() 가 사용하는 OPENFILENAME 구조체의 lpstrFilter 멤버 형태로 바꾸어주는 부분이 있는데, 이를 위해 FileType::toFilter() 메쏘드를 이용합니다. 즉, FileSelector 가 이를 처리하지 않고 FileType 에게 이를 위임합니다. '객체의 출력을 만들지 말고 객체에 요구하라'의 실천이 되겠습니다. Java 로 치자면 toString() 정도의 메쏘드로 볼 수 있겠지요. 스트링을 입출력 매개로 사용하면서도 이름이 toString이 아닌 것은, 필터 정보가 정상적인 NULL로 끝나는null-terminated 문자열이 아니기 때문입니다. 참고로 FileType::toFilter() 메쏘드는 다음과 같습니다.
Settings 는 INI 파일을 통해 설정 정보를 제어하는데 이용됩니다. 지금은 별다른 설계 없이 그냥 get/set 정도의 메쏘드들만 존재하는 다소 무식한 (하지만 할 일은 다 하는) 녀석입니다. ^^
Settings 의 리팩토링은, 아직 설계에 익숙치 않으신 분들이 해볼만한 과제가 아닐까 싶습니다.
INI 파일은 내부에 그룹을 가지고, 그룹은 항목을 가지지요. 그럼, Group과 Item 정도의 클래스를 만들어서 이들이 각각 INI 파일의 그룹과 항목을 가리키게 할 수 있을 겁니다. Item 은 항목 이름과 값의 짝pair 으로 이루어지겠군요.
값의 종류에는 정수, 문자열, 바이너리 데이터 등이 있는데, 이것은 어떻게 표현할까요? 우선 Item 이 type 멤버 변수를 가지는 것을 생각해 볼 수 있겠네요. 그런데 그럼 Item 어딘가에 type 에 따라 처리를 달리하는 if 문의 나열이나 switch 문이 필요해지겠네요. 이를 피하기 위해 Item 을 상속한 ItemInteger, ItemString, ItemBinary 등의 클래스를 만들어 가상 멤버 함수를 이용하는 것은 어떨까요? 몇 개 되지 않는 간단한 분기 처리에 너무 복잡한 설계일까요?
행복한 고민, 선택은 여러분이 하시기 바랍니다. ^^