델파이(오브젝트 파스칼) 언어에서 with 구문이 종종 위험할 수 있다는 말은 델파이 개발자라면 한번쯤은 들어보셨을 겁니다. with 구문은 그 블럭 안의 식별자가 어느 객체에 속한 것인지를 모호하게 만들 수 있기 때문인데요, 주로 self에 속한 멤버와 with에 속한 멤버 사이에서 문제가 발생하기 쉽습니다. 더욱이 with는 하나 이상의 여러 객체를 포함할 수도 있기 때문에 더욱 위험해집니다. 그래서 저는 with 구문은 그 의미가 아주 명확한 경우에만 사용하도록 조언하고 있습니다.
하지만 본인이 작성한 코드가 아닌 서드파티 라이브러리 등에서 with 문을 많이 사용한 경우는 어떻게 컨트롤을 할 수가 없겠지요. 그제 제가 겪은 조금 특이한 케이스 하나를 소개하려고 합니다.
저는 그리드 컨트롤로는 DevExpress의 퀀텀그리드, 즉 cxGrid를 선호합니다. 사용법과 클래스 구조가 지나치게 복잡하다는 단점이 있기는 하지만, 개발자와 사용자가 상상할 수 있는 거의 모든 기능을 다 구현해놓은 궁극의 그리드라고 할 수 있습니다. 물론 성능면에서 보자면 cxGrid의 데이터 처리나 표시 속도가 충분히 빠르지 못하고 또 많이 무겁다는 단점도 있어서, 절대적인 속도가 필요한 경우에는 cxGrid 대신 제가 개발한 별도의 그리드 컴포넌트를 사용하고 있습니다. 하지만 cxGrid의 성능은 제가 접하는 90% 이상의 경우에는 충분한 속도여서, 통상적으로는 cxGrid를 사용하는 경우가 대부분이죠.
저는 10여년 전부터 제가 개발한 업무개발 클라이언트 프레임워크를 여러 고객사에 납품해왔는데, 최근에 기존에는 델파이 XE 버전으로 개발되어 있던 것을 델파이 XE4 버전을 거쳐 10 Seattle 버전으로 마이그레이션하게 되었습니다. 그런데 며칠 전 cxGrid를 사용해서 개발한 프로그램을 납품한 고객사에서, 그리드의 필터 기능의 오동작을 발견했다면서 수정을 요청해왔습니다.
cxGrid의 필터링 기능은 컬럼 헤더에 마우스 커서를 올리면 필터 버튼이 나타나고, 그것을 클릭하면 해당 컬럼의 값들을 집계한 팝업창이 나타나고 거기서 특정 값을 클릭하면 그 값을 가진 행만을 필터링해서 보여주는 기능인데요. 고객사에서 버그라고 수정을 요청한 상황은 아래와 같았습니다.
잘 보시면, ‘퇴’로 보이는 컬럼(‘퇴직’입니다)의 바로 밑에 나타나야 할 팝업창이 엉뚱하게도 화면 왼쪽 끝에 붙어서 나타난 것을 볼 수 있을 겁니다. 어떤 컬럼에서 필터링 버튼을 클릭해도 항상 저 위치에 나타납니다. 정상적으로는 아래와 같이 나와야 하죠.
이 cxGrid의 소스는 최신 버전은 아니고 2010 버전때부터 사용하던 버전입니다. 사용하던 버전에 별 문제나 추가로 필요한 기능이 없어서 델파이 버전을 업그레이드하면서도 기존 cxGrid 소스를 계속 사용해온 것인데요. 그런데 cxGrid의 소스를 전혀 수정한 적이 없는데, 델파이 XE 버전까지는 제대로 동작했었습니다. 실제로 이 cxGrid를 사용하는 제 프레임워크는 지금도 크고 작은 여러 고객사에서 4, 5년 이상 아무 문제 없이 잘 사용하고 있습니다. 즉 cxGrid의 소스는 그대로인데도 XE 버전 이후의 델파이가 뭔가 바뀌면서 이전에는 없던 버그가 생긴 겁니다.
이 문제의 원인을 찾기 위해 몇시간 동안 cxGrid의 관련 소스 파일들을 뒤졌습니다. 그리고 찾아낸 문제는 다음의 코드 부분에 있었습니다. (cxControls.pas)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function TcxPopupWindow.CalculatePosition: TPoint; ... procedure CheckPosition; var ADesktopWorkArea: TRect; procedure CheckCommonPosition; begin with Result, ADesktopWorkArea do begin if (FDirection = pdVertical) or (AAlignHorz = pahCenter) then begin if X + Width > Right then X := Right - Width; if X < Left then X := Left; end; if (FDirection = pdHorizontal) or (AAlignVert = pavCenter) then begin if Y + Height > Bottom then Y := Bottom - Height; if Y < Top then Y := Top; end; end; end; ... |
짐작하시겠지만 이 코드는 필터 팝업창이 나타날 위치를 계산하는 코드입니다. 여기서 직접적으로 문제가 발생한 라인은 아래의 라인입니다.
1 |
if X + Width > Right then X := Right - Width; |
보시다시피 이 라인은 with 블럭으로 ADesktopWorkArea 객체의 영향을 받게 되어 있는데요. 위 코드에서 보다시피 이 ADesktopWorkArea 객체는 TRect 타입입니다. 그런데 델파이 XE 버전까지는 TRect 타입에 Left, Top, Right, Bottom의 네 개 멤버만 있었습니다만, XE2 버전에서 이 TRect 타입에 새로운 멤버들이 대폭 추가되었는데, 이 추가된 멤버들중에 Width와 Height 속성이 포함되어 있습니다.(짐작하시겠지만 Left, Top, Right, Bottom로부터 계산된 값을 리턴해주는 속성입니다).
이렇게 TRect에 Width와 Height 속성이 추가되면서, 위 코드 라인의 의미가 달라지게 되었습니다. XE 이전의 버전에서는 Width가 self, 즉 TcxPopupWindow 타입(TForm에서 상속)의 멤버로 해석되었었지만, XE2 버전 이후에서는 self가 아닌 ADesktopWorkArea의 멤버로 해석된 것입니다. 이런 이유로 XE2 이후의 버전에서는 위 코드의 Result.X 값이 언제나 0 값만을 리턴하게 되었습니다. 이것이 팝업창이 화면 왼쪽에 붙어서 나타난 이유죠.
이런 원인을 알고 나면, 위 코드를 델파이 XE 이후의 버전에서도 제대로 동작하도록 수정하는 방법은 간단합니다. Width와 Height가 원래의 작성 의도대로 self의 멤버로 해석되도록, Width와 Height가 사용된 부분들을 self.Width와 self.Height로 수정하면 됩니다.
이 사례를 요약하자면, cxGrid의 코드는 전혀 바뀌지 않았고 단지 컴파일한 델파이의 버전만이 바뀌었는데 cxGrid가 오동작하게 되었습니다. 이 버그의 원인을 생각해보면, 물론 TRect 타입에 Width와 Height 속성이 추가된 것이 직접적인 원인입니다만, 더 근본적으로 따지자면 이 코드가 with 구문 내에 있었기 때문입니다. with를 사용하지 않았더라면 TRect에 새로운 속성이 어떻게 추가되었더라도 이런 버그는 발생하지 않았을 것이니까요. 이 버그는 with 때문에 버그 증상을 발견하고도 원인을 찾아내기가 어렵습니다. 특히 cxGrid처럼 방대하고도 구조가 대단히 복잡한 클래스 라이브러리에서는 더더욱 그렇습니다.
with를 사용하면 코드가 간략해지고 일견 깔끔해보이게 됩니다만, readability, 가독성을 떨어뜨립니다. 대체적으로 코드가 간략해지면 가독성도 높아지는데, with만은 정반대입니다. 특히 위에서처럼 with에 둘 이상의 객체를 포함하는 경우에는 더욱 그렇습니다. 가독성이 떨어진다는 것은 코드 리뷰와 디버깅이 그만큼 어려워진다는 의미입니다. 또 이 사례에서처럼 with로 인해 버그가 발생하는 경우도 적지 않습니다. 그런만큼, 테스트 코드나 프로토타입 목적이 아닌 실무 목적의 코드라면 가급적 with의 사용을 자제해야 합니다. 특히 오랫동안 유지보수되어야 할 코드라면 더더욱 with의 사용을 조심해야 합니다. 또한 with 구문은 델파이 언어에서 deprecated 혹은 아예 없어질 예정이기도 합니다.
(이 버그는 아마 cxGrid의 신규 버전에서는 이미 수정되어 있겠지만, DevExpress의 담당 개발자도 처음 버그 레포트를 받았을 때는 버그의 원인을 그리 쉽게 찾아내진 못했을 겁니다)