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 상속 그리고 그 이외의 주제들에 대해 말씀 드리겠습니다.

 

 

신고


 

티스토리 툴바