Contract 클래스
namespace System.Diagnostics.Contracts
{
public static class Contract
{
// 계약 위반 이벤트
public static event EventHandler<ContractFailedEventArgs> ContractFailed;
// 전제 조건
[Conditional("CONTRACTS_FULL")]
public static void Requires(bool condition);
[Conditional("CONTRACTS_FULL")]
public static void Requires(bool condition, string userMessage);
public static void Requires<TException>(bool condition, string userMessage) where TException : Exception;
public static void Requires<TException>(bool condition) where TException : Exception;
[Conditional("CONTRACTS_FULL")]
public static void EndContractBlock();
// 사후 조건
[Conditional("CONTRACTS_FULL")]
public static void Ensures(bool condition);
[Conditional("CONTRACTS_FULL")]
public static void Ensures(bool condition, string userMessage);
[Conditional("CONTRACTS_FULL")]
public static void EnsuresOnThrow<TException>(bool condition, string userMessage) where TException : Exception;
[Conditional("CONTRACTS_FULL")]
public static void EnsuresOnThrow<TException>(bool condition) where TException : Exception;
// 단언
[Conditional("DEBUG")]
[Conditional("CONTRACTS_FULL")]
public static void Assert(bool condition, string userMessage);
[Conditional("DEBUG")]
[Conditional("CONTRACTS_FULL")]
public static void Assert(bool condition);
// 가정
[Conditional("DEBUG")]
[Conditional("CONTRACTS_FULL")]
public static void Assume(bool condition);
[Conditional("DEBUG")]
[Conditional("CONTRACTS_FULL")]
public static void Assume(bool condition, string userMessage);
// 객체 불변식
[Conditional("CONTRACTS_FULL")]
public static void Invariant(bool condition);
[Conditional("CONTRACTS_FULL")]
public static void Invariant(bool condition, string userMessage);
// 값 확인
public static T Result<T>();
public static T ValueAtReturn<T>(out T value);
public static T OldValue<T>(T value);
// Quantifier
public static bool Exists<T>(IEnumerable<T> collection, Predicate<T> predicate);
public static bool Exists(int fromInclusive, int toExclusive, Predicate<int> predicate);
public static bool ForAll<T>(IEnumerable<T> collection, Predicate<T> predicate);
public static bool ForAll(int fromInclusive, int toExclusive, Predicate<int> predicate);
}
}
- Contrect (.NET 4.0)
* 코드 계약(Code Contracts): Debug, Trace의 Assert과 예외. 위 두 방식을 하나의 통합된 방식으로 지원
* 일반 단언 뿐만 아니라 계약 기반 단언도 적용 가능
계약
- 계약의 장점
1. XML 문서화 파일에 기록되게 할 수 있음
SandCastle 같은 도구로 프로그램을 문서화 할 때, 계약의 세부사항을 문서에 포함시킬 수 있음
* Code Contracts 탭
1. Contract Referenece Assembly: Build
2. Emit contracts into XML doc file: 체크
2. 정적 계약 검증 도구로 프로그램의 정확성을 검증 할 수 있음
3. 사용하기 쉬움
4. 객체 불변식(Object Invariant)을 지원함 - 코드의 중복 감소, 단언을 견고하게 강제
5. 인터페이스나 추상 메서드에 지정 가능 - 가상 메서드에 지정한 조건을 파생 클래스에서 실수로 수정 할 일 없음
6. 계약 위반 처리 방식을 다양하게 커스텀 가능
7. 계약 위반을 항상 기록 가능(호출 스택의 상위의 예외 처리부가 삼키는 경우에도 기록 가능)
- 계약의 단점
* 컴파일 이후에 어셈블리를 변조하는 이진 실행 파일 변환기(Binary rewriter)에 의존
1. 빌드 공정이 느려짐
2. C# 컴파일러 호출에 의존하는 서비스들의 작동을 복잡하게 만듬
3. 보안에 민감한 점검을 강제하는 데에는 사용할 수 없음 (ContractFailed 이벤트 처리로 우회 가능)
- 계약의 종류
* 전제조건: 함수 시작 시 검증됨
* 사후조건: 함수 반환 직전에 검증
* 단언: 코드 안에 해당 단원문 위치에서 검증
* 객체 불변식: 클래스의 모든 공용 함수의 실행 이후 검증
- etc...
* 계약은 Contrect 클래스의 메서드들을 호출하는 문장으로만 구성 됨. 따라서 코드 계약은 언어에 독립적임
* 메서드 뿐만 아니라 생성자, 속성, 인덱서, 연산자 같은 다른 함수들에도 사용 할 수 있음
CONTRACTS_FULL
- Constact 클래스의 모든 메서드들은 [Conditional("CONTRACTS_FULL")] 특성을 가지고 있음
- Code Contract 탭에서 Perform Runtime Contract Cheacking을 체크하면 이 기호가 자동으로 정의됨
<CodeContractsEnableRuntimeChecking>true</CodeContractsEnableRuntimeChecking>
- Requires<TExcpetion> 조건들은 CONTRACTS_FULL과 무관
Code Contract 탭에서 Contract Reference Assembly를 (none)으로 설정해줘야 계약 코드를 지울 수 있음
<CodeContractsCustomRewriterAssembly>doNotBuild</CodeContractsCustomRewriterAssembly>
이진 실행 파일 변환기
- 계약이 담긴 코드는 이진 실행 파일 변환기(ccrewrite.exe)를 통해서 재변경
- VS에서 코드 계약 점검이 활성화 되어 있다면 이 과정이 자동으로 실행
1. 사후 조건들과 객체 불변식을 적절한 장소로 이동
2. 재정의된 메서드들의 조건들과 객체 불변식들을 점검하는 코드를 추가
3. Contract 메서드 호출문들을 실행시점 계약 클래스에 있는 메서드들을 호출하는 코드(__ContractsRuntime)로 대체
- 변화되지 못한 Contract 메서드들은 예외를 던짐
- /rw 혹은 Code Contract 탭에서 Custom Rewriter Methods를 통해 실행시점 계약 클래스를 직접 작성 가능
<CodeContractsCustomRewriterAssembly>Assembly</CodeContractsCustomRewriterAssembly>
<CodeContractsCustomRewriterClass>Class</CodeContractsCustomRewriterClass>
[Pure], 순수성
- 계약 메서드들을 호출 할 때 인수로 전달하는 표현식에서 호출하는 모든 함수는 반드시 순수해야 함
- 순수하다는 것은 부수 효과가 없다는 것 - 어떤 필드의 값도 변경하지 말아야 함
- 인수 표현식에서 호출하는 모든 함수가 순수하다는 점을 이진 변환기에 알려주기 위해 [Pure] 특성을 부여
- 순수한 것들
1. 모든 속성 조회(get) 접근
2. string, Contract, Type, System.IO.Path를 비롯한 몇몇 .NET 형식
3. 모든 C# 연산자
4. LINQ의 질의 연산자
5. [Pure] 특성이 부여된 대리자를 통해서 호출되는 메서드들 (ex: Comparison<T>, Predicate<T> 대리자들 등)
전제조건
- 전제조건은 단언과 비슷하지만, 함수에 관한 발견 가능한 사실을 생성하는데 강점이 있음
(문서화 도구, 정적 점검 도구는 컴파일된 코드에서 해당 사실을 추출해서 활용)
- 부모 클래스의 가상 메서드에 전제조건들이 있을 때,
파생 클래스가 그 매서드를 재정의해도 부모의 전제조건들이 강제됨
(전제조건의 재정의가 가능하다면 다형성의 원칙이 깨짐)
- 인터페이스 멤버들에 정의된 전제조건들은 암묵적으로 구체 클래스의 구현들에 주입 됨
- 하나의 함수에 여러개의 전제조건이 있을 수 있음
- 바람직한 전제조건
1. 클라이언트(호출자)가 손쉽게 검증 할 수 있도록 긍정적인 조건이어야 함
2. 메서드 자체와 접근 제한이 같거나 덜한 자료와 함수에만 의존해야 함
3. 위반이 곧 버그를 뜻하는 것이어야 함
- 전제조건 vs 단언(예외)
1. 조건 실패가 항상 클라이언트의 버그를 뜻한다면 전제조건
2. 조건 실패가 비정상적 조건을 뜻하며 클라이언트의 버그일 수도 있고, 아닐 수도 있다면 예외
- 인수 점검 코드(예외 처리)와 전제조건을 섞어 쓸 수 있으나, 전제조건은 항상 인수 점검 뒤에 넣어야 함
Requires 메서드
- 함수에 시작에서 Contract.Requires를 호출하면 전제조건이 강제됨
namespace Practice
{
class Program
{
public static void Main(string[] arg)
{
string a = "";
ToProperCase(a);
}
static string ToProperCase(string s)
{
// 전제조건: 스트링이 null이거나 비어있지 않아야 함
Contract.Requires(!string.IsNullOrEmpty(s));
// 메서드 작업...
return s;
}
}
}
Requires<TException> 메서드
// 고전적 접근 방식
static void SetProgressClassic(string message, int percent)
{
if (message == null)
{
throw new ArgumentNullException("message: null");
}
if (percent < 0 || percent > 100)
{
throw new ArgumentOutOfRangeException("percent: Out of Range");
}
// 내용 처리
}
// 현대적인 접근 방식
static void SetProgressModern(string massage, int percent)
{
Contract.Requires(massage != null);
Contract.Requires(percent >= 0 && percent <= 100);
// 내용 처리
}
- 코드 계약 기능과 고전적 오류 점검 패턴과의 충돌
- 고전적 인수 점검을 강제하는 어셈블리가 갖추어진 상태에서 새로운 메서드를 작성하면 라이브러리의 일관성이 깨짐
- 기존 메서드를 계약을 사용하는 버전으로 바꾸기엔 많은 시간이 소모되고, 예외에 의존하는 호출자들이 있을 수 있음
- Contract.Requires의 제네릭 버전을 호출할 경우, 계약 위반시 던질 예외의 형식을 지정 할 수 있음
- 겉으로 보기엔 구식 점검 방식처럼 행동하면서도 코드 계약의 장점을 얻을 수 있음
// 제네릭 버전을 사용한 처리
static void SetProgress(string massage, int percent)
{
Contract.Requires<ArgumentNullException>(massage != null);
Contract.Requires<ArgumentOutOfRangeException>(percent >= 0 && percent <= 100);
// 내용 처리
}
- 계약 점검 수준을 ReleaseRequires로 지정 할 경우 Requires<TException> 호출만 남고 나머지 계약 점검은 사라짐
<CodeContractsRuntimeCheckingLevel>ReleaseRequires</CodeContractsRuntimeCheckingLevel>
EndContractBlock 메서드
- 전통적인 인수 점검 코드를 리펙터링 하지 않고도 코드 계약의 장점을 도입 할 수 있음
- 인수 점검(If <조건> throw <표현식>;)을 수행한 후에 이 메서드를 호출하기만 하면 됨
- 이진 변환기는 코드를 Requires<TException> 형태로 변환시킴
// EndContractBlock()
static void SetProgressWithEndContractBlock(string message, int percent)
{
if (message == null)
{
throw new ArgumentNullException("message: null");
}
if (percent < 0 || percent > 100)
{
throw new ArgumentOutOfRangeException("percent: Out of Range");
}
Contract.EndContractBlock();
// 내용 처리
}
사후조건 (Postcondition)
- 사후조건: 메서드 종료 시 반드지 참이어야 하는 조건을 강제
- 사후조건은 함수 자체의 오류를 검출(단언과 비슷)
- 사후조건 표현식에서는 객체의 전용 상태에 접근해도 괜찮음. 단, 가상 메서드의 사후조건에서는 사용하지 말아야 함
- 재정의된 메서드에서 기반 메서드에 정의된 사후조건을 바꾸거나 무효화 할 수 없음, 추가는 가능함
- 이진 변환기는 사후조건들을 메서드의 종료 지점으로 옮김
- 메서드가 일찍 반환되어도 점검되지만, 처리되지 않은 예외 때문에 일찍 종료되는 경우엔 점검되지 않음
Ensures 메서드
- 메서드 종료 시 반드시 참이어야 하는 조건을 강제
EnsuresOnThrow<TException> 메서드
- 메서드 종료 시 반드시 참이어야 하는 조건과 예외 발생 여부를 강제
즉, 조건을 만족하지 못하고, 지정된 예외가 발생(그리고 Catch되지 않아야 함)하면 ContractException 발생
namespace Practice
{
class Program
{
public static void Main(string[] arg)
{
Test(null);
}
static void Test (string errorMessage)
{
Contract.EnsuresOnThrow<WebException>(errorMessage != null);
throw new WebException("error!");
}
}
}
Result<T> 메서드
- 사후조건은 함수의 실행이 끝난 뒤 평가되므로, 반환값을 점검 하고 싶을 때 사용
namespace Practice
{
class Program
{
public static void Main(string[] arg)
{
Console.WriteLine(GetOddRandomNumber());
}
static int GetOddRandomNumber()
{
// 여기선 항상 홀수가 나와서 사후조건은 무조건 참임
Contract.Ensures(Contract.Result<int>() % 2 == 1);
Random _random = new Random();
return _random.Next(100) * 2 + 1;
}
}
}
ValueAtReturn<T> 메서드
- ref나 out 매개변수의 최종값을 돌려줌
OldValue<T> 메서드
- 메서드 매개변수의 원래 값(매개변수가 처음 메서드에 들어와서, 수정되지 않은 본연의 값)을 돌려줌
Assert 메서드
- 함수의 어느 곳에서도 Assert를 호출해서 어떤 조건을 단언 할 수 있음
- 선택적 두번째 인수에 단언 실패시의 오류 메시지를 지정 할 수 있음
- 단언문들은 이진 변환기가 변환하지 않고 그대로 둠
- Debug.Assert 대신의 Contract.Assert
1. 코드 계약 기능이 제공하는 좀 더 유연한 실패 처리 메커니즘 활용 가능
2. Contract.Assert 위반 여지가 있는 코드를 정적 점검 도구로 잡아낼 수 있음
Assume 메서드
- 가정(Assumption)을 표현하는 메서드
- 실행 시점에서 Assert와 동일하게 동작
- 정적 점검 도구는 단언은 깐깐하게 점검하지만, 가정에 대해서는 문제를 제기하지 않음
- 단언 중에 정적으로 검증할 수 없는 것에 대해 대신 사용
객체 불변식 (Object Invariant)
- 하나의 클래스에 대해 하나 이상의 객체 불변식 메서드를 지정 가능
- 모든 Public 함수의 끝에서 자동으로 실행됨
- 객체가 일관된 내부 상태를 유지하고 있음을 뜻하는 조건 단언 가능
- 방지가 아닌 조건을 검출
- 매개변수, 반환값이 없는 메서드 작성 후 [ContractInvariantMethod] 특성 부여
- Invariant는 [ContractInvariantMethod] 특성이 부여된 메서드에서만 호출 가능
- 객체 불변식 메서드는 오직 Invariant로만 구성되어야 함
namespace Practice
{
class Program
{
public static void Main(string[] arg)
{
Test tt = new Test();
tt.PublicCal(1);
}
class Test
{
int x;
public int X
{
get { return x; }
set { x = value; /* ObjectInvariant(); */ }
}
[ContractInvariantMethod]
void ObjectInvariant()
{
Contract.Invariant(x >= 0);
}
public void PublicCal(int x)
{
this.x = -x;
// ObjectInvariant();
}
void Cal(int x)
{
this.x = -x;
// Public 함수가 아니어서
// 여기에는 ObjectInvariant();가 추가되지 않음
}
}
}
}
인터페이스와 추상 메서드의 코드 계약
- 인터페이스 멤버와 추상 메서드에 계약 조건들을 부여할 수 있음
- 이진 실행 파일 변환기는 조건들을 자동으로 구체 구현 클래스의 멤버들에 주입
// 제약 조건을 위한 계약 클래스
// 인스턴스화 되지 않음(생성자를 만들어도 작동하지 않음)
[ContractClassFor(typeof(ITest))]
sealed class ContractForITest : ITest
{
string ITest.Massage { get; set; }
// 반드시 명시적 구현을 사용
// 이 메서드에서 제약 조건을 추출하여 실제 구현에 주입
int ITest.Process(string s)
{
// 인터페이스의 다른 멤버를 참조하기 위한 임시 변수
ITest test = this;
// 실제로 적용할 조건들
Contract.Requires(s != test.Massage);
// 컴파일러를 통과하기 위한 명목상의 값
// 실제로 실행 안 됨
return 0;
}
}
- 추상 클래스를 위한 계약 클래스는 sealed 대신 abstract로 선언
계약 위반 처리 방식
- 계약 조건을 만족하지 못했을 때, 대화 상자를 띄울 것인지 아니면 ContractException 예외를 던질 것인지 설정
- /throwonfailure, Code Contract 탭에서 Assert on Contract Failure 체크 해제
<CodeContractsRuntimeThrowOnFailure>true</CodeContractsRuntimeThrowOnFailure>
- 대화상자: 메시지 표시, 실행 취소, 오류 무시, 디버거로 진입 옵션
1. CLR이 다른 호스트 프로그램(ex: SQL Server, Exchange) 안에서 실행되는 중이면
대화상자 대신 호스트의 상향 보고 방침(Escalation Policy) 발동
2. 위 경우를 제외하고 어떤 이유로 인해 사용자에게 대화 상자를 띄울 수 없으면 Environment.FailFast 메서드 호출
- 디버그 빌드에서는 대화 상자를 띄우는 방식이 유용
1. 프로그램을 다시 실행하지 않고도 진단하고 디버깅 하기 편함
일반적인 예외와 달리 계약 위반은 거의 항상 코드에 버그가 있음을 의미
2. 스택의 더 상위에 있는 호출자가 예외를 삼켜도 계약 위반 사항을 개발자가 알 수 있음
- throw ContractException
1. 릴리즈 빌드에서, 예외를 스택 위쪽으로 떠오르게 해서 예기치 못한 다른 모든 예외와 같은 방식으로 처리
2. 오류 기록 공정이 자동화된 단위 검사(Unit Testing) 환경에서 해당 예외가 자동으로 기록되게 함
- ContractException은 공용 형식이 아니라서 catch 블록에서 이 예외를 구체적으로 지정할 수 없음
- 일반적인 최종 예외 처리부에서 잡도록 만들어졌지, 특정해서 잡을 용도로 만든 것은 아님
ContractFailed 이벤트
- 프로그램이 계약을 위반하면 CLR은 다른 행동을 취하기 전에 ContractFailed 이벤트를 발동
- 이벤트 처리부에 전달되는 이벤트 객체로부터 오류의 세부사항을 알아낼 수 있음
- 이벤트 처리부에서 SetHandled를 호출함으로써
이후에는 ContractException이 던져지거나 대화상자가 나타나지 않게 할 수 있음
static class Test
{
static void foo()
{
// 계약
// ...
Contract.ContractFailed += Contract_ContractFailed;
// 메서드 로직
// ...
}
private static void Contract_ContractFailed(object sender, ContractFailedEventArgs e)
{
string failureMessage = $"{e.FailureKind}: {e.Message}";
// 단위 검사 프레임워크를 이용해서 failureMessage 기록
// ...
// 다른 이벤트 구독자의 모든 SetHandled 호출의 효과를 무력화
// 즉, 모든 이벤트 처리부가 실행된 후 ContractExcption이나 대화상자가 항상 발생
e.SetUnwind();
}
}
'C#' 카테고리의 다른 글
[C#] PerformanceCounter, PerformanceCounterCategory (0) | 2023.10.28 |
---|---|
[C#] EventLog (Windows 이벤트 로그) (0) | 2023.10.26 |
[C#] StackTrace, StackFrame (0) | 2023.10.24 |
[C#] Debugger (0) | 2023.10.22 |
[C#] Debug, Trace (0) | 2023.10.18 |
[C#] WeakReference (0) | 2023.10.14 |
[C#] GC, GCSettings, 쓰레기 수거(Garbege Collection) (0) | 2023.10.12 |
[C#] IDisposable (Dispose), 종료자(Finalizer) (0) | 2023.10.10 |