Search

'Code Contract'에 해당되는 글 2건

  1. 2009.12.14 Code Contract -2
  2. 2009.12.09 Beyond the Assertion, Code Contract -1

Code Contract -2

프로그래밍 2009.12.14 18:33 Posted by 아일레프

이전 포스팅(Beyond the Assertion, Code Contract -1)에서 Assertion Assertion으로서의 쓰임과 ‘계약’으로써 사용되는 Assertion에 대해 알아보고 그 한계에 대해 언급한적이 있습니다. 그 한계란 다음과 같았습니다.

1.     Assertion 또는 if-then throw exception을 계약의 목적으로 사용했을 때, 그 계약이 존재한다는 사실은 Runtime에 그 계약을 어겼을 때에만 확실히 알 수있다.

2.     PreCondition, PostCondition, Class Invariants 각각의 계약의 특성에 따른 고유의 특성이 필요하다.

3.     코드 중복

4.     Interface에 계약을 명시할 수 없다.

5.     상위 Type의 계약이 하위 Type에 상속되지 않을 수도 있다.

6.     컴파일시 바이너리에 포함할지 결정하는 조건을 다양하게 하고 싶다.

이번 포스팅에서는 2번에 대해서 알아보기로 합니다. 여러분은 이 포스팅으로 Code Contract를 사용해 PreCondition, PostCondition, Object Invariants를 지정할 수 있을 겁니다.

각 계약의 특성과 Code Contract기본 메소드들

n  PreCondition

메소드의 PreCondition을 만족하는지 체크하게 되고 대부분 Argument 확인을 위해 사용됩니다. 따라서 이것은 Callee를 위해 Caller가 메소드를 올바르게 사용했는지 확인하는 절차가 됩니다. 이러한 목적으로 사용되는 메소드가 Contract 클래스의 Require 메소드입니다.

public static class Contract

{

            [Conditional("CONTRACTS_FULL")]

           public static void Requires(bool condition);

           public static void Requires<TException>(bool condition) where TException : Exception;

}

Contract.Requires는 “CONTRACTS_FULL” 심볼이 Define되어있을 때만 바이너리에 추가되며, Requires<TException> Debug, Release 모두 빌드에 추가되게 됩니다. CONTRACTS_FULL심볼이 Define되어있을 때 PreCondition을 만족하지 못하면 RuntimeContractException이 발생되며 심볼과 관계없이 Requires<TException>을 만족시키지 못하면 Runtime TException을 발생시키게 됩니다.

Requires PreCondition의 특성에 따라 메소드의 시작부분 또는 변수 선언 바로 다음에만 사용할 수 있습니다. 그렇지 않으면 빌드에러가 발생합니다.

private int Factorial(int number)

{

      Contract.Requires<ArgumentException>(number >= 0);

}

 

n  PostCondition

PreCondition Caller가 메소드를 올바르게 사용했는지 확인하는 절차라면 PostCondition Caller를 위해 Callee 메소드가 계약에 맞게 수행되었는 지 확인하는 절차입니다. 이러한 목적으로 사용되는 것이 Contract클래스의 Ensures메소드입니다.

public static class Contract

{

           [Conditional("CONTRACTS_FULL")]

           public static void Ensures(bool condition);

           [Conditional("CONTRACTS_FULL")]

           public static void EnsuresOnThrow<TException>(bool condition) where TException : Exception;

}

Ensures EnsuresOnThrow모두 CONTRACTS_FULL심볼이 정의 되어있을 때만 바이너리에 포함되게 됩니다. 이것이 PreCondition PostCondition과의 중요한 차이점 입니다. Requires는 배포시에도 필요성이 있지만 – 사용자에게 잘못된 메소드 사용이라는 것을 알려주기 위해 – PostCondition의 경우는 그렇지 않기 때문입니다.

또한 PostCondition는 그 쓰임에 있어서 별도의 Helper 메소드를 필요로 합니다. 예를 들어 Factorial(number)를 호출했다고 했을 때 반환되는 값은 반드시 argument number’보다 같거나 커야합니다. 이를 어떻게 확인할 수 있을까요? 먼저 argument number가 메소드의 body에 의해서 변할 수 있으니 number를 다른 변수에 저장해 놓아야 할 것입니다. 또한 이 값과 return값을 비교해야 하기 때문에 별도의 returnResult 변수가 필요로 합니다. 코드로 다시 설명하겠습니다. 만약 이 PostCondition을 검사하지 않는 다면 Factorial메소드는 다음과 같을 것입니다.

private int Factorial(int number)

{

           Contract.Requires<ArgumentException>(number >= 0);

           if (number == 1 || number == 0) return 1;

           return number * Factorial(number - 1);

}

이 경우 PostCondition을 확인하기 위해 다음과 같은 메소드가 되어야 합니다.

private int Factorial(int number)

{

           Contract.Requires<ArgumentException>(number >= 0);

        if (number == 1 || number == 0) return 1;

        int result = number * Factorial(number - 1);

        Contract.Ensures(result >= number);

        return result;

}

그 결과 불필요한 result라는 변수가 추가되었습니다. 게다가 기존의 코드를 해치는 SideEffect가 발생했습니다. 당신이 PM이라면 당신의 사랑스러운 개발자들에게 이 일을 하라고 말할 수 있겠습니까? 어떻게 기존의 코드를 수정하지 않고 이 일을 할 수 있는 방법이 없을까요? 있습니다. -- 저는 Code Contract에서 이 기능을 가장 사랑합니다. Contract클래스의 몇가지 Helper메소드를 이용해 위와 똑같은 기능을 하는 코드를 다음과 같이 작성할 수 있습니다.

private int Factorial(int number)

{

Contract.Requires<ArgumentException>(number >= 0);

Contract.Ensures(Contract.Result<int>() > Contract.OldValue(number));

       if (number == 1 || number == 0) return 1;

       return number * Factorial(number - 1);

}

Contract.Ensures(Contract.Result<int>() > Contract.OldValue(number)); 의 의미는 이것입니다. 이 메소드의 결과값은 PreCondition state이었을 때의 number값보다 커야한다 OK, 이걸로 Contract.ResultContract.OldValue의 역할을 유추하실 수 있으실겁니다. Contract.Result<T>() return되는 결과값을 의미하는 것이고 Contract.OldValue<T>(T value) PreCondition state였을 때의 value값을 의미하는 것입니다. 그런데 아무리 그렇다고 하더라도 이 코드에는 의문점이 많이 있습니다. 아니 결과 값은 저 코드 뒤에 나오는데 어떻게 이것이 가능하단 말입니까? 여러분은 이것이 단 한가지 방법으로만 가능하다고 추측하실 수 있을 겁니다. , 맞습니다. Code Contract는 빌드 후에 IL코드를 수정하게 됩니다. 이것을 Code Contract Binary Rewriter라고 부르더군요. 이에 대해서는 나중에 자세히 알아보기로 합니다.

 

n  Class Invariants

Class Invariants 또는 Object Invariants라고 불리는 이 Condition은 모든 Public 메소드가 불린 후에 반드시 만족해야 하는 클래스의 조건입니다. 사실 이것은 모든 public 메소드에 동일한 Contract.Ensures메소드를 추가하는 것으로 이 기능을 만족할 수 있습니다. 그러나 이것 역시 귀찮고 힘든 일이지요. 이와 동일한 일을 Code Contract를 사용해 다음과 같이 할 수 있습니다.

[ContractInvariantMethod]

protected void ObjectInvariant()

{

Contract.Invariant(this.x >= 0);

Contract.Invariant(this.y >= 0);

}

위와 같은 코드를 특정 클래스에 사용하면 CONTRACT_FULL 심볼이 정의되어있을 때 Code Contract [ContractInvariantMethod] 애트리뷰트가 있는 메소드 내의 동작을 모든 public 메소드 뒤에 붙혀버립니다. 그리고 만약 이 Class Invariant를 만족하지 못하면 ContractException을 발생시킵니다.

이번 포스팅에서 가장 PreCondition, PostCondition, Class Invariant를 검사하기 위한 Code Contract의 기본 메소드들을 살펴보았습니다. 다음 포스팅에서는 Interface Contract, Contract 상속 그리고 그 이외의 주제들에 대해 말씀 드리겠습니다.

 

 

신고

Beyond the Assertion, Code Contract -1

프로그래밍 2009.12.09 19:30 Posted by 아일레프

블로그에 따로 포스팅 하지 않았지만 사실 이번에 안재우 과 함께 PDC09에 직접 참석하는 영광을 누렸습니다. 앞으로 PDC09에 참여했던 세션 중 인상 깊었던 것 들에 대해 개인적으로 리뷰할 계획입니다. 이 포스팅은 그 첫번째 입니다.

 

첫날의 마지막 세션이었던 Code Contracts and Pex : Power Charge Your Assertions and UnitTests 에서 두 명의 발표자가 “만담”형식으로 재미있게 Code Contract Pex를 함께 사용하는 방법을 소개했습니다.(약간 통통하신 분의 발음이 아주 재미있습니다.) 사실 Code Contracts Pex PDC09에 처음 소개되는 주제가 아닙니다. Pex PDC08에 발표되어 청중의 열혈한 박수를 얻어 낸 바 있으며, Code Contract또한 PDC08에 소개되었으며 Web상에서도 알려진바 있습니다.

 

하지만 이 세션을 볼 당시 전 Code Contract에 대해서는 잘 알지 못했는데 Code Contract Pex보다 더욱 쓸만할 것 같더군요. 어렵지 않게 사용할 수 있으면서도 가벼운 습관을 통해 더 나은 품질의 결과를 얻을 수 있을 거란 생각이 들었습니다. 여러분은 어떤가요? 이 글을 읽으신후 스스로 판단하시기 바랍니다.

 

 

Why the Assertion?

Assertion as Assertion

Assertion, 단언 이라고 표현되는 이 테크닉은 프로그래머가 코드의 특정 지점 내에서 프로그램 논리 흐름상의 반드시 만족해야 하는 ‘가정’을 코드에 넣는 것을 의미합니다. Introduction to Algorithm QuickSort알고리즘에 사용되는 Partition Function을 예로 들어보겠습니다.

 

Partition(A, p, r)

x ß A[r]

i ß p -1

for j ß p to r-1

           do if A[j] <= x

                     then i ß i+1

                                exchange A[i] ßà A[j]

exchange A[i] ßà A[r]

return i+1

 

책에서는 위 알고리즘을 알리는 것으로 끝나는 것이 아니라 다음과 같은 단언이 따라옵니다.

 

For 루프 안에서 다음 3가지 조건이 항상 만족한다.

1.     If p <= k <= i, then A[k] <= x

2.     If i+1 <= k <= j-1, then A[k] > x

3.     If k = r, then A[k] = x

 

3가지 expression이 바로 알고리즘 논리 상의 Assertion입니다. 만약 for 루프 내에서 1,2,3번 중 하나라도 만족하지 않는 것이 있다면 이 알고리즘은 올바르지 않은 것입니다.

다른 예로 최근 포스팅을 들 수 있겠습니다. 해당 포스팅을 보면 알고리즘 나열 도중 -**단언-으로 시작되는 줄이 보입니다. 그들이 제가 생각 했을 때 반드시 참이어야 하는 논리상의 가정입니다.(실제로는 저 단언중 몇가지가 옳지 않아 알고리즘을 수정해야 했습니다.)

프로그래머는 System.Diagnostics Debug 클래스의 Assert( bool condition)메소드를 이용해 Assertion을 코드에 삽입할 수 있습니다. Debug클래스의 Assert는 조건를 만족하지 않으면 메시지와 함께 프로그램을 종료시켜 버립니다. 이런 Assertion의 사용으로 인해 프로그래머는 자신의 알고리즘에 대해 확신을 가질 수 있고, 만약 논리상의 오류가 있을 때 그 원인을 보다 쉽게 추리할 수 있습니다.

 

Assertion as Contract.

Assertion을 프로그래머간의 ‘계약’으로 사용할 수 있습니다. Argument Check가 좋은 예입니다. 관습적으로 Public 메소드의 Argument Check Assertion이 아닌 Exception으로써 명시합니다. 예를 들어 Math 클래스의 Round메소드의 Signiture는 다음과 같습니다.

public static double Round(double value, int digits, MidpointRounding mode);

Visual Studio 상에서 위 메소드의 주석을 펼쳐보면 다음을 발견할 수 있습니다. Digits 메소드가 0보다 작거나 15보다 크면 Exception을 발생시킨다는 것입니다. 즉 이 메소드는 digits argument 0<= digits <= 15라는 전제 내에서 동작하며, 이 메소드를 이용하는 사용자가 이 전제를 지켜주기를 요구하고 있는 것입니다.

//   System.ArgumentOutOfRangeException:

//     digits is less than 0 or greater than 15.

 

이와 같이 프로그래머는 자신의 코드를 최초에 디자인 할 때 반드시 만족해야 할 “최소한의 조건”를 자신 또는 다른 사용자에게 노출 시킬 필요가 있습니다. 이것을 명시하는 것을 “계약”이라 하겠습니다. 이러한 계약의 종류로 크게 다음 3가지를 들 수 있겠습니다.

 

A.    PreCondition

특정 작업이 시작되기 전에 반드시 지켜져야 하는 전제 조건을 의미합니다. 이미 설명한 메소드의 Argument체크가 이에 해당됩니다.

메소드의 시작에 계약 코드를 삽입하며, public 메소드에는 if-then throw exception 코드가 사용되며 private 메소드에는 Assert가 사용됩니다.

 

B.     PostCondition

특정 작업이 끝난 후에 보장되어야 하는 조건을 의미합니다. 예를 들어 Arc Sine 메소드를 디자인 한다고 했을 때 결과 값이 -π/2 ≤θ≤π/2 를 만족하는 Radian값인 θ이라는 것을 보장할 수 있어야 합니다. 메소드의 마지막에 계약 코드를 사용하게 됩니다.

 

C.     Object Invariants

이것은 클래스 내의 모든 특정 작업이 종료된 후에 항상 지켜져야 하는 불변의 조건을 의미합니다. 예를 찾기 힘들어 Reference로 사용한 Programmin with Assertion의 예를 그대로 말씀드리겠습니다. 여러분이 어떤 필요에 있어서 Balanced Binary Tree 를 만들어야 한다고 가정하겠습니다. 이 때 Balanced Binary Tree는 특정 연산이 이루어지기 전이나 후에 “Balanced, Binary”라는 조건을 항상 만족해야 합니다. Tree내의 모든 Node Node자신의 자식의 수가 0보다 같거나 크고, 2보다 같거나 작다는 조건을 항상 만족해야 하며(Binary), 모든 Node Node 자신의 자식들(많아야 2)간의 Depth의 차이가 1보다 같거나 작아야한다는 사실을 만족해야 합니다.(Balanced)

모든 public 코드 뒤에 Object Invariants Assert코드가 존재하는 메소드를 호출하는 것으로 이 작업을 수행할 수 있습니다.

 

 

Break Point!

여러분은 여기까지 읽었을 때 Assertion -- if-then throw 를 제외한 순수 Assertion -- 이 프로그램을 사용하는 사람을 위한 것이 아니라 프로그램을 개발하는 사람들을 위한 것이라고 추측 할 수 있습니다. 그리고 그 추측은 완전히 옳습니다. 또한 이 추측은 또 한가지 결론으로 우리를 이끌어 가는데, 바로 Release시에는 Assert는 존재해야할 이유가 없다는 것입니다. 따라서 보통 아래와 같이 사용합니다.

#if DEBUG

AssertCode..

#end if

하지만 매번 #if DEBUG를 쓴다는 것도 힘든 일이지요. 따라서 우리를 배려한 Microsoft는 Debug.Assert 메소드가 오직 Debug시에만 컴파일되게 했습니다.

 

 

Why the Code Contract?

Code Contract as Assertion

Code Contract는 이름에서 알 수 있듯이 “계약”을 위한 필요와 목적에서 출발했지만 Assertion으로써의 기능도 할 수 있습니다. Contract클래스의 Assert, Assume 메소드가 그것입니다. 이 기능은 기존의 Debug.Assert의 기능과 완전히 동일합니다.

public static void Assert(bool condition);

public static void Assume(bool condition);

이는 물론 DEBUG시에만 컴파일 대상이됩니다. Assert Assume의 차이점은 이 후에 설명하도록 하겠습니다.

 

Code Contract as Contract

Assertion Assertion으로써의 기능만을 수행한다면 아무런 불만이 없습니다. 그러나 Assertion Contract의 기능까지 함께 할 때 몇 가지 불만이 따라오게 됩니다. 대충 생각해 보면 다음과 같습니다.

 

1.     Assertion또는 if-then throw exception를 계약의 목적으로 사용했을 때, 그 계약이 실재로 존재한다는 사실은 Runtime에 그 계약을 어겼을 때에만 확실하게 알 수 있습니다. 이것은 분명히 비극입니다.

2.     PreCondition, PostCondition, Class Invariants 각각의 계약의 특성에 따른 고유의 기능이 필요한데 기존의 Assertion, if-then throw exception은 그러한 요구를 만족시켜줄 수 없습니다.

3.     그놈의 코드 중복 때문입니다.

4.     Interface에 계약을 명시하고 싶습니다.

5.     상위 Type의 계약이 하위 Type에 ‘반드시’ 상속되었으면 좋겠습니다.

6.     컴파일시 바이너리에 포함될지 결정하는 조건을 다양하게 하고 싶습니다.

 

1번의 필요성 때문에 Code Contract 2가지 유용한 기능을 가지고 있습니다. Document Generation Static Verification 기능이 그것입니다. Documentation Generation으로 개발자들은 계약을 보기도 싫은 코드 조각과 런타임시에만 존재하는 Error 메시지가 아닌 분명한 문서로 받아 볼 수 있게 되었습니다.(문서가 더 싫은 가요? ^^) 또한 Static Verification으로 계약을 잘 지켜서 코딩 했는지 빌드 시에 Warning 메시지로 알 수 있습니다.

 

2번의 필요성 때문에 -- 2번의 필요성에 대한 자세한 설명은 다음 포스팅으로 미루겠습니다. -- Code Contract Precondition, PostCondition, Object Invariants에 특화된 별개의 메소드를 고안했습니다. Require, Ensure, Invariant가 그것입니다. 각각의 설명은 다음 포스팅에 자세히 소개하겠습니다.

 

3번은 사실 4번과 5번을 포함합니다. Assertion만으로 Object Invariants를 구현한다고 상상해보시기 바랍니다. 모든 public 메소드의 마지막에 Object Invariants를 확인하기 위한 Assert 코드를 추가해야 할 것입니다. 이러한 중복은 당연히 사라져야 합니다.

 

4번의 필요성은 상당히 중요합니다. Interface는 물론 다형성을 위한 것이지만 계약은 통일되어야만 합니다. 각각의 Interface 구현 클래스가 다른 계약을 가지는 것을 절대 허용하면 안됩니다. 따라서 Interface에 계약을 명시해야 하며, 그 계약은 모든 구현 클래스에 적용되어야만 합니다.

 

5 입니다. virtual 메소드를 가진 Super클래스가 있다고 가정합니다. Virtual 메소드안에 계약코드를 넣었는데 sub클래스에서 이것을 override해 계약 코드를 지우는 것을 허용하면 안됩니다. 때에 따라서 정해진 계약을 ‘강화’시키는 것을 막아야 할 필요성도 있을 수 있습니다.

 

 

Next

Code Contract가 등장하게 된 배경에 대해 간략히 설명했습니다. 여러분이 스스로 생각하고, 상상할 수 있는 권리를 빼앗고 싶지 않기 때문에 Code Contract가 어떻게 이 일을 해냈는지 말하는 것은 다음 포스팅으로 미룹니다.
감사합니다.
 

 

 

References

Code Contracts and Pex : Power Charge Your Assertions and UnitTests

MSDN Magazine : Code Contracts

Code Contract

Code Contract Manual

Programming With Assertions

저작자 표시
신고


 

티스토리 툴바