작게 시작한 프로젝트 규모가 점점 커지면서 리팩토링을 하게 됐는데
크고 작은 실수가 몇개 있었다. 덕분에 엄청난 시행착오를 거쳐 오랜 시간걸렸다.
몇가지 실수를 적어 보겠다.


첫번째는 메세지 전달 부분에서의 의존성을 줄이지 못한 부분이다.
각 모듈을 DLL로 쪼개는 리팩토링을 감행 하였는데 여기서 각 DLL 간 의존성을 0으로 하고, 최대한 독립적으로 General 하게 코딩하려 했다.
A DLL에서 B DLL로 메세지를 전달 하는 과정에서 상수값을 어쩔 수 없이 공유하게 됐는데,
차라리 A DLL에 SetListener 메소드를 구현하고, B DLL에서 해당 메소드를 콜해서 원하는 핸들러를 등록하게 했어야 했다. 

두번째는, Data와 Controller를 완벽하게 분리 하지 못했다.
기본적으로 Data 객체가 있고, 해당 객체를 컨트롤 하는 Controller를 만들어 두긴 했는데,
무슨 생각인지 Data 안에서 사용하는 vector를 바로 다른 곳에서 사용 가능하게 dllexport 해버렸다. (std::vector<T>& 를 리턴하는 Getter 제공)이렇게 하면 안됐고, 차라리 vector 안의 데이터에 접근하는 메소드 하나를 제공했어야 했다. 예를 들면 SetData(int nVectorIndex, T Data) 이런식으로 말이다.
지금 짠 코드는, Command 패턴이 많이 사용됐기 때문에 Command를 호출하는 곳에서
std::vector<T>& vecData = GetVecData();
vecData.push_back(t) .. 이렇게 Controller 외부에서 Data에 바로 접근하는 코드가 많이 들어가게 됐는데, 제대로 짰다면 해당 데이터를 처리 하기 위해서는 Controller의 Setter 부분을 호출해서 처리하는게 맞았다.
Command의 Execute 부분에서 바로 데이터를 처리하려다 보니, 나중에 멀티쓰레드에서 동기화 처리 하는데도 애를 꽤나 먹었다.
차라리 Controller에 동기화 처리를 해주면 한방에 해결되는 문제였는데..

세번째는 Action Tool와 Mode Tool의 모호성에 따른 세번에 걸친 리팩토링..
아직 이 부분은 해답을 내리진 못하겠다. 일단은 정리해보겠다.
보통 Tool을 만들 때, 아래의 상황을 고려한다.

1. Tool Button을 클릭 하는 순간 팝업 창 등이 뜨고, 값을 입력 한 후 OK 누르면 이벤트가 발생하는 경우.

2. 포토샵의 Marquee Tool, 룰러 툴 처럼 Mouse Down 시 이벤트 발생/ Mouse Up시 이벤트 끝 인 경우

3. 포토샵의 Path툴, Brush툴, 페인트 툴 처럼 매 마우스 이벤트 마다 자기 툴 이벤트를 계속 행해주는 경우

4. 그리고 제일 짜증나는.. Toggle Mode... 예를 들면 포토샵의 마스크툴 같은 경우다.. 마스크 모드 일 때랑 아닐 때랑 툴들의 역할이 달라진다..

1의 경우는 Tool Button을 클릭하는 순간- 까지는 Command로 처리후, 값 입력 후 OK 누르면 처리되는 이벤트 역시 Command로 처리한다.
그래서 이걸 Tool이라고 부르는게 맞나 의문이다. View에 마우스 클릭이 필요 없기 때문에..
그림판의 새 도화지'/'파일 열기' 등이 이에 해당한다. 그래서 이런 경우는 굳이 Tool로 분류하지도 않고, UI적으로도 Tool Box에 배치시키지 않는다. 따로 배치한다.

2와 3의 경우는 잘 생각해야한다.
사각형 영역을 선택하는 방법은 여러가지가 있지만 일단 대충 2개만 생각해보자.
첫번째는 좌상단을 LButtonDown 한 후 우하단으로 드래그 하여 사각형을 그리고 LButtonUp 하는 것이다.
두번째는 좌상단을 클릭(LButtonDown&Up 동시), 우 하단을 클릭 하여 설정 하는 것.(마치 Path툴 처럼) 이 모두는 Action Tool 로 관리한다.
ActionTool에 Start / Doing / End 를 만들어서 처리한다. 그리고 각각의 Trigger용 method도 설정한다. Trigger용 메소드를 이벤트 리스너로 등록가능하게 해서 처리한다.
Start는 말그대로 ActionTool의 Start다. ActionTool로 설정 된 이후 View에 마우스 이벤트가 최초로 들어왔을 경우 발생시킨다. (SetStartListner를 해당 위치에 등록함. 툴의 성격에 따라 LButtonDown을 Start의 Trigger로 할지, Up을 Trigger로 할지 정한다.)
Doing은 보통 MouseMove 이벤트 발생시에 동작하도록 리스너를 등록한다, 혹은 첫번째 방법대로 할 경우는 딱히 해 줄 것은 없고, 두번째 방법 대로 할 경우에는 추후 들어오는 클릭 이벤트를 모두 체크한다.
End용 Trigger 역시 클릭 이벤트가 들어오는 방법에 따라서 End 조건을 만족하는지 체크한다. (Path Tool이 폐곡선을 그렸다던지, 더블클릭을 했다던지)
참고로 End가 Call 됐다고 해서 ActionTool이 다른것으로 바뀌진 않는다. 클릭 이벤트가 다시 들어올 경우 Start부터 다시 시작하면 되는 것이다.

4의 경우는 Mode Tool로 관리한다.
Mode가 Enable/Disable 돼 있는 경우에 따라서 발생하는 이벤트가 모두 다르다.
이 때, 중복 가능한 Mode가 있고 하나만 선택 가능한 Mode가 있기 때문에, Mode 를 Group으로 묶는다. (ModeGroup)
같은 ModeGroup 안에 있는 Mode들 끼리는 중복이 안된다. 마치 RadioBox 처럼.
Mode의 경우 모드가 Enable 됐는지 안돼있는지 알기 위해서는 Flag를 체크해야 하기 때문에, 위에서 만든 Action들이 처리 될 때 마다
ModeManager를 Broadcast해서 Enable 여부를 파악해, Event를 Trigging 해야 한다.

생각나는 대로 적어봤지만 여전히 햇깔린다.결론은.. 마우스 클릭 이벤트로 처리 가능한 웬만한 애들은 ActionTool. 아 참고로 ActionTool간의 중복은 당연히 안된다.
Toggle 가능한 툴들은 ModeTool,
마우스클릭 굳이 필요 없는 툴 들은 그냥 Command로 처리.

마지막으로.. 필터/알고리즘 적용 등을 처리 하는 경우 웬만하면 Async Thread말고 Sync Thread로 처리하자.
Async로 해놓는 동안 View에 이벤트가 발생한다던지 Data가 바뀐다던지 하면 처리하기 곤란하다..이것 역시 Command로 만들어서, Sync로 생성 후 Progress Dlg로 처리하는 방법이 편하다.

아.. 하나 더 생각났다.
로직 처리를 Command에 맡기자. 로직을 DLL에서 처리시켜려다 보니 DLL이 다른 프로젝트와의 의존도가 계속해서 높아지더라.
예를 들면 파일로 부터 값을 읽어서 vector<Data>에 저장해야 하는게 있고, FileController를 DLL로 빼두었다면,
설계 가능한 방법은 아래와 같이 두개다.
1. vector<Data> 자체를 FileController로 넘기는 것.
2. Main의 Logic 부분에서 FileController로 읽은 값을 vector<Data>에 넣는것.
지금 짠 플젝은 아무 생각 없이 1번으로 했는데, 그러다 보니 vector<Data>가 자꾸만 여기저기서 불려서 의존성이 높아진다.
차라리 2번 방법으로 할 걸 그랬다. 그러면 FileController 자체도 최대한 범용으로 쓸 수 있고 좋다.(꼭 vector<Data> IO 용이 아니라 다른 용도로 쓸 수 있을 듯)


너무 두서 없이 적었지만 대충 생각 나는건 이정도..
그렇다고 너무 작은 소규모 프로젝트에서 처음부터 이런 설계를 적용하는 건 배보다 배꼽이 커진다. 특히 Mode Tool의 설계는, 언제든지 집어 넣을 수 있을 수준으로만 설계하고 당장 넣을 필요는 없다.
아.. 갑자기 생각 난건데.. Action Tool에서 최종적으로 처리가 되는 부분은 결국 Command 패턴 설계를 집어넣어야 한다.
Undo/Redo를 위해서...

오늘은 여기 까지... 나중에 다시 설계할 일 있으면 다시 곰곰히 생각해야지..''




'

저작자 표시 비영리 변경 금지
신고
블로그 이미지

roter

JHB / Peripheral Programmer

댓글을 달아 주세요

CRect의 생성자에 NULL을 넣어서 초기화 해주면 쓰레기 값이 들어간다.

예를 들어

CRect rt(NULL);

하면 rt에는 막 -2349823 이런 쓰레기 값이 들어간다
0으로 초기화 하는게 목적이라면 그냥
CRect rt
까지만 해주는게 제일 좋다.

이 차이가 어디서 발생하냐면

IsRectNull()과 IsRectEmpty() 의 성공 여부 때문이다.

나는 당연히

CRect rt(NULL)
한 다음에 rt.IsRectNull() 하면 TRUE가 리턴될 줄 알았다.
그런데 아니더라.
IsRectNull()은 해당 CRect가 0으로 초기화 돼있는지를 검사하기 때문이다.
하지만 쓰레기 값이 들어있으니 rt(NULL)로 초기화 했다면 IsRectNull()로 FALSE가 나오는 것이다.

안전하게 rt의 값을 확인하고 싶다면 IsRectEmpty()를 쓰는게 좋다.
이는 쓰레기 값이 들어있거나, 0으로 초기화 돼있으면 TRUE를 리턴한다.

결론 :
1. CRect rt(NULL) 하지 말고 걍 CRect rt 까지만.
2. IsRectNull() 말고 IsRectEmpty()로 초기화 상태 검사하는게 안전.
저작자 표시 비영리 변경 금지
신고
블로그 이미지

roter

JHB / Peripheral Programmer

Tag MFC

댓글을 달아 주세요

MFC DLL에서 class에 vector를 담아서 Export 하려 시도 하였다.
처음에는 잘 되는 듯이 보였지만, class를 delete 하는 순간, 와장창 무너지기 시작했다.

계속해서 dbgheap.c의 _CrtIsValidHeapPointer(const void * pUserData) 안에 있는
HeapValidate( _crtheap, 0, pHdr(pUserData) ); 에서 FALSE을 리턴하여 ASSERT에 걸리게 되었다.

계속 디버깅을 해보니, vector의 소멸을 제대로 하지 못하고 있었다.
vector의 소멸자를 따라가 보니 dbgheap.c의 _crtheap이 caller의 heap을 가리키고 있는 것이 보였다.
아니, 애당초 call stack의 소멸 지점이 callee 인 dll이 아니라, caller인 application 이었다.

구글링을 해보니 STL 객체가 포함된 클래스를 내보내는 방법에 대한 msdn 문서가 있었다.
https://support.microsoft.com/ko-kr/kb/168958

중요한 것은, export 하는 class에서 사용하는 stl 타입의 선언을 아래와 같이 class 외부에서 한번 더 해줘야 하며,
template class __declspec(dllexport) std::vector<자료형>;
import 하는 class에서는 아래와 같이 선언해 줘야 한다는 것이다.
extern template class __declspec(import) std::vector<자료형>;
또한 STL 컨테이너 중에서 유일하게 vector<T> 타입만이 DLL Export가 가능하다.

위와 같이 해주니, vector의 소멸 시점에 정상적으로 callee인 DLL의 heap을 잡는 것이 보였고,
소멸 역시 제대로 이루어 졌다.

사족 )
이번 삽질을 통해 알아낸 재밌는 사실 중 하나는, MFC project의 경우 allocator delete(*)를 crt library에서 부르는 것이 아니라 mfc library에서 먼저 호출한다는 것이다.
이는 afxmem.cpp에 있는 void __cdecl operator delete(void* p)로 오게 된다. (결국엔 crt로 오기는 한다. [debug 모드에서 찍어보면 dbgheap.c로 간다])
crt library의 경우 중간 단계 없이 바로 delete로 간다.
이 순서는 재밌게도, 라이브러리 import 순서에 따르며 이렇게 되는 이유는, CRT 라이브러리가 new, delete 및 DllMain 함수에 대해서 약한 외부 링크를 사용하기 때문이다.
따라서 CRT 라이브러리가 링크 되기 전에 MFC 라이브러리를 링크 하면 MFC 라이브러리에 있는 new, delete 및 DllMain 함수가 먼저 호출되는 것이다.
C++도 아니고 C에서 이런 신박한 코드를 작성하다니 재밌다 :)
사족 참조 : https://support.microsoft.com/ko-kr/kb/148652
저작자 표시 비영리 변경 금지
신고
블로그 이미지

roter

JHB / Peripheral Programmer

Tag DLL, MFC

댓글을 달아 주세요