지난번에 "간단한 리팩토링 예제" 게시물에 달린 댓글들에서 with와 assert 사용에 대한 것들이 있었는데, 이 중 with 사용법에 대해서 논해보도록 하자.
델파이 매뉴얼을 보면, with는 동일한 객체에 대해서 여러 멤버를 참조할 때 매우 편하며, 실행 속도 역시 최적화한다고 적혀 있다. 즉, 델파이 매뉴얼에는 with의 사용을 권장하고 있는 듯 하다. 그러나, 이론과 실제는 다르듯이 with는 경우에 따라서 매우 미묘한 버그의 원인이 된다.
지난 강좌의 소스를 with를 사용한 것으로 바꿔 보자.
[지난번 강좌의 원본 클릭 이벤트]
procedure TForm1.Button1Click(Sender: TObject);
const
s1 = '하하';
s2 = '호호';
begin
if TButton(Sender).Caption = s1 then
TButton(Sender).Caption := s2
else
TButton(Sender).Caption := s1;
end;
[with 사용으로 수정한 클릭 이벤트]
procedure TForm1.Button1Click(Sender: TObject);
const
s1 = '하하';
s2 = '호호';
begin
with TButton(Sender) do
begin
if Caption = s1 then
Caption := s2
else
Caption := s1;
end;
end;
with 사용으로 TButton(Sender) 강제 형변환은 줄어 졌으나, 다른 문제가 새로 생겼다. Caption 속성이 Button1의 Caption인지, 이 이벤트 메서드가 속한 Form의 Caption인지 아리송하다는 것이다. 물론 델파이 컴파일러는 전혀 혼동하지 않는다. 델파이 컴파일러는 가까운 지역 명칭 우선 참조 원칙에 의거하여 Button1의 Caption 속성을 참조한다. 그러나, 이 코드를 읽는 사람은 델파이 컴파일러처럼 명확하게 알지 못할 수도 있다. 이 코드를 읽는 사람은 Caption 속성이 폼의 그것인지 Button1의 그것인지 혼동할수도 있다는 것이다.
위의 코드는 매우 간단한 경우지만, with의 begin와 end 사이에 여러 라인의 코드가 있고, 여러 속성을 참조하는 경우에는 어떤 객체의 속성인지 혼동할 가능성이 매우 커진다. 사실 본인도 과거에 이런 미묘한 버그에 봉착하여 서너 시간 동안 고생한 적이 있었다.
with 구문이 최악인 경우는 다음과 같이, with 뒤에 여러 객체가 나오는 경우이다.
with obj1, obj2, obj3, obj4 do
begin
xx := 100;
end;
이 문장은 대략 다음과 같은 역할을 한다.
with obj1 do
with obj2 do
with obj3 do
with obj4 do
begin
xx := 100;
end;
이런 경우, obj1과 obj2 객체 모두 xx라는 동일한 이름의 속성을 가진다면, xx가 대체 어떤 객체의 속성인지 판별하기 매우 혼란스럽다. 물론, 이 경우에도 델파이 컴파일러는 명확한 방식으로 어떤 객체의 속성을 참조할지 제대로 결정하지만, 이 코드를 보는 사람은 매우 혼란스럽다는 것은 분명하다. 내 경험에 의하면 with를 사용하더라도 이런 방식으로 사용하는 것은, 아주 간단한 코드가 아닌 한, 가급적 피하라는 것이다.
with문이 유발할 수 있는 이러한 미묘한 버그 때문에 아예 with문을 전혀 사용하지 않는 델파이 개발자들도 있다. 그러나, with문은 단점도 있지만 장점도 있다. 현명하게 사용하면 득도 있는 것이다. with 문을 잘 사용하는 방법을 알아보자.
첫번째로 with 블럭의 길이를 최소화하라. 간단한 with 블럭은 혼란이 적기 때문이다.
with objx do
begin
... // 이 라인수를
... // 할수만 있다면
... // 최소화하란 것이다.
end;
두번째로, with 블럭내의 코드는 가급적 한 객체의 멤버 참조로 제한하라. 다음 with 블럭에서 aa, bb, cc 모두 objx의 멤버들이다. 다시 말해서 with 블럭 내에서는 with 객체의 멤버들만 참조하는 코드들만 두라.
with objx do
begin
aa := 10;
bb := 'aaa';
cc := 'dddd
end;
마지막으로 할수만 있다면, with 블럭을 다른 구조 제어문 안에 내포시키지 말라. 모든 구조 제어문은 중첩 정도가 심하면 심할 수록 코드 이해가 어려워 진다. 따라서 차라리 타이핑량이 좀 많아지는 번거로움이 있더라도 다음과 같은 코딩은 피하라는 것이다.
[bad..]
if ... then
with objx do
begin
aa := 10;
bb := 20;
end;
[good]
if ... then
begin
objx.aa := 10;
objx.bb := 20;
end;
간혹 개발자들 중에는 컴파일러가 이해하기 쉽도록 코딩하는 사람들이 있는데, 코드의 가독성은 인간을 위한 것이지 컴파일러를 위한 것이 아니다. 가급적 인간이 이해하기 쉽게 작성하는 것이 좋다.
|
예로 드신 폼과 버튼의 캡션의 경우에, 버튼관련 코드들 사이에 폼의 캡션을 지칭하는 코드가 하나라도 섞여있는 경우, 그 전체를 with로 묶어버리면 당연히 엉뚱한 객체의 캡션이 엉뚱하게 참조되게 되지요.
그나마 캡션의 경우에는 눈에 보이고 또 잘못 지정되더라도 치명적인 결과가 되지 않지만, 캡션만큼 자주 등장하는 멤버인 Handle일 경우라면? 단 하나의 with문 때문에 실제로 엉뚱한 객체의 핸들을 가져오는 바람에 어처구니없는 에러나 버그가 생기기도 했습니다. 사실 개발자의 입장에서는 컴파일타임이든 런타임이든 에러라도 나면 차라리 다행이지만, 핸들을 잘못 참조해서 메시지가 사라지거나 해서 표가 안나게 문제가 생기면 두고두고 골치를 썩이는 문제가 되죠. 그래서 더욱 with문은 말씀하신 것과 같은 일정한 원칙을 지켜서 사용해야 하는, 무시무시한(?) 문법이죠. ㅎㅎㅎ