일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 구글 플레이 비공개 테스트
- 클린코드
- 커밋 메시지
- 구글 플레이 스토어 배포 방법
- 우테코
- 프리코스
- 설계
- 운영체제 #CS지식
- git
- 플레이 스토어 20명
- 객체지향
- 구글 비공개 테스트 20명
- 객체지향설계
- 플레이스토어 비공개 테스트
- 기능명세서
GYUD-TECH
[스터디: 오브젝트] 계약에 의한 설계 본문
메시지의 의도가 잘 드러나게 하기 위해서 메시지 명을 짓고, 인터페이스를 다듬고, 명령과 쿼리를 분리하였다.
하지만 이렇게 하더라도 시그니처만으로 부수효과를 명확하게 드러내는 것에는 한계가 존재한다.
이번 장에서는 부수 효과를 명확하게 표현하는 계약에 의한 설계에 대해서 공부한다.
자바에서 계약 조건을 명시하는 기능을 제공하지는 않지만, 이 개념을 이해하고 조건을 명시할 때 고려해야할 점에 초점을 맞추어서 이번 장을 공부하면, 객체지향에 대한 이해가 더욱 높아질 것이다.
계약에 의한 설계
일상생활에서의 계약
일상 생활에서 계약을 하는 상황을 떠올려보자.
인턴 계약을 한다고 하면 인턴 입장에서는 돈을 지급 받으면서, 그에 대한 대가로 회사에게 일을 해준다.
반대로, 회사는 일을 진행 하면서 그에 대한 대가로 돈을 지급한다.
인턴을 시작할 때 이런 내용을 계약서에 담아서 명시적으로 계약관계를 맺는다.
만약 계약서에 명시한 내용을 위반하여 각자의 의무를 다하지 못했다면, 이익으로 명시된 것을 받지 못한다.
그리고 계약서에 명시된 내용 외에 일을 어떻게 하는지와 같은 방법에 대해서는 만족스러운 결과를 내기만 하면 계약을 이행한것으로 보고 이익을 받을 수 있다.
결국 계약은 협력을 명확하게 문서화 하여 정의하는 것이다.
객체세계에서의 계약
객체세계에서 계약은 사전조건, 사후조건, 불변식으로 정의된다.
- 사전조건: 메서드가 호출되기 위해서 만족해야하는 조건으로 클라이언트의 의무이다.
- 사후조건: 메서드가 실행된 후 클라이언트에게 보장해야하는 조건으로 서버의 의무이다.
- 불변식: 항상 참이라고 보장되는 서버의 조건으로, 메서드 실행 도중에는 만족되지 못하더라도, 메서드 실행 전이나 실행 후에는 반드시 만족되어야 한다.
예시를 통해서 계약에 의한 설계를 이해해보자.
일반적으로 메서드는 시그니처를 통해 메서드 사용 시 필요한 타입과 반환타입이 어떤 것인지 판단할 수 있다.
public Reservation reserve (Customer customer, int audienceCount) { ... }
하지만 시그니처만으로 세부 계약 조건까지 명시하는 것은 어렵기 때문에 계약에 의한 설계를 사용한다.
public Reservation reserve (Customer customer, int audienceCount) {
// 사전 조건 명시
Contract.Requires(customer != null);
Contract.Requires(audienceCount >= 1);
// 사후 조건 명시
Contract.Ensures(Contract.Result<Reservation>() != null);
return new Reservation(Customer, this, calculateFee(audienceCount), audienceCount);
}
위 코드에서 Contract.Requires 를 통해서 클라이언트에서 수행해야하는 계약의 사전조건을 명시하였다.
만약 Client에서 이를 어기는 요청을 한다면 ContractException 예외가 발생할 것이다.
또한 Contract.Ensures를 통해서 메서드의 반환값이 null 이면 안된다는 계약 사후조건을 명시하였다.
서버 측에서 이를 어기는 반환을 한다면 똑같이 ContractException 예외가 발생할 것이다.
사후 조건을 명시할 때 메서드를 통해서 값이 변경되지만, 그 이전 값을 활용하고 싶을 때는 OldValue<T> 메서드를 활용할 수 있다.
public string Middle(string text) {
Contract.Requires(text != null && text.Length >= 2);
Contract.Ensures(Coontract.Result<string>().Length < Contract.OldValue<string>(text).Length);
text = text.Substring(1, text.Length-2);
return text.trim();
}
위와 같이 메서드 내부에서 text의 길이가 변경되지만, OldValue 메서드를 통해서 메서드 실행 이전과 비교하여 계약 달성 여부를 확인할 수 있다.
불변식은 사전조건과 사후조건에 공통으로 적용되는 조건이라고 생각하면 이해하기 쉽다.
예를 들어 Screening 인스턴스가 생성 될 때 movie는 null이 아니고 sequence는 1보다 크거나 같으며, whenScreened는 현재시간 이후여야 한다면 아래와 같이 ContractInvariantMethod를 활용하여 불변식을 작성할 수 있다.
public class Screening {
private Movie movie;
private int sequence;
private DateTime whenScreened;
[ContractInvariantMethod]
private void Invariant() {
Contract.Invariant(movie != null);
Contract.Invariant(sequence >= 1);
Contract.Invariant(whenScreened > DateTime.Now);
}
}
불변식을 사용하지 않는다면 모든 메서드에서 사전, 사후 조건을 작성해야 하지만, 불변식을 사용함으로써 클래스 내에서 한번만 계약 조건을 작성하면 모든 메서드가 계약조건을 따르도록 할 수 있다.
이를 통해 코드의 중복을 없에면서 계약의 조건을 명시할 수 있다.
자바에서의 계약
자바에서는 계약에 의한 설계를 위한 라이브러리를 제공하지 않지만, assert 선언문을 통해서 계약 조건을 명시하고 이를 검증할 수 있다.
assert expression1;
assert expression1 : expression2;
첫번째 assert 문은 인자로 전달된 값이 참이면 그냥 통과하고, 거짓이라면 AssertionError를 발생시킨다.
두번째 assert 문은 첫번째 표현식이 거짓인 경우 두번째 표현식의 값이 예외 메시지로 보여지도록 동작한다.
앞서 코드로 설명한 C#과 같이 편리하게 사전조건, 사후조건, 불변식을 명시할 순 없지만 계약조건이 필요하다면 assert 문을 활용하여 조건을 확인하도록 할 수 있다.
계약에 의한 설계 시 서브타이핑
계약에 의한 설계에 상속을 적용한다면 리스코프 치환 원칙을 고려하여 계약을해야 한다.
계약에 의한 설계의 관점에서 리스코프 치환 원칙을 만족시키는 것은 계약 규칙과 가변성 규칙이라는 두가지 원칙을 만족시키는 것으로 정의할 수 있다.
계약규칙
계약 규칙은 말 그대로 계약을 명시할 때 따라야 하는 규칙이다.
1. 서브 타입에 더 강력한 사전조건을 정의할 수 없다.
사전 조건을 만족 시키는 것은 클라이언트의 책임이다.
클라이언트는 슈퍼타입만을 의존하기 때문에 슈퍼타입과의 계약 조건을 만족하는 요청을 보낸다.
하지만 서브타입에서 슈퍼타입보다 더 강력한 계약조건을 명시한다면, 특정 요청에 대해서는 계약조건이 위배될 수 있다.
클라이언트 측에서는 계약한 슈퍼타입과의 계약조건보다 더 강력한 계약조건을 이행할 의무는 없기 때문에 이는 올바르지 못한 계약에 의한 설계이다.
반면 사전조건을 서브타입에서 완화하는 것은 상관없다.
이미 슈퍼타입의 사전조건을 만족하기 위한 요청을 클라이언트가 보내기 때문에, 서브타입에서 이를 완화화더라도 서브타입을 슈퍼타입으로 교체할 수 있다.
2. 서브타입에 더 완화된 사후 조건을 정의 할 수 없다.
서브타입이 슈퍼타입보다 더 완화된 사후조건을 가지고 있다면, 서브타입이 더 넓은 범위의 조건을 포함하게 된다.
이렇게 되면 슈퍼타입의 객체를 서브타입이 대체할 수 없기 때문에 리스코프 치환 원칙을 위반한다.
3. 슈퍼타입의 불변식은 서브타입에서도 반드시 유지되어야 한다.
서브타입에서 불변식을 따르지 않는다면, 서브타입이 슈퍼타입의 객체를 대체할 수 없다.
가변성 규칙
가변성 규칙을 배우기 전에 공변성, 반공변성, 무공변성에 대해 먼저 이해해보자.
S가 T의 서브타입이라고 가정할 때
- 공변성: S와 T 사이의 서브타입 관계가 그대로 유지된다.
- 반공변성: S와 T 사이의 서브타입 관계가 역전된다.
- 무공변성: S와 T사이에는 아무런 관계도 존재하지 않는다.
즉, 슈퍼타입과 서브타입 관계가 어떻게 변화하여 대체가 가능한지를 표현할 때 공변성, 반공변성 용어를 사용한다.
가변성 규칙은 대체 가능성에 관한 규칙으로 아래 3가지 규칙이 포함된다.
1. 서브타입의 리턴타입은 공변성을 가져야 한다.
쉽게 말해서 서브 타입의 리턴 타입은 부모타입으로 대체가 가능해야한다라는 의미이다.
앞서 서브타입에서 더 완화된 사후조건을 사용할 수 없다고 하였다.
서브타입의 리턴타입이 공변성을 가진다는 것은 서브타입이 더 강력한 사후조건을 가지고 있다는 뜻과 동일하기 때문에 계약 조건과 같은 의미이다.
2. 서브 타입의 메서드 파라미터는 반공변성을 가져야 한다.
부모 클래스에서 구현된 메서드를 자식 클래스에서 오버라이딩 할 때 파라미터 타입을 부모 클래스에서 사용한 파라미터의 슈퍼타입으로 지정할 수 있어야 한다는 의미이다.
앞서 서브타입에서 더 약화된 사전조건을 적용하는것은 문제가 되지 않는다고 하였다.
슈퍼타입은 서브타입보다 더 약화된 조건이기 때문에 이를 대체하는 것이 가능하다는 것과 연결된다.
하지만 자바에서는 서브타입에서 메서드를 오버라이딩 시 파라미터를 부모타입으로 변경하면 메서드 오버라이딩이 아닌 메서드 오버로딩으로 여기며 완전히 다르게 동작한다.
(언어마다 계약에 의한 설계의 지원에 차이가 있다고 이해하면 될 것이다.)
3. 서브타입은 슈퍼타입이 발생시키는 예외와 다른 타입의 예외를 발생시켜서는 안된다.
서브타입에서 슈퍼타입과 다른 타입의 예외를 발생시키면, 예외를 잡지 못하여 사용자가 예측하지 못한 결과를 얻을 수 있기 떄문에 슈퍼타입이 발생시키는 예외와 같거나 하위 타입의 예외를 발생시켜야 한다.
이렇게 계약 규칙과 가변성 규칙을 만족해야만 슈퍼타입 객체를 서브타입이 완전히 대체하여 리스코프 치환 원칙을 만족 시킬 수 있으며, 계약이 위반된 곳에서 즉시 에러가 발생하여 디버깅을 용이하게 할 수 있다.
느낀점
계약에 의한 설계가 자바에서 지원하는 기능이 아니기 때문에 생소하다 느껴 이해하는데 많은 시간이 걸렸다.
중요한 것은 설계를 할 때 서브타이핑을 고려해서 조건을 명시해야한다는 것이라고 생각한다.
이번 챕터의 내용을 이해하는 과정에서 리스코프 치환 원칙을 단순히 슈퍼타입과 서브타입의 대체 여부로만 보는 것이 아니라, 조건을 적용하는 상황에서 어떻게 적용될 수 있는지도 공부할 수 있었다.
'스터디' 카테고리의 다른 글
[스터디: 오브젝트] 동적인 협력과 정적인 코드 (0) | 2024.03.21 |
---|---|
[스터디: 오브젝트] 타입계층의 구현 방법 (0) | 2024.03.21 |
[스터디: 오브젝트] 디자인패턴과 프레임워크 (0) | 2024.03.18 |
[스터디: 오브젝트] 다형성 (0) | 2024.03.07 |
[OS 모의 면접 스터디] 컴퓨터 시스템의 동작원리 (4) | 2024.03.06 |