타입프로그래밍 언어 관점의 타입객체지향 패러다임 관점의 타입타입 계층상속을 언제 사용해야 하는가?1. is-a 관계2. 행동 호환성클라이언트의 기대에 따라 계층 분리하기 (ISP - 인터페이스 분리 원칙)서브 클래싱과 서브 타이핑 (상속의 용도 2가지)리스코프 치환원칙리스코프 치환 원칙은 유연한 설계의 기반임
이 장에서는 올바른
타입 계층
을 구성하는 원칙을 살펴본다.타입
프로그래밍 언어 관점의 타입
프로그래밍 언어에서 타입의 두 가지 목적
- 타입에 수행될 수 있는 유효한 오퍼레이션의 집합을 정의
- 타입에 수행되는 오퍼레이션에 대해 미리 약속된 문맥을 제공
정리하면 타입은 적용 가능한 오퍼레이션의 종류와 의미를 정의 함으로써 코드의 의미를 명확하게 전달하고 개발자의 실수를 방지하기 위해 사용됨
객체지향 패러다임 관점의 타입
- 개념 관점에서 타입이란 공통의 특징을 공유하는 대상들의 분류
- 프로그래밍 언어 관점에서 타입이란 동일한 오퍼레이션을 적용할 수 있는 인스턴스들의 집합
객체의 퍼블릭 인터페이스가 객체의 타입을 결정한다. 따라서 동일한 퍼블릭 인터페이스를 제공하는 객체들은 동일한 타입으로 분류된다. 객체에게 중요한 것은 속성이 아니라 행동이다!! 어떤 객체들이 동일한 상태를 가지고 있어도 퍼블릭 인터페이스가 다르다면 이들은 서로 다른 타입으로 분류!
타입 계층
슈퍼타입
: 서브타입이 정의한 퍼블릭 인터페이스를 일반화시켜 상대적으로 범용적이고 넓은 의미로 정의한 것
서브타입
: 슈퍼타입이 정의한 퍼블릭 인터페이스를 특수화시켜 상대적으로 구체적이고 좁은 의미로 정의한 것
상속을 언제 사용해야 하는가?
두 타입 사이에 행동이 호환될 경우에만 타입 계층으로 묶어야 한다.
행동의 호환 여부를 판단하는 기준은 클라이언트의 관점이다.
클라이언트가 두 타입이 동일하게 행동할 것이라고 기대한다면 두 타입을 타입계층으로 묶을 수 있다.
- 상속 관계가 is-a 관계를 모델링하는가?
- 클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가?
- 상속 계층을 사용하는 클라이언트의 입장에서 부모 클래스와 자식 클래스의 차이점을 몰라야 한다. 이를 자식 클래스와 부모 클래스 사이의
행동호환성
이라 부름
1. is-a 관계
- 이 관계만으로는 불충분 함.
- 펭귄은 새다
- 새는 날 수 있다
- 펭귄은 날 수 없는 새임. 따라서 어휘적인 정의가 아닌 기대되는 행동에 따라 타입 계층을 구성해야 한다는 사실을 잘 보여줌
- 슈퍼타입과 서브타입 관계에서는 is-a 보다 행동 호환성이 더 중요하다.
2. 행동 호환성
- 펭귄이 새가 아니라는 사실을 받아들이기 위한 출발점은 타입이 행동과 관련이 있다는 사실에 주목하는 것
public class Bird { public void fly() { ... } ... } public class Penguin extends Bird { ... @Override public void fly() {} }
- 두 타입 사이에 행동이 호환될 경우에만 타입 계층으로 묶어야 한다
- 행동의 호환 여부를 판단하는 기준은
클라이언트의 관점
이라는 것
- 클라이언트가 두 타입이 동일하게 행동할 것이라고 기대한다면 두 타입을 타입 계층으로 묶을 수 있음
public void flyBird(Bird bird) { // 인자로 전달된 모든 bird는 날 수 있어야 한다 bird.fly(); }
- Penguin은 클라이언트에게 Bird와 행동 호환이 되지 않기에 Bird의 서브타입이 아니다.
클라이언트의 기대에 따라 계층 분리하기 (ISP - 인터페이스 분리 원칙)
행동 호환성을 만족시키지 않는 상속 계층을 그대로 유지한 채 클라이언트의 기대를 충족시킬 수 있는 방법은 쉽지 않다 → 클라이언트의 기대에 맞게 상속 계층을 분리하는 것 뿐이다.
public class Bird { ... } public class FlyingBird extends Bird { public void fly() { ... } ... } public class Penguin extends Bird { ... }
public void flyBird(FlyingBird bird) { bird.fly(); }
- flyBird 메서드는 FlyingBird 타입을 이용해 날 수 있는 새만 인자로 전달돼야 한다는 사실을 코드에 명시할 수 있음. 만약 날 수 없는 새와 협력하는 메서드가 존재한다면 파라미터의 타입을 Bird로 선언하면 됨

- 인터페이스는 클라이언트가 기대하는 바에 따라 분리돼야 한다는 것을 기억하기
- 하나의 클라이언트가 오직 fly 메시지만 전송하기를 원한다면 이 클라이언트에게는 fly 메시지만 보여야 함
- 다른 클라이언트가 오직 walk 메시지만 전송하기를 원한다면 이 클라이언트에게는 walk 메시지만 보여야 함
- 클라이언트에 따라 인터페이스를 분리하면 변경에 대한 영향을 더 세밀하게 제어할 수 있게 된다.
- 클라이언트에 따라 인터페이스를 분리하면 각 클라이언트의 요구가 바뀌더라도 영향의 파급 효과를 효과적으로 제어할 수 있게 된다.
인터페이스 분리원칙은 ‘비대한’ 인터페이스의 단점을 해결한다. 비대한 인터페이스를 가지는 클래스는 응집성이 없는 인터페이스를 가지는 클래스다. 즉, 이런 클래스의 인터페이스는 메서드의 그룹으로 분해될 수 있고, 각 그룹은 각기 다른 클라이언트 집합을 지원한다.
서브 클래싱과 서브 타이핑 (상속의 용도 2가지)
상속을 언제 사용해야 하는가? 상속의 두가지 목적
코드 재사용
(서브 클래싱
↔ 구현 상속 ↔ 클래스 상속 ) : 자식 클래스와 부모 클래스의 행동이 호환되지 않기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 없음- 서브 클래싱은 클래스의 내부 구현 자체를 상속받는 것에 초점을 맞춤 ( 구현 상속, 클래스 상속 )
타입 계층을 구성
(서브 타이핑
↔ 인터페이스 상속 ) : 타입 계층을 구성하기 위해 상속을 사용하는 경우 서브타이핑에서는 자식 클래스와 부모 클래스의 행동이 호환되기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스 대체 가능 부모 클래스는 자식 클래스의 슈퍼 타입, 자식 클래스는 부모 클래스의 서브타입- 서브 타이핑 관계가 유지되기 위해서는 서브타입이 슈퍼타입이 하는 모든 행동을 동일하게 할 수 있어야 함. 즉
행동 호환성
을 만족 시켜야 함 - 다시 말해 자식 클래스와 부모 클래스 사이의
행동 호환성
은 부모 클래스에 대한 자식 클래스의대체 가능성
을 포함함
행동 호환성
과대체 가능성
은 올바른 상속 관계를 구축하기 위해 따라야 할 지침 ⇒ 리스코프 치환 원칙
리스코프 치환원칙
여기서 요구되는 것은 다음의 치환 속성과 같은 것이다. S형의 각 객체 o1에 대해 T형의 객체 o2가 하나 있고, T에 의해 정의된 모든 프로그램 P 에서 T가 S로 치환될 때, P의 동작이 변하지 않으면 S는 T의 서브타입이다.
- 리스코프 치환 원칙을 한마디로 정리하면
“클라이언트가 차이점을 인식하지 못한 채 기반 클래스의 인터페이스를 통해 서브클래스를 사용할 수 있어야 한다”
는 것 리스코프 치환 원칙은 앞에서 논의한 행동 호환성을 설계 원칙으로 정리한 것임
자식 클래스가 부모 클래스와 행동 호환성
을 유지함으로써 부모 클래스를 대체할 수 있도록 구현된 상속 관계만을 서브타이핑
이라고 불러야 한다.
- 10장의 Vector와 Stack은 리스코프 치환 원칙을 위반하는 전형적인 예임
- 클라이언트가 부모 클래스인 Vector에게 기대하는 행동을 Stack에 대해서는 기대할 수 없기 때문에 행동 호환성을 만족시키지 않음
- 정사각형과 직사각형의 예시가 상속 개념에서 많이 쓰이는데 행동 호환성을 준수하지 않는(리스코프 치환원칙을 위반하는) 고전적인 사례 중 하나
public class Rectangle { private int x, y, width, height; public void setWidth(int width) { this.width= width; } public int setHeight(int height) { this.height = height; } } public class Square extends Rectangle { @Override public void setWidht(int width) { super.setWidth(width); super.setHeight(width); } @Override public void setHeight(int height) { super.setWidth(height); super.setHeight(height); } }
public void resize(Rectangle rectangle, int width, int height) { rectangle.setWidth(width); rectangle.setHeight(height); assert rectangle.getWidth() == width && rectangle.getHeight() == height; }
- 해당 코드에 Rectangle 대신 Square를 전달한다면?? 문제가 생기게 된다.
- resize의 메서드 구현은 Rectangle이 세운 가정에 기반하기 때문에 직사각형의 너비와 높이를 독립적으로 변경할 수 있다고 가정한다. 하지만 Rectangle의 자리에 Square를 전달할 경우 이 가정은 무너지게 된다. ⇒ Square는 Rectangle의 구현을 재사용하고 있을 뿐 행동 호환이 되지 않기에 리스코프 치환 원칙을 위반함.
서브타이핑 x
,서브클래싱 o
- 리스코프 치환 원칙은 클라이언트와 격리한 채로 본 모델은 의미 있게 검증하는 것이 불가능하다는 아주 중요한 결론을 이끈다. 어떤 모델의 유효성은 클라이언트의 관점에서만 검증 가능하다.
리스코프 치환 원칙은 유연한 설계의 기반임
- 리스코프 치환 원칙을 따르는 설계는 유연할 뿐 아니라 확장성이 높다.
- 자식 클래스가 클라이언트의 관점에서 부모 클래스를 대체할 수 있다면 기능 확장을 위해 자식 클래스를 추가하더라도 코드를 수정할 필요가 없어진다. 따라서 리스코프 치환 원칙은 개방-폐쇄 원칙을 만족하는 설계를 위한 전제 조건이다.