계약에 의한 설계(Design By Contract) with Java

윤영로·2025년 5월 17일

개발관리

목록 보기
2/3

목표

  • 계약에 의한 설계
  • 일반화된 계약에 의한 설계
  • Java 구현 예시

용어 정의

용어설명
메시지(Message)시스템이나 객체가 처리할 수 있는 기능
협력(Cooperation)메시지에 처리에 책임을 가진 역할들의 시퀀스
역할(Role)메시지 처리에 참여하는 메타 타입
책임(Responsibility)메시지 처리 과정에서 각 역할의 기능
계약(Contract)메시지 처리 과정에서 각 역할 사이의 명세

계약에 의한 설계

Design By Contract(DBC)는 객체 지향 설계 기법 중 하나로, 책임을 세분화하여, 테스트 가능한 명세로 유지하는 것을 의미한다. DBC 하에 책임은 3가지로 나뉜다.

유형설명책임 대상
사전 조건(precondition)기능 호출 전 명세호출자(Client)
사후 조건(postcondition)기능 호출 후 명세실행자(Server)
불변식((role) invariant)역할의 생명주기 내의 명세실행자(Server)

각 조건의 충족 여부를 가능한 빨리 판단하여, 시스템이 불안정한 상태를 빠르게 극복/종료하도록 하는 것을 목표로 한다(Fail-fast)

DBC Flow

일반화된 계약에 의한 설계

DBC는 리스코프 치환 원칙(Liskov substitution principle)에 따라 타입에 대해 확장될 수 있다.

규칙조건위계(Base -> Drived)에 따른 강도설명
계약(Contract)사전 조건같거나 약함Client에게 계약 이상을 요구할 수 없다.
사후 조건같거나 강함Client에게 계약 이하를 제공할 수 없다.
불변식같거나 강함상위 위계의 불변식은 유지되어야 한다.
가변성(Variance)반환 타입같거나 강함사후조건과 동일
매개변수 타입같거나 약함사전조건과 동일
예외 타입같거나 강함사후조건과 동일. 기대하지 않은 예외는 발생하면 안된다.

가변성 규칙은 Operation에 대한 것이며, 다음의 용어를 사용하여 정리할 수 있다.

용어설명
공변성(covariance)Operation의 위계가 동일하게 적용됨
반공변성(contravariance)Operation의 위계가 반대로 적용됨

Variance Rule

확장된 DBC를 준수하면 더욱 견고한 타입 계층을 제공할 수 있다는 장점이 있다.

Java 구현 예시

DBC(명세 및 검증)은 Java Native로 지원되지 않으나, 다음과 같이 간단히 구현해볼 수 있다.

사용한 라이브러리는 다음과 같다.

  • spring-boot-starter-validation
    계약의 작성은 jakarta validation specification으로 수행할 수 있으며, 검증을 하기 위해 구현체(Hibernate or Spring validation)을 사용할 수 있다. 검증 로직은 다음과 같다.
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,
  }
}
OperationDescription
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) {}
OperationConditions
create불변식
update사전조건, 불변식
getItself사후조건

참고 자료

profile
夫唯嗇是以早服

0개의 댓글