| 용어 | 설명 |
|---|---|
메시지(Message) | 시스템이나 객체가 처리할 수 있는 기능 |
협력(Cooperation) | 메시지에 처리에 책임을 가진 역할들의 시퀀스 |
역할(Role) | 메시지 처리에 참여하는 메타 타입 |
책임(Responsibility) | 메시지 처리 과정에서 각 역할의 기능 |
계약(Contract) | 메시지 처리 과정에서 각 역할 사이의 명세 |
Design By Contract(DBC)는 객체 지향 설계 기법 중 하나로, 책임을 세분화하여, 테스트 가능한 명세로 유지하는 것을 의미한다. DBC 하에 책임은 3가지로 나뉜다.
| 유형 | 설명 | 책임 대상 |
|---|---|---|
사전 조건(precondition) | 기능 호출 전 명세 | 호출자(Client) |
사후 조건(postcondition) | 기능 호출 후 명세 | 실행자(Server) |
불변식((role) invariant) | 역할의 생명주기 내의 명세 | 실행자(Server) |
각 조건의 충족 여부를 가능한 빨리 판단하여, 시스템이 불안정한 상태를 빠르게 극복/종료하도록 하는 것을 목표로 한다(Fail-fast)

DBC는 리스코프 치환 원칙(Liskov substitution principle)에 따라 타입에 대해 확장될 수 있다.
| 규칙 | 조건 | 위계(Base -> Drived)에 따른 강도 | 설명 |
|---|---|---|---|
| 계약(Contract) | 사전 조건 | 같거나 약함 | Client에게 계약 이상을 요구할 수 없다. |
| 사후 조건 | 같거나 강함 | Client에게 계약 이하를 제공할 수 없다. | |
| 불변식 | 같거나 강함 | 상위 위계의 불변식은 유지되어야 한다. | |
| 가변성(Variance) | 반환 타입 | 같거나 강함 | 사후조건과 동일 |
| 매개변수 타입 | 같거나 약함 | 사전조건과 동일 | |
| 예외 타입 | 같거나 강함 | 사후조건과 동일. 기대하지 않은 예외는 발생하면 안된다. |
가변성 규칙은 Operation에 대한 것이며, 다음의 용어를 사용하여 정리할 수 있다.
| 용어 | 설명 |
|---|---|
공변성(covariance) | Operation의 위계가 동일하게 적용됨 |
반공변성(contravariance) | Operation의 위계가 반대로 적용됨 |

확장된 DBC를 준수하면 더욱 견고한 타입 계층을 제공할 수 있다는 장점이 있다.
DBC(명세 및 검증)은 Java Native로 지원되지 않으나, 다음과 같이 간단히 구현해볼 수 있다.
사용한 라이브러리는 다음과 같다.
import static jakarta.validation.Validation.buildDefaultValidatorFactory;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ValidationException;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import error.InvariantContractException;
import error.PostConditionContractException;
import error.PreConditionContractException;
import lombok.extern.slf4j.Slf4j;
/**
* Validator for Design by Contract
*
* @param <T> target message or object
*/
@Slf4j
public final class ContractValidator<T> {
private final String contractPath;
private final ContractType contractType;
/**
* Initialize the validator
*
* @param contractPath class or operation's path
* @param contractType contract type
* @throws InvariantContractException invalid arguments
*/
private ContractValidator(@NotBlank String contractPath, @NotNull ContractType contractType) {
final var properties = new HashMap<String, Object>();
if ((contractPath == null) || contractPath.isBlank()) {
properties.put("contractPath", "null");
}
if (contractType == null) {
properties.put("contractType", "null");
}
if (!properties.isEmpty()) {
throw new InvariantContractException(ContractValidator.class.getName(), properties);
}
this.contractPath = contractPath;
this.contractType = contractType;
}
/**
* Create operation's precondition contract validator. <br/> To apply this, operations should be
* currying.
*
* @param operationPath operation's path like <code>com.happy.Power.love</code>
* @param <T> message's type
* @return Initialized validator
* @throws InvariantContractException invalid arguments
*/
public static <T> ContractValidator<T> preCondition(@NotBlank String operationPath) {
return new ContractValidator<>(operationPath, ContractType.PRECONDITION);
}
/**
* Create operation's postcondition contract validator.
*
* @param operationPath operation's path like <code>com.happy.Power.love</code>
* @param <T> message's type
* @return Initialized validator
* @throws InvariantContractException invalid arguments
*/
public static <T> ContractValidator<T> postCondition(@NotBlank String operationPath) {
return new ContractValidator<>(operationPath, ContractType.POSTCONDITION);
}
/**
* Create operation's invariant contract validator.
*
* @param classPath class's path like <code>com.happy.Power</code>
* @param <T> message's type
* @return Initialized validator
* @throws InvariantContractException invalid arguments
*/
public static <T> ContractValidator<T> invariantCondition(@NotBlank String classPath) {
return new ContractValidator<>(classPath, ContractType.INVARIANT);
}
/**
* Test this contract to the target
*
* @param target message or class
* @throws PreConditionContractException whether <code>check</code> fails itself or
* <code>ContractType.PRECONDITION</code> fails.
* @throws PostConditionContractException <code>ContractType.POSTCONDITION</code> fails.
* @throws InvariantContractException <code>ContractType.INVARIANT</code> fails.
*/
public void check(@NotNull T target) {
if (target == null) {
throw new PreConditionContractException(ContractValidator.class.getName(),
Map.of("target", "null"));
}
try (final var validatorFactory = buildDefaultValidatorFactory()) {
final var validator = validatorFactory.getValidator();
final var errors = validator.validate(target);
if (errors.isEmpty()) {
return;
}
final Map<String, Object> properties = errors.stream().collect(
Collectors.toMap(violation -> violation.getPropertyPath().toString(),
ConstraintViolation::getMessage));
switch (contractType) {
case PRECONDITION -> throw new PreConditionContractException(this.contractPath, properties);
case POSTCONDITION ->
throw new PostConditionContractException(this.contractPath, properties);
case INVARIANT -> throw new InvariantContractException(this.contractPath, properties);
}
} catch (ValidationException e) {
throw new PreConditionContractException(ContractValidator.class.getName(), Map.of(), e);
}
}
private enum ContractType {
INVARIANT, PRECONDITION, POSTCONDITION,
}
}
| Operation | Description |
|---|---|
preCondition | 사전조건 검증기 생성 |
postCondition | 사후조건 검증기 생성 |
invariantCondition | 불변식 검증기 생성 |
check | 검증 수행 |
사용 예는 다음과 같다.
class User {
private @NotBlink String name;
private @Email String email;
private User(String name, String email) {
this.name = name;
this.email = email;
}
public static User create(String name, String email) {
final var user = new User(name, email);
ContractValidator.<User>invariantCondition(User.class.getName()).check(user);
return user;
}
public void update(UserUpdate spec) {
final var operationPath = User.class.getName() + ".update";
ContractValidator.<UserUpdate>preCondition(operationPath).check(spec);
this.name = spec.name();
this.email = spec.email();
ContractValidator.<User>invariantCondition(User.class.getName()).check(this);
}
public User getItself() {
final var operationPath = User.class.getName() + ".getItself";
ContractValidator.<User>postCondition(operationPath).check(this);
return this;
}
}
public record UserUpdate(@NotBlink String name, @Email email) {}
| Operation | Conditions |
|---|---|
create | 불변식 |
update | 사전조건, 불변식 |
getItself | 사후조건 |