계약에 의한 디자인이란?
개발자가 코드를 짜고 직접 실행하는 경우도 있지만, 작성된 코드를 다른 레이어나 컴포넌트에서 호출하는 경우도 있다. 계약에 의한 디자인은 이런 경우 어떻게 컴포넌트들 간 교류가 이루어져야 하는 가에서 출발한다.
여기서 컴포넌트란, 특정 기능을 수행하거나 캡슐화되어 기능을 제공하는 소프트웨어 모듈, 클래스, 함수, 혹은 코드를 의미한다.
함수를 사용할 클라이언트에게는 API(Application Programming Interface)를 호출하게 한다.
API를 디자인할 때 예상되는 입출력과 부작용을 문서화 해야한다. 그러나 런타임시 소프트웨어의 동작까지 강제할 수는 없기 때문에 코드가 정상적으로 동작하기 위해 필요한 것과 클라이언트가 반환 받게 될 형태는 모두 디자인에 포함이 되어야 한다. 이것이 계약의 개념이다. 계약은 컴포넌트 간의 통신 중 반드시 지켜야 할 몇 가지 규칙을 강제하는 것이다.
디자인 원칙의 주된 가치는 문제가 있는 부분을 효과적으로 식별하는 데 있다.
계약을 정의함으로써 런타임 오류가 발생했을 때 코드의 어떤 부분이 손상되었는지 그리고 무엇이 계약을 파손시켰는지 명확해진다.
DbC에서 중요한 것은 무엇을 기꺼이 검증할 것인지 신중히 검토하는 것이다. 단순히 파라미터 타입만 검사하는 것은 큰 의미가 없는 검증일 수 있다. 객체의 속성과 반환 값을 검사하고 이들이 유지해야하는 조건을 확인하는 작업을 해야한다.
사전 조건 precondition
•
코드가 실행되기 전에 체크해야 하는 것들
•
함수가 진행되기 전에 처리되어야 하는 모든 조건을 체크한다.
•
일반적으로 파라미터에 제공된 데이터의 유효성을 검사
•
많이 할 수록 좋다.
•
데이터베이스, 파일, 이전에 호출된 다른 메서드 검사 등
•
호출자에게 부과된 임무
사후조건 postcondition
•
사전조건과 반대로 함수 반환값의 유효성 검사가 수행
•
호출자가 이 컴포넌트에서 기대한 것을 제대로 받는지 확인하기 위해 수행
불변식 invariant
때로는 함수의 docstring에 불변식에 대해 문서화하는 것이 좋다
•
함수가 실행되는 동안에 일정하게 유지되는 것으로 함수 로직에 문제가 없는 지 확인하기 위한 것
•
키, 나이 등의 변수는 항상 양수를 담아야한다.
부작용 side effect
•
선택적으로 코드의 부작용을 docstring에 언급하기도 한다
이는 책임 소재를 파악하기 위함이다. 이후 핵심 코드가 실행되었을 때 생길 수 있는 문제를 방지해야한다. 사전조건 검증에 실패하면 클라이언트 결함이고, 사후조건 검증에 실패하면 특정 모듈이나 제공 클래스 자체에 문제가 있는 것이다.
사전 조건 (precondition)
사전조건은 함수나 메서드가 제대로 동작하기 위해 보장해야 하는 모든 것을 말한다. 제공하는 데이터가 적절한 형태인가?
•
null check, type check
두 가지 접근 방법
1.
클라이언트에서 체크, 관대한(tolerant) 접근법, 함수 입장에서는 어떠한 데이터라도 수용
2.
함수가 자체적으로 로직 수행 전에 검사, 까다로운 접근법(demanding)
일반적으로 후자가 더욱 안전하고 견고하며 업계에서도 널리 쓰이는 방법
어떤 방식을 택하든 중복 제거의 원칙을 기억해야한다. 사전 조건은 양쪽에서 검증을 해선 안 된다.
이후 다룰 DRY 원칙과 관련이 있다.
사후조건(postcondition)
사후조건은 메서드 또는 함수가 반환된 후의 상태를 강제하는 것이다.
사전조건에 맞는다면, 사후조건은 특정 속성이 보존되어야한다.
사후조건까지 검증까지 통과하고 클라이언트가 반환 받은 객체는 아무 문제 없이 사용 가능해야한다.
파이썬스러운 계약
PEP-316 (Programming by Contract for Python)은 계약을 위한 원칙을 제시한다.
20년 전에 Deferred 된 것으로 봐서 심도 깊게 읽을 필요는 없을 것 같다.
어플리케이션 특성에 따라 검사에 실패할 경우 RuntimeError나 ValueError를 발생기키자.
필요하다면 사용자 정의 예외를 만드는 것도 좋을 것이다.
코드는 가능한 한 격리된 상태를 유지하는 것이 좋다. 즉, 사전조건에 대한 검사와 사후조건에 대한 검사 그리고 핵심 기능에 대한 구현을 구분하는 것이다. 더 작은 함수를 생성하여 해결할 수도 있고, 데코레이터를 사용하는 것도 좋은 방법이다.