소프트웨어 개발에서 '버그가 없는 코드'는 환상에 가깝지만, '예측 가능한 코드'는 충분히 달성 가능한 목표입니다. 시스템의 규모가 커질수록 예상치 못한 입력값이 전체 아키텍처를 뒤흔드는 경우가 많습니다. 이를 방지하기 위해 설계 단계부터 객체 간의 약속을 명시하는 '계약에 의한 설계(Design by Contract, DbC)' 개념을 실무에 녹여내는 방법을 살펴봅니다.
1. 개념 소개: 계약에 의한 설계란 무엇인가
계약에 의한 설계는 베르트랑 마이어(Bertrand Meyer)가 창안한 개념으로, 소프트웨어의 각 컴포넌트가 서로의 요구사항과 보장사항을 명확한 '계약'으로 정의해야 한다는 원리입니다. 여기서 계약은 세 가지 요소로 구성됩니다.
- 사전 조건(Pre-conditions): 루틴이 호출되기 전에 클라이언트가 만족시켜야 하는 조건.
- 사후 조건(Post-conditions): 루틴이 종료될 때 루틴이 보장해야 하는 결과.
- 불변 조건(Invariants): 객체가 항상 유지해야 하는 상태의 제약.
현대 개발 환경에서 이 개념이 중요한 이유는 복잡도가 기하급수적으로 증가하는 마이크로서비스 아키텍처에서 서비스 간의 데이터 흐름이 파편화되기 때문입니다. 무조건적인 예외 처리를 넘어, 인터페이스 수준에서 호출자와 구현자의 책임을 분리할 수 있습니다.
2. 핵심 원리: '요리사'와 '손님'의 비유
식당의 주방을 떠올려봅시다. 주방장(함수)은 요리를 내놓기 위해 식재료(입력)가 신선해야 한다는 '사전 조건'을 겁니다. 손님(호출자)은 돈을 지불하고 맛있는 음식(결과)을 기대하는 '사후 조건'을 갖습니다.
만약 재료가 썩어있다면 주방장은 조리를 거부해야 합니다(방어적 프로그래밍). 이때 단순히 "오류 발생"이라고 하지 않고, "채소가 신선하지 않아 조리할 수 없습니다"와 같이 명확한 계약 위반 사항을 통보하는 것이 설계의 핵심입니다. 시스템 전체로 보면, 이 계약이 명확할수록 에러가 발생한 지점이 시스템의 경계면인지, 내부 로직인지 즉각 파악할 수 있습니다.
3. 코드 예제: 파이썬을 활용한 명시적 계약
파이썬의 assert 구문과 타입 힌트를 활용해 계약을 구현하는 방식입니다.
def process_payment(amount: float, balance: float) -> float:
# 사전 조건: 결제 금액은 0보다 커야 하며, 잔액은 결제 금액보다 커야 함
assert amount > 0, "결제 금액은 양수여야 합니다."
assert balance >= amount, "잔액이 부족합니다."
new_balance = balance - amount
# 사후 조건: 남은 잔액은 0 이상이어야 함
assert new_balance >= 0, "사후 조건 위반: 잔액이 음수가 됨"
return new_balance
JavaScript 환경에서 객체 불변성을 지키며 상태를 관리할 때도 비슷한 원리가 적용됩니다.
/**
* 계약에 의한 설계: 데이터의 유효성 검증
* @param {Object} user
*/
function updateUserName(user, newName) {
// 사전 조건: 사용자 객체가 존재하고 새 이름이 문자열이어야 함
if (!user || typeof user.name !== 'string') {
throw new Error("Invalid Pre-condition: User object is required.");
}
const updatedUser = { ...user, name: newName };
// 사후 조건: 이름이 실제로 변경되었는지 검증
if (updatedUser.name !== newName) {
throw new Error("Post-condition Violation: Name not updated.");
}
return updatedUser;
}
4. 실무 적용 사례
대규모 분산 시스템에서는 API 게이트웨이와 데이터 검증 계층에서 이 패턴을 강하게 적용합니다.
- 입력 데이터의 직렬화(Serialization) 시점: DTO(Data Transfer Object)가 유효한지 검사하는
Pydantic이나Zod같은 라이브러리는 계약에 의한 설계를 코드로 구현한 대표적인 도구입니다. - 비즈니스 로직 경계: 도메인 모델 내부에서 상태가 변경될 때마다 불변 조건(Invariant)을 체크하여 잘못된 상태로 데이터베이스에 저장되는 것을 방지합니다.
5. 자주 하는 실수와 해결법
가장 흔한 실수는 **'모든 곳에 계약을 거는 것'**입니다.
- 실수: 함수 내부 로직의 모든 줄에
assert를 넣어 코드의 가독성을 해치고, 비즈니스 로직과 검증 로직을 뒤섞는 행위. - 해결법: 검증 로직을 데코레이터나 별도의 인터셉터 패턴으로 분리하세요. 또한, 프로덕션 환경에서
assert문이 최적화로 인해 제거될 수 있는 언어(Python 등)를 사용한다면, 런타임 검증을 위해 별도의 유효성 검사 라이브러리를 사용해야 합니다.
또한, 사전 조건을 클라이언트 탓으로 돌리기만 하는 것도 위험합니다. 클라이언트의 실수가 발생했을 때 왜 문제가 생겼는지 디버깅이 가능하도록 명확한 오류 메세지를 전달하는 것이 필수적입니다.
6. 더 공부할 리소스 추천
- Bertrand Meyer의 "Object-Oriented Software Construction": 이 개념의 정수를 담고 있는 고전 명저입니다.
- Pydantic (Python) 공식 문서: 현대적인 파이썬에서 어떻게 데이터 계약을 강제하는지 학습하기 가장 좋은 자료입니다.
- Zod (TypeScript) 공식 문서: 인터페이스 정의를 통한 타입 세이프한 계약 설계를 배우기에 최적입니다.