원본 비디오(YouTube) 보기 (11 min): 아래 원본 비디오에는 이 요약글 보다 더 자세한 예시와 설명이 있습니다.
DelphiCon 의 2023 시리즈 중, You're doing it wrong! - Carlos Agnes (11 min) 의 한글 요약본입니다.
- 이 세션에서는 델파이로 개발할 때 우리 주위에서 매우 흔하게 일어나는 일반적인 작은 실수 몇가지를 정리하고 올바른 방식을 제시합니다.
발표자
- Carlos Agnes (Tatu라고도 알려져 있음)
- 엠바카데로 MVP
- TMR 컨설팅 개발 사의 CFO
- 공인 델파이 마스터 개발자 (Delphi Master Developer Certified)
- 델파이를 사랑한지 20년이 넘었음
목차
1 IfThen 함수는 (진정한) 삼항 연산자 (ternary operator)가 아니다
IfThen 함수
- 델파이에서 IfThen 함수는 2개이다 (하나는 Math 유닛에 있고, 다른 하나는 StrUtils 유닛에 들어있다).
- 매우 유용한 함수이고, 나는 많이 사용한다.
-
문법
- IfThen (파라미터1, 파라미터2, 파라미터3)
-
처리 방식
- 첫번째 파라미터 안에 있는 불리언 값이 True이든 Fulse이든 상관없이, 일단 2번째와 3번째 파라미터의 값을 모두 구하고 난 후에, 둘 중 하나의 값을 최종 결과로 반환한다.
-
즉, IfThen 함수는 진정한 삼항 연산자(ternary operator)가 아니다.
- 컴파일러 수준에서 보면 IfThen은 일반적인 라이브러리와 같은 함수이다, 진정한 삼항 연산자가 아니다.
// 잘못된 사용: 2번째 그리고 (또는) 3번째 파라미터 안에 상당히 복잡한 함수를 넣는 실수 Result := IfThen (BooleanValue, ComplexFuction1, ComplexFuction2);
예를 들어, 위 예에서 ComplexFuction1 함수에서 값을 얻는데 1초가 걸리고, ComplexFuction2 함수가 2초가 걸린다면, 위 구문을 실행하려면 최소한 3초 이상이 걸린다. 둘 중 어느 함수의 결과가 최종 결과가 되는 지와 관계없이 그렇다.
// 올바른 사용: IfThen 함수를 사용할 때는 2번째와 3번째 파라미터에 상수를 사용하자. 매우 매우 비용이 적게 드는 함수가 아니면 IfThen을 쓰면 좋지 않다. Result := IfThen (BooleanValue, Const1, Const2);
// 만약, IfThen의 2,3번째 파라미터에 복잡한 함수가 꼭 필요하다면, 일반적인 if 구문을 사용해야 한다. if BooleanValue then Result := ComplexFuction1 else Result := ComplexFuction2;
2 Short Circuit Evaluation (단축 평가)를 고려하지 않은 코드를 피하자
Short Circuit Evaluation (단축 평가)
틀린 말이 아니다. 하지만, 문제는...
// 잘못된 사용: if 조건문이 2개일 필요가 없고, 안전하지 않다 if Assigned(SomeList) then begin if SomeList.Count > 1 then begin {...} end; end;
위 코드는 SomeList 오브젝트가 할당되었는 지를 확인한 후에는 안전하고 판단되므로, 이어서 SomeList를 사용한다.
이 코드가 나쁜 이유는 반드시 안전하다고 보장하지는 못하고, 들여쓰기가 많아서 코드가 더 복잡하다.
Short Circuit Evaluation (단축 평가)에 대해 얼마나 알고 있는 지 모르겠지만, 기본적인 불리언(Boolean) 로직을 생각해보자.
False는 어느 것과 and 연산을 해도 결과가 False이다. (따라서 and 연산으로만 연결된 조건인 경우, 각 요소 중에서 어느 하나라도 False가 판단되면, 그 뒤의 다른 요소를 확인할 필요가 없다.
이와 마찬가지로, True는 어느 것과 Or 연산을 해도 결과가 True이고, 이 경우 일단 True가 판단되고 나면, 나머지 요소를 확인할 필요가 없다.
델파이 컴파일러 역시 위와 같은 Short Circuit Evaluation (단축 평가)가 기본 설정이다. 즉, 불리언 값 1개만으로 전체 조건을 결정하기에 충분할 수 있다는 점을 컴파일러가 파악하면 컴파일러는 그 뒤의 조건을 평가하지 않는다.
// 올바른 사용: Short Circuit Evaluation를 활용한 조건 평가 if Assigned(SomeList) and (SomeList.Count > 1) then begin {...} end;
위 코드는 더 간결하고, 더 안전하다. 왜냐하면 1번째 조건이 True인 경우에만 2번째 조건을 평가하기 때문이다. 즉 1번째 조건이 False라면 and 그 뒤에 있는 2번째 조건은 평가하지 않는다.
Short Circuit Evaluation이 델파이 컴파일러의 기본 설정이긴 하지만 델파이 프로젝트 옵션 중 Compiling 페이지에서 직접 설정을 변경할 수 있다.
하지만, 절.대.로 이 옵션을 켜지 않기를 바란다. 즉, "Complete Boolean evaluation (불리언을 모두 평가하기)" 옵션이 False로 되어 있다면 변경하지 말고 그대로 두어라.
이 설정을 변경하면 당신의 코드 중 많은 것들이 깨질 수도 있다.
3 타입 캐스팅을 불필요하게 검사 (Unnecessary type casting test)하는 코드를 피하자
- is 연산자: https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Class_References#The_is_Operator
- as 연산자: https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Class_References#The_as_Operator
// 잘못된 사용: 너무 너무 흔하다. if ISomeObject is TSomeClass then // is 연산자를 통해 자신이 원하는 다른 타입으로도 변환될 수 있는 오브젝트인지 확인 (ISomeObject as TSomeClass).DeSomething; // 잘못된 부분: as 연산자를 통해 자신이 원하는 다른 타입으로도 변환
is 연산자를 사용하여 타입 전환 가능성을 확인하는 경우는 흔하다. 위 코드가 틀린 건 아니다.
문제는 then 뒤에 있는 코드이다. 이미 is로 확인했는데도, 타입 변환(type casting)을 할 때 as 연산자를 사용한다는 점이다.
위와 같이 is와 as를 이어서 사용하면 복잡한 처리를 두번 반복하는 것이다.
is와 as 연산자 모두 처리 로직이 간단하지 않다 (즉 비용이 크다). 그런데 많은 개발자들이 이 사실을 모른다.
// 올바른 사용: if ISomeObject is TSomeClass then // 변환 가능성이 확인되면 TSomeClass(ISomeObject).DeSomething; // 다시 확인하지 않고 바로 타입을 변환
일단 is를 통해서 타입을 바꿀 수 있다는 것이 확인되었다면, as를 사용한 안전한 타입 전환을 하지 않아도 된다. 바로 타입 캐스팅을 해도 안전하다.
즉, 컴파일러에게 점검을 다시 하지 말고 그냥 타입 캐스팅을 하라고 말하면 된다. 그러면 컴파일러는 as에서 사용되는 무거운 점검 작업을 생략하고 그냥 변환한다.
4 Create x Try x Finally x Free 관계
try finally 블록을 사용하면, try 블록 안에서 예외가 발생될 때, try 블록 바로 앞의 지점으로 되돌아간 후에 바로 finally 블록을 실행한다. 우리는 이 finally 블록을 이용하여 리소스가 확실히 해제될 수 있도록 한다.
// 올바른 사용: 오브젝트를 직접 Create 하고나면, 그 바로 밑에서 try 블록을 사용해야 한다. IObject := TYourClass.Create; try {...} finally IObject.Free; end;
그런데,
// 잘못된 사용 try IObject := TYourClass.Create; // 에러가 발생된다면 (코드1) {...} finally IObject.Free; // Access Violation 등이 발생 (코드2) end;
(코드1)은 try 블록 안에 있다. (코드1)에서 예외가 발생하면, finally 블록의 코드가 실행되지만, finally 안에서는 IObject 변수에 담긴 오브젝트를 Free 하지 못한다.
그 이유는 try 블록 바로 앞의 지점으로 되돌아간 후에 finally 블록이 처리되기 때문이다. 그곳은 아직 IObject 변수에 오브젝트가 아예 할당하지도 않은 상태이므로, 그 상태에서 실행되는 Finally 블록 안의 (코드2)는 IObject 변수 안에 할당된 것이 무엇인지 전혀 알 지도 못하기 때문이다.
그 결과 Access Violation 즉 잘못된 포인터 연산 (invalid pointer operation) 에러 메시지를 받게 될 수 있다.
잘못된 예를 하나 더 보자.
// 잘못된 사용 IObject1 := TYourClass.Create; IObject2 := TYourClass.Create; // 에러가 발생된다면 (코드3) try {...} finally IObject2.Free; // 해제됨 IObject1.Free; // 해제되지 않음 end;
위 코드는 오브젝트를 2개 만들어서 사용하지만 ,try finally 블록은 1개만 만들었다. 오브젝트 2개 모두의 리소스를 해제는 finally 블록 1개 안에 모두 넣었다.
이 경우, IObject2를 생성하는 (코드3)에서 에러가 발생하면, 그 아래 바로 try 블록이 있기 때문에 IObject2는 해제되지만, 그보다 더 앞에서 생성된 Object1은 finally 블록의 보호를 받지 못하므로 해제되지 못한다. 그 결과 메모리 누수가 발생한다.
// 올바른 사용 (가장 일반적인 방식): 오브젝트 2개를 만들어서 사용하려면, try finally 블록도 2개를 만들어야 한다. IObject1 := TYourClass.Create; try IObject2 := TYourClass.Create; try {...} finally IObject2.Free; end; finally IObject1.Free; end;
위 코드는 올바르고 가장 일반적인 방식이지만, 보호하려는 리소스가 많아 질 수록 들여쓰기가 많아져서 코드가 복잡해진다.
들여쓰기를 최소화하는 것이 중요하다면,
// 올바른 사용 (가장 일반적이지는 않지만 올바른 코드) IObject1 := nil; IObject2 := nil; try IObject1 := TYourClass.Create; IObject2 := TYourClass.Create; {...} finally IObject1.Free; IObject2.Free; end;
위 코드처럼, 오브젝트 변수에 일단 nil을 넣어둔 후에, try finally 블록 하나를 만들고 try 블록 안에서 모든 오브젝트 변수의 리소스를 생성할 수 있다.
nil을 참조하고 있는 오브젝트 변수를 Free 하는 곳에서 예외가 발생할 것이라고 말하는 개발자가 있을 것이다.
하지만 사실이 아니다. Free는 설계 상 해당 참조가 nil이 아닌 지를 확인한다. 그리고 오직 nil이 아닌 경우에만 Desctuctor를 호출한다.
5 컴파일러의 Warning(경고)과 Hint(힌트)가 전혀 없도록 유지하는 정책
마지막으로 내가 너무 많이 본 흔한 실수를 소개한다.
델파이 컴파일러는 Warning(경고)과 Hint(힌트)롤 제시한다. 컴파일러는 이를 통해 개발자들에게 개선할 수 있는 지점을 알려주는 것이다.
이 지점은 심한 경우에는 버그를 일으키는 원인인 경우이기도 하다.
개발자들은 델파이에서 제시하는 Warning(경고)과 Hint(힌트)롤 무시하는 경향이 있다. 그저 수천개 경고 중 하나라고 생각하여 보지도 않는다. 하지만,
Warning(경고)과 Hint(힌트)를 남겨두지 않는 정책을 유지하기 바란다. 나는 경험 상 이것을 매우 중요하게 생각한다.
이 정책은 당신의 소중한 시간을 지켜줄 것이다. 믿어도 좋다.
Recommended Comments
There are no comments to display.