본문 바로가기
💻 개발 이야기/객체지향 OOP

계약에 의한 설계(Design by Contract)를 실전에 적용하기

by Jinseong Hwang 2024. 10. 13.

퇴근길, 지하철을 타고 집으로 향하던 중 문득 이런 생각이 스쳤다.

  • 지하철은 나를 안전하게, 정해진 시간 안에, 내가 원하는 역까지 데려다줄 것이다.
  • 지하철 기관사는 성실하게 그 임무를 수행할 것이다.
  • 개찰구에 카드를 태그 하면 결제가 완료되고 문이 열릴 것이다.
  • 운행 시간이 지나면 지하철을 이용할 수 없을 것이다.

이 모든 것은 사회 속에서 정해진 규칙 덕분에 자연스럽게 이루어지는 일들이다. 우리는 그 규칙들을 믿고 다양한 서비스를 사용하며, 그 규칙이 지켜질 것이라는 전제 아래 다른 사람들과 협력하고 살아간다.

소프트웨어 세계에서도 이런 규칙을 적용할 수 있다. 바로 “계약에 의한 설계(Design by Contract)“라는 프로그래밍 패러다임이 그렇다. 이번 글에서는 이 패러다임이 소프트웨어에서 어떻게 작동하고, 그 의미가 무엇인지 함께 알아보자.
 

01 | 부수효과, 참조투명성, 불변성


우선 계약에 의한 설계를 이해하기 위해서는 부수효과, 참조투명성, 불변성에 대한 이해가 필요하다. 아래에서 3가지 특성에 대해 짚어보자.
 

[ 부수효과(Side Effect) ]

부수효과는 실제로 '사이드 이펙트'라는 표현으로 많이 쓰기 때문에 익숙할 것이다. 부수효과는 함수의 실행으로 인해 함수 외부가 영향을 받는 것을 의미한다. 부수효과가 존재하는 함수의 예시를 들면 다음과 같다.

var foo = "Hello"

fun sideEffect() {
    foo = "Bye"
}

class FooTest : FunSpec({
    test("testSideEffect") {
        foo shouldBe "Hello"
        sideEffect()
        foo shouldBe "Bye"
    }
})

sideEffect() 함수의 호출로 함수 외부의 변수인 foo의 값이 "Hello"에서 "Bye"로 변경됐다. 부수효과가 발생한 것이다.
 
반면에 부수효과가 있는 함수와 달리 순수 함수(Pure Function)도 존재한다. 순수 함수는 온전히 함수의 결과값이 입력값에 의존한다. 순수 함수의 예시는 다음과 같다.

var foo = "Hello"

class FooTest : FunSpec({
    test("testPureFunction") {
        foo = "Hello"
        foo.uppercase() shouldBe "HELLO"
        foo shouldBe "Hello"
    }
})

uppercase() 함수를 호출했음에도 foo에는 영향을 미치지 않음을 알 수 있다. 다시 정리하면, 함수의 return 값 이외의 모든 영향이 부수 효과인 것이다.
 

[ 참조 투명성(Referential Transparency) ]

참조 투명성은 "어떤 표현식 e가 있을 때 e의 값으로 e가 나타내는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성"을 의미한다. 참조 투명성에 대해 설명할 때는 수학의 함수가 가장 적절하다.
 
예를 들어, 어떤 함수 f(x)가 있고, f(1) = 3 이라고 가정하자. 우리는 다음 식을 쉽게 계산할 수 있다.
f(1) + f(1) = 6
f(1) * 2 = 6
f(1) - 1 = 2
 
f(1) 을 3으로 치환하더라도 결과는 변하지 않는다. 
3 + 3 = 6
3 * 2 = 6
3 - 1 = 2
 
수학에서 함수는 동일한 입력에 대해서는 동일한 값을 반환한다. f(1)이 존재하는 어디든 3으로 치환하더라도 모든 수식은 만족한다. 따라서 수학은 참조 투명성을 만족하는 이상적인 예시이다.
 

[ 불변성(Immutability) ]

f(1)을 항상 3이라고 표현할 수 있는 이유는 처음에 f(1) = 3 이라고 가정했기 때문이다. 이처럼 어떤 값이 변하지 않는 성질을 불변성이라고 부른다. 어떤 값이 불변한다는 말은 부수효과가 발생하지 않는다는 말과 동일하다. 
 

[ 정리 ]

수학에서의 함수는 어떤 값도 변경하지 않기 때문에 부수효과가 존재하지 않는다. 그리고 부수효과가 없는 불변의 세상에서는 모든 로직이 참조 투명성을 만족시킨다. 따라서 불변성은 부수효과의 발생을 방지하고 참조 투명성을 만족시킨다.
 
함수형 프로그래밍(Functional Programming)은 부수효과가 존재하지 않는 수학적 함수에 기반한다. 따라서 함수형 프로그래밍에서는 부수효과를 최소화하고 참조 투명성의 장점을 극대화할 수 있다.
 

02 | 계약에 의한 설계란?


 

https://uk.m.wikipedia.org/wiki/%D0%A4%D0%B0%D0%B9%D0%BB:Eiffel_logo.svg

 
Eiffel 언어의 창시자인 버트란트 마이어는 실행 시점에 필요한 구체적인 제약이나 조건을 명확하게 표현하기 위한 방법으로 "계약에 의한 설계"라는 개념을 제안했다. 계약에 의한 설계는 협력을 위해 클라이언트(전송객체)와 서버(수신객체)가 준수해야 하는 제약을 코드 상에 명시적으로 표현할 수 있는 방법이다.

아래에서 클라이언트와 전송객체, 서버와 수신객체를 혼용하나 동일한 의미로 사용한다.

 
내가 은행에 가서 다른 계좌로 송금을 한다고 가정해보자. 나는 거래 가능한 상태여야 하고, 상대 계좌도 거래 가능한 상태여야 한다. 그리고 나에게 충분한 잔액이 있어야 하고, 상대의 계좌번호도 정확하게 알고 있어야 한다. 이러한 조건들이 모두 만족한다면 나는 상대방에게 송금을 할 수 있다.
 
소프트웨어 세계로 가져와보자. 객체지향 프로그램에서 객체들은 협력하기 위해 메시지로 소통한다. 내가 상대방에게 송금 요청을 보내는 메시지를 전송했다. 송금 요청 메시지는 계약을 지켜야만 성공할 수 있다. 메시지의 내용은 계약을 지켜야 하고, 계약을 지키지 않은 메시지에 대해서는 수신객체가 송금 처리를 하지 않아도 된다. 계약을 지켰다면 수신객체는 송금 처리를 책임진다. 이것이 계약에 대한 핵심이다.
 
계약은 다음과 같은 특성을 가진다고 정리된다.

  • 협력에 참여하는 각 객체는 계약으로부터 이익을 기대하고 이익을 얻기 위해 의무를 이행한다.
  • 협력에 참여하는 각 객체의 이익과 의무는 객체의 인터페이스 상에 문서화된다.

 

03 | 계약을 구성하는 3가지 조건


https://commons.wikimedia.org/wiki/File:Design_by_contract.svg

 
계약은 전제 조건, 사후 조건, 불변 조건으로 표현될 수 있다. 각 조건에 대해 은행 출금 예시와 함께 알아보자.
 

[ 전제 조건 (Preconditions) ]

클라이언트는 서버로 어떤 값이든 보낼 수 있다. 서버는 모든 입력값에 대해 처리할 수 있어야 한다. 즉, 메시지가 계약을 지키는지에 대한 검증은 수신객체에서 진행한다. 전제 조건은 전달받은 메시지가 지켜야 하는 규격을 의미한다.
 
예를 들어, 계좌에서 돈을 출금한다고 가정하자. 출금 금액은 반드시 0보다 커야하고, 계좌의 잔액보다 작거나 같은 금액만 출금할 수 있다. 두 조건이 모두 만족할 때 수신객체는 출금 명령을 수행한다. 코드 예시는 다음과 같다.

fun withdraw(amount: Double, balance: Double): Double {
    require(amount > 0) { "Withdrawal amount must be greater than zero" }
    require(balance >= amount) { "Insufficient balance" }

    return balance - amount
}

 

[ 사후 조건 (Postconditions) ]

클라이언트는 서버로 메시지를 전송할 때, 기대하는 응답의 형태를 가지고 있다. 사후 조건은 서버에서는 클라이언트로 응답 메시지를 제공하기 전, 기대하는 형태를 의미한다.
 
출금을 했다면 출금하기 전의 잔액보다는 출금한 후의 잔액이 더 작은 것이 마땅하다. 따라서 행위가 종료되기 전에 추가로 검증을 수행한다. 코드 예시는 다음과 같다.

fun withdraw(amount: Double, balance: Double) {
    require(amount > 0) { "Withdrawal amount must be greater than zero" }
    require(balance >= amount) { "Insufficient balance" }
    val oldBalance = balance
    balance -= amount

    check(balance < oldBalance) { "Balance should decrease after withdrawal" }
}

 

[ 불변 조건 (Invariants) ]

메시지를 처리하는 객체(클래스)의 상태가 항상 계약을 지켜야 한다. 클래스가 요청을 처리할 준비가 되었는지 검증하는 조건을 불변 조건이라고 한다. 불변 조건의 검증 대상은 메시지와 무관한 객체의 상태(매개변수, 지역변수, 리턴값 등)이다.
 
예를 들어, 계좌의 잔액은 항상 0 이상인 것이 보장되어야 한다. 계좌 생성 및 입출금 시점에 항상 계좌 잔액이 0 이상인지 검증을 해서 객체의 상태를 검증할 수 있다. 코드 예시는 다음과 같다.

class BankAccount(private var balance: Double) {

    init {
        require(balance >= 0) { "Initial balance must be non-negative" } // 불변 조건
    }

    fun deposit(amount: Double) {
        require(amount > 0) { "Deposit amount must be greater than zero" }
        balance += amount
        check(balance >= 0) { "Balance must never be negative" } // 불변 조건
    }

    fun withdraw(amount: Double) {
        require(amount > 0) { "Withdrawal amount must be greater than zero" }
        require(balance >= amount) { "Insufficient balance" }
        balance -= amount
        check(balance >= 0) { "Balance must never be negative" } // 불변 조건
    }
}

 


 
여기까지 왔다면 계약에 의한 설계의 핵심을 모두 이해한 것이다!
조금 더 깊이있게 살펴보자.

 
 

04 | 계약의 위임


User는 정상적인 계좌를 최대 3개만 가질 수 있다고 가정하자. 계좌(BankAccount)의 상태 정보는 BankAccount.isNormal (bool) 로 구분한다. 코드로 작성하면 다음과 같다.
 

class BankAccount(val isNormal: Boolean) {
}

class User(private var accounts: List<BankAccount>) {
    fun addAccount() {
        if (accounts.count { it.isNormal } >= 3) {
            throw IllegalStateException("Cannot add more than 3 normal accounts")
        }
        accounts += BankAccount(true)
    }
}

User는 계좌 정보(BankAccount)가 정상적인 상태인지 검증을 해야 한다면, isNormal 필드를 반드시 참조해야 한다. 하지만 아래와 같이 BankAccount가 직접 검증하도록 위임해서 개선할 수 있다.
 

class BankAccount(private var isNormal: Boolean) {
    fun check() {
        if (!isNormal) {
            throw IllegalStateException("Account is not normal")
        }
    }
}

class User(private var accounts: List<BankAccount>) {
    fun addAccount() {
        val count = accounts.count {
            try {
                it.check()
                true
            } catch (ignored: IllegalStateException) {
                false
            }
        }
        if (count >= 3) {
            throw IllegalStateException("Cannot add more than 3 normal accounts")
        }
        accounts += BankAccount(true)
    }
}

BankAccount의 isNormal 필드는 private으로 숨겼고, User는 더 이상 isNormal 필드에 접근할 수 없고 접근하지 않아도 된다. 검증에 관한 책임은 완전히 BankAccount로 위임했다. 적절히 위임함으로써 객체 간 결합도를 낮출 수 있었다.
 

05 | 계약을 여러 번 하지 않기


우리는 코드의 중복을 죄악처럼 여기곤 한다. 여기서 궁금한 점이 생긴다.

  • 계약의 중복 또한 마찬가지 아닐까?
  • 처음으로 메시지를 처리하는 곳에서만 검증하고 그 이후로는 안 하면 되지 않을까?
  • 계약을 어떻게 전파하면 좋을까?

 
아래에서 상품, 재고, 주문 예시를 통해 살펴보자.

상품을 판매하기 위해서는 해당 상품이 현재 판매 중이어야 하고, 재고가 1개 이상 존재해야 한다. 이때 비로소 성공적으로 주문을 생성할 수 있다. 이는 적어도 Happy Path 에서는 문제가 없어 보인다.
 

하지만 누군가가 상품 검증, 재고 검증을 거치지 않고 곧장 주문 생성을 한다면 어떻게 될까? 우리는 견고한 소프트웨어를 만들어야 하기 때문에 주문 생성 시에도 상품 검증, 재고 검증을 추가하기로 결정했다. 아, 운이 좋게도 검증의 중복이 발생한 것을 발견했다. 어떻게 하면 이 문제를 해결할 수 있을까?
 
실세계의 예시로, 나는 회사를 다니는 직장 근로자이다. 나는 입사할 때 회사와 근로 계약을 맺었다. 이 계약은 나와 회사 사이에서만 유효한 것이다. 나는 다른 회사가 아닌 우리 회사와 계약을 맺은 것이고, 회사는 다른 사람이 아닌 나와 계약을 맺은 것이다. 그리고 이 계약은 외부로 유출되어서는 안 되고, 유출할 수도 없어야 한다. 이를 어떻게 소프트웨어 세계에도 녹여낼 수 있을까?

이에 대한 해답은 패키지 가시성(Visibility)에 있다. 상품과 주문 생성 사이에는 계약이 존재하고, 해당 계약은 둘 사이에서만 유효하다. 갑자기 다른 객체가 찾아와서 계약을 무시할 수도 없다.
 
물론 그림에서는 커다랗게 internal 가시성으로 묶었지만, 실제로는 계약 라이프사이클에 따라 정확하게 동작할 수 있게 아주 세밀하게 가시성을 다뤄야 한다. 계약을 검증하는 것은 쉽지만 누가 검증할 것인지를 결정하는 것은 매우 섬세하게 이뤄져야 한다. 이는 절대 쉬운 일이 아니기 때문에 많은 연습이 필요해 보인다.

Java Interface를 사용한다면 가시성을 반드시 public으로 열어야 하는 제약이 있다. 이는 가시성 누수 문제가 생길 수 있음을 암시한다. 안타깝지만 이 문제를 해결하기 위해서는 Interface를 포기하고 Abstaract class 등을 선택하는 방법을 사용해야 한다.

 
 

06 | 내 코드에 적용하는 다양한 방법


[ 명시적인 예외 처리 ]

Java, Kotlin에서 계약에 의한 설계의 전제 조건(Preconditions), 사후 조건(Postconditions), 불변 조건(Invariants)을 구현하기 위해 가장 흔히 사용되는 방법은 명시적인 예외 처리를 사용하는 것이다. IllegalArgumentException, IllegalStateException 등 Java 표준 예외를  사용하여 계약을 강제할 수 있다.
 
예시로는 위 03 | 계약을 구성하는 3가지 조건에서 소개한 방식이다. 단, 위 코드는 예시일 뿐이고 다른 방식들도 가볍게 소개한다.
 
첫번째로, if 조건문을 사용하는 방식이다. 가장 간단하지만 코드를 조금 더 간결하게 작성하고 싶다면 아래의 방법도 고려해 보자.

public void deposit(double amount) {
    if (amount <= 0) {
        throw new IllegalArgumentException("Deposit amount must be positive");
    }
    this.balance += amount;
}

 
두번째로, Kotlin의 require, check을 사용하는 방식이다.

fun deposit(amount: Double) {
    require(amount > 0) { "Deposit amount must be positive" }
    balance += amount
    check(balance >= 0) { "Balance must be non-negative after deposit" }
}

내부적으로 IllegalArgumentException, IllegalStateException를 만들어서 던져주기 때문에 조금 더 나은 가독성을 챙길 수 있다.
 
세번째로, Spring의 Assert를 사용하는 방식이다.

import org.springframework.util.Assert;

public void deposit(double amount) {
    Assert.isTrue(amount > 0, "Deposit amount must be positive");
    this.balance += amount;
    Assert.isTrue(balance >= 0, "Balance must be non-negative after deposit");
}

예시 코드의 isTrue 뿐만 아니라, Spring의 다른 유틸 함수들과 결합해서 isNull, hasLength, hasText, doesNotContain, notEmpty, notNullElements, isInstanceOf, isAssignable 등 굉장히 많은 검증 방식을 제공한다. 개인적으로 나는 이 Spring의 Assert를 좋아한다.
 

[ assert 키워드 활용 ]

Java의 assert를 사용해서 검증할 수 있다. assert는 조건이 false일 경우 AssertionError를 발생시킨다. 런타임에 특정 조건을 확인하는 디버깅 용도로 자주 활용된다. assert는 기본적으로 비활성화 상태이므로, 활성화를 하고 싶다면 java 명령어에 -ea 옵션을 추가해야 한다. (IntelliJ에서는 Run Configurations > VM Options에 추가하면 된다.)
사용 예시는 다음과 같다.

public int divide(int numerator, int denominator) {
    assert denominator != 0 : "Denominator must not be zero"; // 전제 조건
    int result = numerator / denominator;
    assert result >= 0 : "Result must be non-negative"; // 사후 조건
    return result;
}

단, 검증 실패 시 발생하는 AssertionError는 Exception이 아닌 Error이다. Error는 Error 답게 처리되어야 한다. 필히 개발 환경에서만 사용해야 하고 운영 환경에서는 절대 -ea 옵션을 사용하지 않는 것을 권장한다.
 

Java의 java.lang.Error 클래스와 java.lang.Exception 클래스 모두 java.lang.Throwable 클래스를 상속받고 있다. try-catch는 Throwable을 잡기 때문에 Error/Exception 모두 잡아서 처리할 수 있다.

하지만 Error는 잡지 않는 것이 좋다. 주로 JVM이나 시스템 레벨의 문제인 경우가 많은데 Error가 발생할 경우에는 따로 처리하지 않고 프로그램이 종료되는 것이 더 나은 선택일 수 있다.

 

[ 어노테이션을 통한 검증 ]

jakarta.validation에는 완성도 높은 훌륭한 어노테이션을 제공하고 있다. Spring Boot를 사용하고 있다면 spring-boot-starter-validation 의존성을 추가하여 사용 가능하다.

import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Min

fun withdraw(@NotNull amount: Double, @Min(0) balance: Double): Double {
    return balance - amount
}

만약 검증에 실패하면 jakarta.validation.ValidationException을 던진다. API 개발자라면 ValidationException이 발생한 시점에 400 Bad Request 등의 응답을 내려줄 수도 있다.
 

[ 검증이 완료된 타입으로 갈음 ]

05 | 계약을 여러 번 하지 않기에서 다룬 문제를 해결하는 또 다른 패턴이다. 검증되지 않은 객체를 검증이 완료된 타입으로 갈음하는 방법은 언어에서 제공하는 Type Safety 시스템을 활용하여 객체가 검증되었음을 보장하는 패턴이다. 검증되지 않은 상태에서는 해당 객체를 사용할 수 없게 하고, 검증이 완료되었을 때만 객체를 사용할 수 있도록 타입을 구분하는 방식으로 중복을 줄일 수 있다.
 
전통적인 디자인 패턴 중 유사한 패턴으로는 어댑터 패턴이 있다.

https://dev.to/carlillo/design-patterns---adapter-2pi3

 
일반 유저 엔티티가 있고, 검증된 유저 엔티티가 있다. 검증된 유저로 변환되어야지만 다른 객체로 메시지를 보낼 수 있게 하고 싶다면 아래와 같이 구성할 수 있다.

아래는 코드 예시이다.

data class User(val name: String?, val email: String?)

data class ValidatedUser(val name: String, val email: String)

fun validateUser(user: User): ValidatedUser {
    require(!user.name.isNullOrBlank()) { "User name must not be blank" }
    require(!user.email.isNullOrBlank() && user.email.contains("@")) { "Invalid email format" }

    return ValidatedUser(user.name!!, user.email!!)
}

fun sendWelcomeEmail(user: ValidatedUser) {
    println("Sending welcome email to ${user.name} at ${user.email}")
}

 

[ 이외에도... ]

Kotlin의 Contracts

Kotlin 컴파일러가 불필요한 타입 캐스팅이나 타입 체크를 하지 않고, 계약을 통해 더 많은 정보를 줄 수 있도록 설계된 Kotlin 공식 API이다. v1.3부터 나와서 현재 v2.0까지 릴리즈 됐지만 여전히 실험적인 기능으로 제공하고 있다. 하지만 Kotlin 내부 구현에서는 사용하고 있다.

https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/Timeout.kt

KEEP 문서는 여기에서 확인할 수 있다.

  • 참고로 KEEP은 Kotlin Evolution and Enhancement Process의 약자로 Java 진영의 JEP와 동일하다.

 

Jetbrains의 @Contract

Jetbrains에서 만든 IDE과 결합해 사용할 수 있는 기능이다.

import org.jetbrains.annotations.Contract

@Contract(" -> fail")
void doNothingWithWrongContract() {}

항상 실패한다는 계약을 가지고 있지만 이 메서드는 항상 성공한다. 따라서 IntelliJ의 Code Inspection을 실행하면 계약 위반 경고로 알려준다. 이외에도 부수 효과가 없는 순수 함수임을 나타내는 pure 옵션도 계약에 포함할 수 있다.
 

Google Guava의 Preconditions

Guava 라이브러리에 포함된 기능이다. 이름처럼 전제 조건(Preconditions)을 검증할 수 있다. 사용법은 여기에서 자세히 확인할 수 있다.
 

Google의 Cofoja

Google 직원이 비공식적으로 만든 계약에 의한 설계를 도와주는 라이브러리이다. Cofoja는 Contracts for Java의 약자이다.
 
다만 마지막 릴리즈가 2016년 2월이고, 그 이후로는 Unmaintained 상태로 접어든 것으로 추측된다. 2016년 이후로 Java는 많은 변화가 있었기 때문에 요즘 Java에서는 사용하기 힘들다. 코드는 여기에서 확인할 수 있다.
 
 

07 | 계약에 의한 설계에 단점은 없을까?


https://bryanmmathers.com/no-silver-bullet/

계약에 의한 설계는 소프트웨어의 신뢰성과 안정성을 높이는 방법론 중 하나이다. 전제 조건, 사후 조건, 불변 조건을 통해 명확한 계약을 설정함으로써 코드의 가독성을 높이고, 의도하지 않은 입력이나 잘못된 상태로부터 프로그램을 보호할 수 있다. 이러한 계약은 특히 대규모 시스템이나 복잡한 비즈니스 로직에서 유용하며, 각 컴포넌트가 자신의 역할과 책임을 명확히 할 수 있도록 도와준다. 또한, 개발자 간의 의사소통을 명료하게 하고, 코드에서 발생할 수 있는 잠재적 오류를 사전에 방지할 수 있는 이점이 있다.

그러나 계약에 의한 설계에는 몇 가지 단점 및 주의할 점이 있다. 첫째, 모든 메소드나 클래스에 전제 조건, 사후 조건, 불변 조건을 정의하려면 추가적인 코드와 문서가 필요하다. 이로 인해 코드가 길어지고 복잡해질 수 있으며, 유지보수 시 계약 조건이 변경되면 이를 수정하는 비용이 발생할 수 있다. 또한, 모든 조건을 꼼꼼하게 정의하기 위해서는 상당한 시간과 노력이 요구되며, 실무에서는 이를 완전히 적용하기 어려울 때도 있다. 둘째, 계약 검증으로 인한 런타임 비용이 증가할 수 있다. 계약 검증이 메소드 호출마다 실행되면, 특히 성능이 중요한 시스템에서는 성능 저하를 야기할 수 있다. 따라서 계약 검증을 어디에서, 언제 수행할지에 대해 신중한 판단이 필요하다.
 

08 | 결론


계약은 반드시 코드로 명시적으로 작성되어야 하며, 그 대상자는 단지 나만이 아니라 코드를 읽는 모든 사람, 심지어 기억이 희미해진 미래의 나까지 포함된다. 코드 내에서 계약을 명확하게 정의하면, 개발자가 코드를 유지보수할 때나 새로운 기능을 추가할 때 필수적인 가이드라인 역할을 한다. 이렇게 명시된 계약은 코드의 의도를 명확히 하고, 실수나 오해로 인한 문제를 사전에 방지할 수 있다.

모든 계약이 반드시 엄격하게 지켜져야 하는 것은 아니다. 만약 상황에 따라 계약을 무시해도 괜찮다면 굳이 문제로 삼을 필요가 없다. 하지만 계약을 어겼을 때 반드시 외부에 알릴 필요가 있는 경우라면, return 값을 통해 상태를 전달하거나, 적절한 예외를 발생시켜 명확하게 알려줘야 한다. 결국 이 모든 것은 내가 만드는 제품의 컨텍스트에 맞게 설계되어야 하며, 그 상황에 맞는 행동을 선택하는 것이 중요하다. 계약에 의한 설계를 실무에 적용하려면 상당한 경험과 판단력이 필요하며, 이를 제대로 사용하기 위해서는 끊임없이 더 나은 코드를 작성하려는 노력이 필요하다.
 
 

References