오브젝트 책 핵심 정리

2022-01-13

오브젝트 - 조영호

객체지향 설계의 핵심은 적절한 협력을 식별하고 협력에 필요한 역할을 정의한 후에 역할을 수행할 수 있는 적절한 책임을 할당 하는 것이다.

객체지향 패러다임의 관점에서 핵심은 역할(role), 책임(responsibility), 협력(collaboration) 이다.

구현을 먼저 생각하지 말고, 역할과 책임과 협력에 대해 먼저 생각하자. 그 후 필요한 것은 상속과 다형성을 이용하는 일이다.

협력

객체들이 애플리케이션의 기능을 구현하기 위해 수행하는 상호작용을 협력이라고 한다.

책임

객체가 협력에 참여하기 위해 수행하는 로직을 책임이라고 부른다.

책임이란 객체에 의해 정의되는 응집도 있는 행위의 집합으로, 객체가 유지해야 하는 정보와 수행할 수 있는 행동에 대해 개략적으로 서술한 문장이다. 즉, 객체의 책임은 객체가 ‘무엇을 알고 있는가’ 와 ‘무엇을 할 수 있는가’로 구성된다. 크레이그 라만은 이러한 분류 체계에 따라 객체의 책임을 크게 ‘하는 것’과 ‘아는 것’의 두 가지 범주로 나누어 세분화 하고 있다.

하는 것

  • 객체를 생성하거나 계산을 수행하는 등이 스스로 하는 것
  • 다른 객체의 행동을 시작시키는 것
  • 다른 객체의 활동을 제어하고 조절하는 것

아는 것

  • 사적인 정보에 관해 아는 것
  • 관련된 객체에 관해 아는 것
  • 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것

또한 설계를 할 때 “이 객체가 어떤 데이터를 포함해야 하는가?”라는 질문을 다음과 같은 두개의 개별적으로 질문으로 분리해야 한다.

  • 이 객체가 어떤 데이터를 포함해야 하는가?
  • 이 객체가 데이터에 대해 수행해야 하는 오퍼레이션은 무엇인가?

역할

책임들이 모여 객체가 수행하는 역할을 구성한다.

객체는 협력이라는 주어진 문맥 안에서 특정한 목적을 갖게 된다. 객체의 목적은 협력 안에서 객체가 맡게 되는 책임의 집합으로 표시된다. 이처럼 객체가 어떤 특정한 협력 안에서 수행하는 책임의 집합을 역할이라고 부른다. 실제로 협력을 모델링할 때는 특정한 객체가 아니라 역할에게 책임을 할당한다고 생각하는게 좋다.

역할을 구현하는 가장 일반적인 방법은 추상 클래스와 인터페이스를 사용하는 것. 책의 예제로 들자면 DiscountPolicy가 역할을 맡고, NoneDiscountPolicy가 역할을 구현했음.

객체에 관해 생각할 때 “이 객체가 무슨 역할을 수행해야 하는가?” 라고 자문하는 것이 도움이 된다.

// 역할
public interface DiscountPolicy {
    Money calculateDiscountMoney(Screen screen);
}

응집도 계산

좋은 설계를 갖기 위해 높은 응집도와 낮은 결합도를 가진 설계를 추구해야한다.

하나의 변경을 수용하기 위해 모듈 전체가 함께 변경된다면 응집도가 높은 것이고 모듈의 일부만 변경된다면 낮은 것이다.


캡슐화

상태와 행동을 하나의 객체 안에 모으는 이유는 객체의 내부 구현을 외부로부터 감추기 위해서다. 여기서 구현이란 나중에 변경될 가능성이 높은 어떤 것을 가리킨다. 객체지향이 강력한 이유는 한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않도록 파급효과를 적절하게 조절할 수 있는 장치를 제공하기 때문이다. 객체를 사용하면 변경 가능성이 높은 부분은 내부에 숨기고 외부에는 상대적으로 안정적인 부분만 공개함으로써 변경의 여파를 통제할 수 있다.

변경될 가능성이 높은 부분을 구현이라고 부르고 상대적으로 안정적인 부분을 인터페이스라고 부른다는 사실을 기억하라. 객체를 설계하기 위한 가장 기본적인 아이디어는 변경의 정도에 따라 구현과 인터페이스를 분리하고 외부에서는 인터페이스에만 의존하도록 관계를 조절하는 것이다.

정리하면 캡슐화란 변경 가능성이 높은 부분을 객체 내부로 숨기는 추상화 기법이다. 객체 내부에 무엇을 캡슐화해야 하는가? 변경될 수 있는 어떤 것이라도 캡슐화해야 한다. 이것이 바로 객체지향 설계의 핵심이다.

유연한 객체지향 프로그램을 위해서는 컴파일 시간 의존성과 실행 시간 의존성이 달라야 한다.

만약 데이터 중심의 설계를 하게 된다면 너무 이른 시기에 데이터에 대해 고민하기 때문에 캡슐화에 실패하게 된다.

CREATOR 패턴

객체 A를 생성해야 할 때 어떤 객체에게 객체 생성 책임을 할당해야 하는가? 아래 조건을 최대한 많이 만족하는 B에게 객체 생성 책임을 할당하라.

  • B가 A 객체를 포함하거나 참조한다.
  • B가 A 객체를 기록한다.
  • B가 A 객체를 긴밀하게 사용한다.
  • B가 A 객체를 초기화하는 데 필요한 데이터를 가지고 있다.( 이 경우 B는 A에 대한 정보 전문가다)

CREATOR 패턴의 의도는 어떤 방식으로든 생성되는 객체와 연결되거나 관련될 필요가 있는 객체에 해당 객체를 생성할 책임을 맡기는 것이다. 생성될 객체에 대해 잘 알고 있어야 하거나 그 객체를 사용해야 하는 객체는 어떤 방식이로든 생성될 객체와 연결될 것이다. 다시 말해서 두 객체는 서로 결합된다.

이미 결합돼 있는 객체에게 생성 책임을 할당하는 것은 설계의 전체적인 결합도에 영향을 미치지 않는다. 결과적으로 CREATOR 패턴은 이미 존재하는 객체 사이의 관계를 이용하기 때문에 설계가 낮은 결합도를 유지할 수 있게 한다.

기존에 있는 클래스를 리팩토링 해서 다듬어보는 방법

기존의 설계가 이상 없는지 확인하는 방법은, 변경의 이유가 하나 이상인 클래스를 찾는 것으로부터 시작하는 것이 좋다.

첫 번째 방법은 인스턴스 변수가 초기화되는 시점을 살펴보는 것이다. 응집도가 높은 클래스는 인스턴스를 생성할 때 모든 속성을 함께 초기화한다. 반면 응집도가 낮은 클래스는 객체의 속성 중 일부만 초기화하고 일부는 초기화되지 않는 상태로 남겨진다.

두 번째 방법은 메서드들이 인스턴스 변수를 사용하는 방식을 살펴보는 것이다. 모든 메서드가 객체의 모든 속성을 사용한다면 클래스의 응집도는 높다고 볼 수 있다. 반면 메서드들이 사용하는 속성에 따라 그룹이 나뉜다면 클래스의 응집도가 낮다고 볼 수 있다.

클래스 응집도 판단하기

클래스가 다음과 같은 징후로 몸살을 앓고 있다면 클래스의 응집도가 낮은 것이다.

  • 클래스가 하나 이상의 이유로 변경돼야 한다면 응집도가 낮은 것이다. 변경의 이유를 기준으로 클래스를 분리하라.
  • 클래스의 인스턴스를 초기화하는 시점에 경우에 따라 서로 다른 속성들을 초기화하고 있다면 응집도가 낮은 것이다. 초기화되는 속성의 그룹을 기준으로 클래스를 분리하라.
  • 메서드 그룹이 속성 그룹을 사용하는지 여부로 나뉜다면 응집도가 낮은 것이다. 이들 그룹을 기준으로 클래스를 분리하라.

인터페이스와 설계 품질

좋은 인터페이스는 최소한의 인터페이스와 추상적인 인터페이스라는 조건을 만족해야 한다. 최소한의 인터페이스는 꼭 필요한 오퍼레이션만을 인터페이스에 포함한다. 추상적인 인터페이스는 어떻게 수행하는지 아니라 무엇을 하는지 표현한다.

다음은 퍼블릭 인터페이스의 품질에 영향을 미치는 원칙과 기법이다.

  • 디미터 법칙
  • 묻지 말고 시켜라
  • 의도를 드러내는 인터페이스
  • 명령-쿼리 분리

1. 디미터 법칙

디미터 법칙은 잘 알려진 발견법으로 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다. 객체는 자료를 숨기고 함수를 공개한다. 즉 객체는 조회 함수로 내부 구조를 공개하면 안된다는 의미다. 그러면 내부 구조를 노출하는 셈이니까, 정확히 표현하자면, 디미터 법칙은 “클래스 C의 메소드 f는 다음 객체의 메소드만 호출해야 한다”고 주장한다

  • 클래스 C
  • f가 생성한 객체
  • f 인수로 넘어온 객체
  • C 인스턴스 변수에 저장된 객체

그리고 오직 하나의 도트만 사용해야 한다.

다음은 디미터의 법칙을 위반하는 전형적인 코드이다.

screening.getMovie().getDiscountConditions();

가끔식은 묻는 것 외에도 다른 방법이 존재하지 않는 경우도 있다. 컬렉션에 포함된 객체들을 처리하는 유일한 방법은 객체에게 물어보는 것이다. 다음 코드에서 Moive에게 묻지 않고도 movies 컬렉션에 포함된 전체 영화의 가격을 계산할 수 있는 방법이 있을까?

for(Movie each :  movies) {
	total += each.getFee(); 
}

물으려는 객체가 정말로 데이터인 경우가 있다. 로버튼 마틴은 클린코드에서 디미터 법칙의 위반 여부는 묻는 대상이 객체인지, 자료 구조인지에 달려있다고 설명한다. 객체는 내부 구조를 숨겨야 하므로 디미터 법칙을 따르다는 것이 좋지만 자료구조라면 당연히 내부를 노출해야 하므로 디미터 법칙을 적용할 필요가 없다.

객체에게 시키는 것이 항상 가능한 것은 아니다. 가끔씩은 물어야 한다. 여기서 강조하고 싶은 것은 소프트웨어 설계에 법칙이란 존재하지 않는다는 것이다. 원칙을 맹신하지 마라. 원칙이 적절한 상황과 부적절한 상활을 판단할 수 있는 안목을 길러라. 설계는 트레이드오프의 산물이다. 소프트웨어 설계에 존재하는 몇 안 안되는 법칙 중 하나는 “경우에 따라 다르다”라는 사실을 명심하자.

💡

협력이라는 컨텍스트 안에서 객체보다 메시지를 먼저 결정하면 두 객체 사이의 구조적인 결합도를 낮출 수 있다. 수신할 객체를 알지 못한 상태에서 메시지를 먼저 선택하기 때문에 객체의 내부 구조에 대해 고민할 필요가 없어진 다.따라서 메시지가 객체를 선택하게함으로써 의도적으로 디미터법칙을 위반할 위험을 최소화할수있다.

2. 묻지 말고 시켜라

묻지 말고 시켜라 원칙과 디미터 법칙은 훌륭한 인터페이스를 제공하기 위해 포함해야 하는 오퍼레이션에 대한 힌트를 제공한다. 내부의 상태를 묻는 오퍼레이션을 인터페이스에 포함시키고 있다면 더 나은 방법은 없는지 고민해 보라. 내부의 상태를 이용해 어떤 결정을 내리는 로직이 객체 외부에 존재하는가? 그렇다면 해당 객체가 책임져야 하는 어떤 행동이 객체 외부로 누수된 것이다.

상태를 묻는 오퍼레이션을 행동을 요청하는 오퍼레이션으로 대체함으로써 인터페이스를 향상시켜라. 협력을 설계하고 객체가 수신할 메시지를 결정하는 매 순간 묻지 말고 시켜라 원칙과 디미터 법칙을 머릿속에 떠올리는 것은 퍼블릭 인터페이스의 품질을 향상시킬 수 잇는 좋은 습관이다.

public boolean getAdmin(); // 이것 보단

public boolean isAdmin(); // 이렇게..

하지만 단순하게 객체에게 묻지 않고 시킨다고 해서 모든 문제가 해결되는 것은 아니다. 훌륭한 인터페이스를 수확하기 위해서는 객체가 어떻게 작업을 수행하는지를 노출해서는 안 된다. 인터페이스는 객체가 어떻게 하는지가 아니라 무엇을 하는지를 서술해야 한다.

💡

메시지를 먼저 선택하면 묻지 말고 시켜라 스타일에 따라 협력을 구조화하게 된다. 클라이언트의 관점에서 메시지를 선택하기 때문에 필요한정보를 물을 필요 없이 원하는 것을 표현한 메시지를 전송하면 된다.

3. 의도를 드러내는 인터페이스

켄트 벡이 메서드를 명명하는 두 가지 방법을 설명했다. 첫번 째 방법은 메서드가 작업을 어떻게 수행하는지를 나타내도록 이름 짓는 것이다. 이 경우 메서드의 이름은 내부의 구현 방법을 드러낸다. 다음은 첫 번째 방법에 따라 PeriodCondition과 SequenceCondition의 메서드를 명명한 것이다.

public class PeriodCondition {
	public boolean isSatisfiedByPeriod(Screening screening) {...}
}

public class SequenceCondition {
	public boolean isSatisfiedBySequence(Screening screening) {...}
}

이런 스타일은 좋지 않는데 그 이유를 두 가지로 요약할 수 있다.

  • 메서드에 대해 제대로 커뮤니케이션하지 못한다. 클라이언트의 관점에서 isSatisfiedByPeriod와 isSatisfiedBySequence 모두 할인 조건을 판단하는 동일한 작업을 수행한다. 하지만 메서드의 이름이 다르기 때문에 두 메서드의 내부 구현을 정확하게 이해하지 못한다면 두 메서드가 동일한 작업을 수행한다는 사실을 알아채기 어렵다.
  • 더 큰 문제는 메서드 수준에서 캡슐화를 위반한다는 것이다. 이 메서드들은 클라이언트로 하여금 협력하는 객체의 종류를 알도록 강요한다. PeriodCondition을 사용하는 코드를 SequenceCondition을 사용하도록 단순히 참조하는 객체를 변경하는 것뿐만 아니라 호출하는 메서드를 변경해야 한다. 만약 할인 여부를 판단하는 방법이 변경된다면 메서드의 이름 역시 변경해야 할 것이다. 메서드 이름을 변경한다는 것은 메시지를 전송하는 클라이언트의 코드도 함께 변경해야 한다는 것을 의미한다. 따라서 책임을 수행하는 방법을 드러내는 메서드를 사용한 설계는 변경에 취약할 수 밖에 없다.

💡

메시지를 먼저 선택한다는 것은 메시지를 전송하는 클라이언트의 관점에서 메시지의 이름을 정한다는 것이다.당연히 그 이름에는 클라이언트가 무엇을 원하는지,그 의도가 분명하게 드러날 수밖에 없다.

4. 명령-쿼리 분리 원칙

명령-쿼리 분리 원칙은 퍼블릭 인터페이스에 오퍼레이션을 정의할 때 참고할 수 있는 지침을 제공한다.

명령-쿼리 분리 원칙의 요지는 오퍼레이션은 부수효과를 발생시키는 명령이거나 부수효과를 발생시키지 않는 쿼리중 하나여야 한다는 것이다. 어떤 오퍼레이션도 명령인 동시에 쿼리여서는 안 된다. 따라서 명령과 쿼리를 분리하기 위해서는 다음의 두 가지 규칙을 준수해야 한다.

  • 객체의 상태를 변경하는 명령은 반환값을 가질 수 없다.
  • 객체의 정보를 반환하는 쿼리는 상태를 변경할 수 없다.

명령-쿼리 분리 원칙을 한 문장으로 표현하면 “질문이 답변을 수정해서는 안 된다”는 것이다.명령은 상태를 변경할 수 있지만 상태를 반환해서는 안 된다. 쿼리는 객체의 상태를 반환할 수 있지만 상태를 변경해서는 안 된다.

부수효과를 발생시키지 않는 것만을 함수로 제한함으로써 소프트웨어에서 말하는 ‘함수’의 개념이 일반 수학에서의 개념과 상충되지 않게 한다. 객체를 변경하지만 직접적으로 값을 반환하지 않는 명령(Command)과 객체에 대한 정보를 반환하지만 변경하지 않는 쿼리(Query)간의 명확한 구분을 유지할 것이다.

명령-쿼리 분리 원칙을 잘 지키면 예측이 가능하고 이해하기 쉬우며 디버깅이 용이한 동시에 유지보수가 수월해질 것이다.

💡

메시지를 먼저 선택한다는 것은 협력이라는 문맥 안에서 객체의 인터페이스에 관해 고민한다는 것을 의미한다. 객체가 단순히 어떤 일을 해야 하는지뿐만 아니라 협력 속에서 객체의 상태를 예측하고 이해하기 쉽게 만들기 위한 방법에 관해 고민하게 된다.따라서 예측가능한 협력을만들기 위해 명령과쿼리를 분리하게 될것이다.

의존성 역전 원칙과 패키지

이미지내용

(그림 1) 인터페이스가 서버 모듈 쪽에 위치하는 전통적인 모듈 구조

이미지내용

(그림 2) 인터페이스의 소유권을 역전시킨 객체지향적인 모듈 구조

의존성 역전 원칙에 따라 상위 수준의 협력 흐름을 재시용하기 위해서는 추상화가 제공히는 인터페이스의 소유권 역시 역전시켜야 한다. 전통적인 설계 패러다임은 그림1 와 같이 인터페이스의 소유권을 클라이언트 모듈이 아닌 서버 모듈에 위치시키는 반면 잘 설계된 객체지향 애플리케이션에서는 그림 2 과 같이 인터페이스의 소유권을 서버가 아닌 클라이언트에 위치시킨다. 객체지향 프레임워크의 모듈 구조를 설계하는 데 가장 중요한 핵심 원칙이다.

상속에 대해서

코드 재사용을 목적으로 상속을 사용하면 변경하기 어렵고 유연하지 못한 설계에 이를 확률이 높아진다. 싱속의 목적은 코드 재사용이 아니다. 싱속은 타입 계층을 구조화하기 위해 사용해야 한다. 타입 계층은 객체지향 프로그래밍의 중요한 특성 중 하나인 다형성의 기반을 제공한다.

상속을 이용해 자식 클래스를 추가하려 한다면 스스로에게 디음과 같은 질문을 해보기 바란다. 싱속을 사용하려는 목적이 단순히 코드를 재사용하기 위해서인가? 아니면 클라이언트 관점에서 인스턴스들을 동일하게 행동하는 그룹으로 묶기 위해서인기? 첫 번째 질문에 대한 답이 ‘예’라면 싱속을 사용하지 말 이야 한다.

서브클래싱과 서브타이핑

사람들은 상속을 사용하는 두 가지 목적에 특별한 이름을 붙였는데 서브클래싱과 서브타이핑이 그것이다.

  • 서브클래싱(subclassing) : 다른 클래스의 코드를 재사용할 목적으로 상속하는 경우를 말한다. 자식 클래스와 부모 클래스의 행동이 호환되지 않기 때문에 자식 클래스의 인스턴스가 부모 인스턴스를 대체할 수 없다. 서브클래싱을 구현 상속 또는 클래스 상속 이라고 부르기도 한다.
  • 서브타이핑(subtyping) : 타입 계층을 구성하기 위해 상속을 사용하는 경우를 가리킨다. 서브타이핑에서는 자식 클래스와 부모 클래스의 행동이 호환되기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 있다. 이때 부모 클래스는 자식 클래스의 슈퍼타입이 되고 자식 클래스는 부모 클래스의 서브타입이 된다. 서브타이핑을 인터페이스 상속 이라고 부르기도 한다.

자식 클래스가 부모 클래스의 코드를 재사용할 목적이라면 서브 클래싱, 부모 클래스의 인스턴스 대신 자식 클래스의 인스턴스를 사용할 목적으로 상속했다면 서브 타이핑

서브클래싱과 서브타이핑을 나누는 기준은 상속을 사용하는 목적이다.

is-a 관계로 표현된 문장을 볼 때마다 문장 앞에 “클라이언트 입장에서” 라는 말이 빠져 있다고 생각하라. (클라이언트 입장에서) 정사각형은 직사각형이다. (클라이언트 입장에서) 펭퀸은 새다. 클라이언트를 배제한 is-a 관계는 혼란으로 몰아갈 가능성이 높다.

is-a 관계는 객체지향에서 중요한것은 객체의 속성이 아니라 객체의 행동이라는 점을 강조한다. 일반적으로 클라이언트를 고려하지 않은 채 개념과 속성의 측면에서 상속 관계를 정할 경우 리스코프 치환 원칙을 위반하는 서브클래싱에 이르게 될 확률이 높다.

슈퍼타입과 서브타입이 클라이언트 입장에서 행동이 호환된다면 두 타입을 is-a로 연결해 문장을 만들어도 어색하지 않은 단어로 타입의 이름을 정하라는 것이다. 행동을 고려하지 않은 두 타입의 이름이 단순히 is-a로 연결 가능하다고 해서 상속 관계로 연결하지 마라. 이름이 아니라 행동이 먼저다. 객체지향과 관련된 대부분의 규칙이 그런 것 처럼 is-a 관계 역시 행동이 우선이다.

결론적으로 상속이 서브타이핑을 위해 사용될 경우에만 is-a 관계다. 서브클래싱을 구현하기 위해 상속을 했다면 is-a 관계라고 말할 수 없다.

사전조건과 사후조건

(추후 글로 설명)

저자의 의도를 그림으로 표현 한 것

이미지내용

위임(delegation)

자신이 수신한 메세지를 다른 객체에게 동일하게 전달해서 처리를 요청 하는 것을 위임(delegation)이라고 부른다.

위임은 본질적으로 자신이 정의하거나 처리할 수 없는 속성 또는 메서드의 탐색 과정을 다른 객체로 이동시키기 위해 사용한다. 이를 위해 위임은 항상 현재의 실행 문맥을 가리키는 self 참조를 인자로 전달한다. 바로 이것이 self 참조를 전달하지 않는 포워딩과 위임의 차이점이다.

포워딩과 위임

객체가 다른 객체에게 요청을 처리할 때 인자로 self를 전달하지 않을 수도 있다. 이것은 요청을 전달받은 최초의 객체에 다시 메시지를 전송할 필요는 없고 단순히 코드를 재사용하고 싶은 경우라고 할 수 있다. 이처럼 처리를 요청할 때 self 참조를 전달하지 않는 경우를 포워딩이라고 부른다. 이와 달리 self 참조를 전달하는 경우에는 위임이라고 부른다. 위임의 정확한 용도는 클래스를 이용한 상속 관계를 객체 사이의 합성 관계로 대체해서 다형성을 구현하는 것이다.

설계에 일관성 부여하기

일관성 있는 설계를 위한 조언은 널리 알려진 디자인 패턴을 학습하고 변경이라는 문맥 안에서 디자인 패턴을 적용해 보는 것이다. 디자인 패턴은 특정한 변경에 대해 일관성 있는 설계를 만들 수 있는 경험 법칙을 모앙놓은 일종의 설계 템플릿이다. 디자인 패턴을 학습하면 빠른 시간 안에 전문가의 경험을 흡수 할 수 있다.

비록 디자인 패턴이 반복적으로 적용할 수 있는 설계 구조를 제공한다고 하더라도 모든 경우에 적합한 패턴을 찾을 수 있는 것은 아니다. 따라서 협력을 일관성 있게 만들기 위해 다음과 같은 기본 지침을 따르는 것이 도움이 된다.

  • 변하는 개념을 변하지 않는 개념으로부터 분리하라.
  • 변하는 개념을 캡슐화 하라.

애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리시킨다. 이것은 여러 설계 원칙 중에서 첫번 째 원칙이다. 즉, 코드에서 새로운 요구사항이 있을 때마다 바뀌는 부분이 있다면 그 행동을 바뀌지 않는 다른 부분으로부터 골라내서 분리해야 한다는 것을 알 수 있다. 이 원칙은 다음과 같은 식으로 생각할 수도 있다. “바뀌는 부분을 따로 뽑아서 캡슐화한다. 그렇게 하면 나중에 바뀌지 않는 부분에는 영향을 미치지 않은 채로 그 부분만 고치거나 확장할 수 있다.”

공변성 반공변성 무공변성

S extends T 일 경우를 가정.

  • 공변성(covariance) : S와 T 사이의 서브타입 관계가 그대로 유지된다. 이 경우 해당 위치에서 서브타입인 S가 슈퍼타입인 T 대신 사용될 수 있다. 우리가 흔히 이야기하는 리스코프 치환 원칙은 공변성과 관련된 원칙이라고 생각하면 된다.

    • 리턴 타입 공변성(return type covariance) : 부모 클래스에서 구현된 메서드를 자식 클래스에서 오버라이딩 할 때 부모 클래스에 선언한 반환타입의 서브타입으로 지정할 수 있는 특성을 리턴 타입 공변성이라고 부른다. 간단하게 말해서 리턴 타입 공변성이란 메서드를 구현한 클래스의 타입 계층 방향과 리턴 타입의 타입 계층 방향이 동일한 경우를 가리킨다.

      이미지내용

  • 반공변성(contravariance) : S와 T 사이의 서브타입 관계가 역전된다. 이 경우 해당 위치에서 슈퍼타입인 T가 서브타입인 S 대신 사용될 수 있다.

    • 파라미터 타입 반공변성(parameter type contravariance) : 부모 클래스에서 구현된 메서드를 자식 클래스에서 오버라이딩할 때 파라미터 타입을 부모 클래스에서 사용한 파라미터의 슈퍼타입으로 지정할 수 있는 특성을 파라미터 타입 반공변성이라고 부른다. 간단하게 말해서 파라미터 타입 반공변성이란 메서드를 정의한 클래스의 타입 계층과 파라미터의 타입 계층의 방향이 반대인 경우 서브타입 관계를 만족한다는 것을 의미한다.

      이미지내용

      💡

      IndependentPublisher가 Publisher의 서브타입일 경우를 가정.

  • 무공변성(invariance) : S와 T 사이에는 아무런 관계가 존재하지 않는다. 따라서 S대신 T를 사용하거나 대신 S를 사용할 수 없다.

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

부록

CRC 카드

이미지내용