본문 바로가기

Book

계약에 의한 설계

오브젝트 책 부록 부분에 좋은 내용이 있어서 정리했다.

인터페이스만으로는 객체의 행동에 관한 다양한 관점을 전달하기 어렵다는 것. 그래서 부수효과를 쉽고 명확하게 표현할 수 있는 커뮤니케이션 수단이 필요한데 그게 계약에 의한 설계임(Design By Contract)

01. 협력과 계약

부수효과를 명시적으로

인터페이스를 통해서 메시지는 정의할 수 있지만, 객체 사이의 의사소통 방식은 정의할 수 없음. 시그니처를 통해 메시지 이름과 파라미터 목록은 전달하지만, 협력을 위해 필요한 약속과 제약은 인터페이스를 통해 전달할 수 없음.

계약

계약은 협력을 명확하게 정의하고 커뮤니케이션할 수 있는 범용적인 아이디어임. 객체 사이의 계약은 다음과 같음

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

02. 계약에 의한 설계

  • 사전조건: 메서드가 호출되기 위해 만족돼야 하는 조건. 메서드의 요구사항을 명시함. 사전조건이 만족되지 않을 경우 메서드가 실행돼서는 안 된다. 사전조건을 만족시키는 것은 메서드를 실행하는 클라이언트의 의무다.
  • 사후조건: 메서드가 실행된 후에 클라이언트에게 보장해야 하는 조건, 클라이언트가 사전조건을 만족시켰다면 메서드는 사후조건에 명시된 조건을 만족시켜야 한다. 만약 클라이언트가 사전조건을 만족시켰는데도 사후조건을 만족시키지 못한 경우에는 클라이언트에게 예외를 던져야 함. 사후조건을 만족시키는 것은 서버의 의무
  • 불변식: 항상 참이라고 보장되는 서버의 조건, 메서드가 실행되는 도중에는 불변식을 만족시키지 못할 수도 있지만 메서드를 실행하기 전이나 종료된 후에 불변식은 항상 참이어야 한다.
    사전조건, 사후조건, 불변식을 기술할 때는 실행 절차를 기술할 필요 없이 상태 변경만을 명시하기 때문에 코드를 이해하고 분석하기 쉬워진다.

사전조건

사전조건은 메서드가 정상적으로 실행되기 위해 만족해야 하는 조건임. 사전조건을 만족시키는 것은 메서드를 실행하는 클라이언트의 의무임. 사전조건을 만족시키지 못해서 메서드가 실행되지 않을 경우 클라이언트에 버그가 있다는 것을 의미함.

사후조건

클라이언트가 사전조건을 만족시켰는데도 서버가 사후조건을 만족시키지 못한다면 서버에 버그가 있음을 의미함.
일반적으로 사후조건은 다음과 같은 3가지 용도로 사용됨

  • 인스턴스 변수의 상태가 올바른지를 서술하기 위해
  • 메서드에 전달된 파라미터의 값이 올바르게 변경됐는지를 서술하기 위해
  • 반환값이 올바른지를 서술하기 위해
    다음과 같은 두 가지 이유로 인해 사전조건보다 사후조건을 정의하는 것이 더 어려울 수 있다.
  • 한 메서드 안에서 return 문이 여러 번 나올 경우
  • 실행 전과 실행 후의 값을 비교해야 하는 경우

불변식

불변식은 인스턴스 생명주기 전반에 걸쳐 지켜져야 하는 규칙을 명세함. 불변식은 일반적으로 객체의 내부 상태와 관련 있음

  • 불변식은 클래스의 모든 인스턴스가 생성된 후에 만족되어야 함. 이것은 클래스에 정의된 모든 생성자는 불변식을 준수해야 한다는 것을 의미함
  • 불변식은 클라이언트에 의해 호출 가능한 모든 메서드에 의해 준수되어야 한다.

불변식은 사전조건과 사후조건에 추가되는 공통의 조건으로 생각할 수 있음.

03. 계약에 의한 설계와 서브 타이핑

계약에 의한 설계는 클라이언트가 만족시켜야 하는 사전조건과 클라이언트 관점에서 서버가 만족시켜야 하는 사후조건을 기술함. 이 부분이 계약에 의한 설계와 리스코프 치환 원칙이 만나는 구간임
서브타입이 리스코프 치환 원칙을 만족시키기 위해서는 클라이언트와 슈퍼타입 간에 체결된 계약을 준수해야 함

리스코프 치환 원칙의 규칙을 2가지 종류로 세분화할 수 있음. 첫 번째 규칙은 협력에 참여하는

객체에 대한 기대를 표현하는 계약 규칙이고, 두 번째 규칙은 교체 가능한 타입과 관련된 가변성 규칙이다.

  • 계약 규칙은 슈퍼타입과 서브타입 사이의 사전조건, 사후조건, 불변식에 대해 서술함
    • 서브타입에 더 강력한 사전조건을 정의할 수 없음.
    • 서브타입에 더 완화된 사후조건을 정의할 수 없음.
    • 슈퍼타입의 불변식은 서브타입에서도 반드시 유지되어야 한다.
  • 가변성 규칙은 파라미터와 리턴 타입의 변형과 관련된 규칙이다
    • 서브타입의 메서드 파라미터는 반공변성을 가져야 한다.
    • 서브타입의 리턴 타입은 공변성을 가져야 한다.
    • 서브타입은 슈퍼타입이 발생시키는 예외와 다른 타입의 예외를 발생시켜서는 안 된다.

계약 규칙

클라이언트 사전 조건 사후조건 관계가 헷갈리면 다음처럼 이해하면 됨.
서브 타입 사전 조건은 더 강하게 걸면 안 됨. --> 클라이언트 입장에서 부담이 더 생김. 그니까 넘겨주는건 클라이언트 책임인데 그 부담이 커지는걸 바라는 클라이언트는 없음
서브 타입 사후 조건은 더 약해지면 안됨 --> 클라이언트 입장에서 서버가 계약을 제대로 충족시키지 못했다는 거임. 그래서 유효하지 않음. 명시된 이익보다 더 적은 이익을 받게 된다는 사실을 납득할 수 있는 클라이언트는 없다.
이런 식으로 이해하면 납득이 좀 감.

일찍 실패하기

위의 내용이랑 연관되어 있는데, 사후 조건 위반은 서버에게 책임이 있음 클라이언트는 넘겨받은 값을 다시 다른 데로 넘기거나 사용할 거임. 근데 이게 다른 곳으로 전파돼서 에러가 나게 되면 원인으로부터 너무 멀리 떨어지게 됨. 그렇기 때문에 에러에 대한 예외를 그 서버에서 바로 발생시켜야 함. 그니까 일찍 그냥 실패하도록 만드는 게 낫다.

슈퍼타입의 불변식은 서브타입에서도 반드시 유지되어야 한다

부모 타입에서 불변식에 포함된 변수가 protect 여서 서브 타입에서 수정가능하면 계약 규칙을 어길 수도 있음(null값으로 바꾼다던가). 이럴 때는 수정하는 기능을 하는 부모의 퍼블릭 인터페이스 내부에서 불변식을 체크하면 됨. 그런 자연스럽게 그 메서드를 통해 서브 타입에서 수정할 때 체크하게 됨.

가변성 규칙

서브타입은 슈퍼타입이 발생시키는 예외와 다른 타입의 예외를 발생시켜서는 안 된다.

일반적으로 부모 클래스가 던지는 예외가 속한 상속 계층이 아닌 다른 상속 계층에 속하는 예외를 던질 경우 자식 클래스는 부모 클래스를 대체할 수 없다. (자식 예외도 부모 클래에서 던지는 예외의 자식이면 괜찮음)
또 다른 경우는 부모에서 정의하지 않은 예외를 자식에서 예외를 발생시키는 경우가 있음. 이것도 안됨. 일단 클라이언트가 원하는 바는 부모 타입의 퍼블릭 인터페이스일 테고 거기서는 원하는 결과가 부모에 없는 거라면 올바른 서브타입이 아님. 또한 예외가 아니라 오버라이딩한 메서드의 내용을 비워놔도 안됨.
이 경우들 모두 공통점이 있는데,

자식 클래스가 부모 클래스가 하는 일 보다 적은 일을 수행한다는 공통점임. 클라이언트 관점에서 부모 클래스에 대해 기대했던 것보다 더 적은 일을 수행하는 자식 클래스는 부모 클래스와 동일하지 않음

서브타입의 리턴 타입은 공변성을 가져야 한다

부모 클래스에서 구현된 메서드를 자식 클래스에서 오버라이딩할 때 부모 클래스에서 선언한 반환타입의 서브타입으로 지정할 수 있는 특성을 리턴 타입 공변성이라고 부른다. 간단하게

말해서 리턴타입 공변성이란 메서드를 구현한 클래스의 타입 계층 방향과 리턴 타입의 타입 계층 방향이 동일한 경우를 가리킴.

슈퍼타입 대신 서브타입을 반환하는 것은 더 강력한 사후조건을 정의하는 것과 같음. 리턴 타입 공변성은 계약에 의한 설계 관점에서 계약을 위반하지 않음.

리턴 타입 공변성은 메서드의 구현 계층과 리턴 타입의 계층이 동일한 방향을 가진다

그러나 이런 부분은 언어에서 지원 안 하는 경우도 있음. 그니까 자바에서는 이걸 지원하는데 C# 같은 경우에는 같은 경우에 서브타입을 리턴하면 컴파일 에러를 출력함 . C#에서는 Book 타입을 반환하는 BookStall 의 sell 메서드는 Magazine 타입을 반환하는 MagazineStore 의 sell 메서드와는 아무런 상관도 없음 이렇게 타입 사이에 어떤 관계도 없는 것을 무공변성이라고 함.

서브타입의 메서드 파라미터는 반공변성을 가져야 한다

일단 자바는 파라미터 반공변성을 지원하지 않음. 여기서는 허용한다고 가정하고 설명함 다음과 같은 코드예시

public class MagazineStore extends BookStall{
    @Override
    public Magazine sell(Publisher publisher){
        return new Magazine(publisher);
    }
}

원래 코드에서는 저 파라미터 부분이 서브타입인 IndependentPublisher Publisher 이렇게 되어 있었음
기존의 Customer가 BookStall.sell()이렇게 호출할때 BookStall가 MagzineStore이면 IndependentPublisher 의 인스턴스가 MagzineStore 의 sell의 파라미터로 들어갈 거임. 그니까 파라미터 타입이 슈퍼타입인 Publisher 타입으로 선언되어 있기 때문에 IndependentPublisher 인스턴스가 전달되더라도 문제 없음.
이처럼

부모 클래스에서 구현된 메서드를 자식 클래스에서 오버라이딩할 때 파라미터 타입을 부모 클래스에서 사용한 파라미터의 슈퍼타입으로 지정할 수 있는 특성을 파라미터 타입 반공변성 이라고 부름

. 간단히 말해서 메서드를 정의한 클래스의 타입 계층과 파라미터의 타입 계층의 방향이 반대인 경우 서브타입 관계를 만족한다는 거임.

파라미터 타입 반공변성은 메서드의 구현 계층과 리턴 타입의 계층이 반대 방향을 가진다

서브타입 대신 슈퍼타입을 파라미터로 받는 것은 더 약한 사전조건을 정의하는것과 같음. 파라미터 타입 반공변성은 계약에 의한 설계 관점에서 계약을 위반하지 않음. 서브타입에서 오버라이딩해서 구현할때 파라미터 타입을 슈퍼타입에서 사용한 파라미터 타입의 슈퍼타입으로 사용해도 클라이언트 입장에서는 대체 가능한 것임.

리턴 공변성, 파라미터 반공변성 정리하면

리턴 타입 공변성과 파라미터 타입 반공변성을 사전조건과 사후조건의 관점에서 설명이 가능함. 서브타입은 슈퍼타입에서 정의한 것보다 더 강력한 사전조건을 정의할 수는 없지만 사전조건을 완화할 수는 있음.
사전 조건은 파라미터에 대한 제약조건이므로 이것은 슈퍼타입에서 정의한 파라미터 타입에 대한 제약을 좀 더 완화할 수 있다는 것을 의미함.
리턴 타입은 사후조건과 관련이 있으며 서브타입은 슈퍼타입에서 정의된 사후조건을 완화시킬 수 없지만 강화할 수는 있음. 이게 슈퍼타입에서 정의한 리턴 타입보다 강화된 서브타입 인스턴스를 반환하는 것이 가능한 것임.

근데 객체지향 언어 중에서 파라미터 반공변성을 지원하는 언어는 거의 없다고 한다. 여기서 언급한 이유는 제네릭 프로그래밍에서는 파라미터 반공변성이 중요한 의미를 가지기 때문에 도움이 된다고 함.

함수 타입과 서브타이핑

이름 없이 메서드를 정의하는 것을 허용하는 언어들은 객체의 타입뿐만 아니라 메서드의 타입을 정의 할 수 있게 허용한다. 그리고 타입에서 정의한 시그니처를 준수하는 메서드들을 이 타입의 인스턴스로 간주한다. 다음의 예를 보자

def sell(publisher:IndependentPublisher) : Book = new Book(publisher)
IndependentPublisher =>Book

sell 메서드와 이 메서드의 익명 함수 형태의 경우 파라미터 타입과 리턴 타입이 동일하기 때문에 같은 타입의 인스턴스임
따라서 다음 처럼 사용할 수 있음

class Customer{
    var book:Book = null
def order(store: IndependentPublisher => Book):Unit={
    book = store(new IndependentPublisher() )
    }
}
new Customer().order((publisher:IndependentPublisher) => new Book(publisher))

이번 장의 내용을 보면, 메서드에 대한 타입을 정의할 수 있다면 함수 타입의 서브타입을 정의할 수 있는걸 알 수 있음.
앞에서 파라미터 타입이 반공변성을 가지고 리턴 타입이 공변성을 가질 경우 메서드가 오버라이드 가능하다고 했던 것을 기억해보면, 메서드가 오버라이드 가능하다는 것은 메서드가 대체 가능하며, 따라서 두 메서드 사이에 서브타이핑 관계가 존재한다는 것을 의미함. 따라서 다음도 가능함

new Customer().order((publisher:Publisher)=> new Magazine(publisher))

파라미터는 반공변적이고 리턴 타입은 공변적임 따라서 (publisher:Publisher)=> new Magazine(publisher) 의 타입인 Publisher => Magazine 은 IndependentPublisher => Book 타입의 서브타입이다.

파라미터 타입 반공변성과 리턴 타입 공변성과 함수 서브타입 사이의 관계

서브 타입이 슈퍼타입을 치환할 수 있다는 것은 계약에 의한 설계에서 정의한 계약 규칙과 가변성 규칙을 준수한다는 것을 의미함.
진정한 서브타이핑 관계를 만들고 싶다면 서브타입에 더 강력한 사전조건이나 더 완화된 사후조건을 정의해서는 안 되며 슈퍼타입의 불변식을 유지하기 위해 항상 노력해야 함. 또한 서브타입에서 슈퍼타입에서 정의하지 않은 예외를 던지면 안됨.

 

참고 및 출처: <오브젝트> (조영호 지음, 위키북스 , 2019)

'Book' 카테고리의 다른 글

동적인 협력, 정적인 코드  (0) 2024.02.15
부분적 경계  (1) 2024.02.02
정책과 수준 그리고 업무 규칙  (0) 2024.01.19
컴포넌트 응집도, 결합  (1) 2023.12.31
프로그래밍 패러다임  (0) 2023.12.22