본문 바로가기
Study/오브젝트

[오브젝트] 13장. 서브클래싱과 서브타이핑

by Nahwasa 2023. 1. 14.

스터디 메인 페이지

목차

    - ☆ 표시가 붙은 부분은 스터디 중 나온 얘기 혹은 제 개인적인 생각이나 제가 이해한 방식을 적어놓은 것으로, 책에 나오지 않는 내용입니다. 따라서 책에서 말하고자 하는 바와 다를 수 있습니다.

    - 모든 이미지의 출처는 오브젝트(조용호 저) 책 입니다.

     


     

    CHAPTER 13. 서브클래싱과 서브타이핑

     

    ⚈ 상속은 두 가지 용도로 사용된다.

    • 첫 번째 용도는 타입 계층을 구현하는 것이다. 타입 계층 안에서 부모 클래스는 일반적인 개념을 구현하고 자식 클래스는 특수한 개념을 구현한다. 타입 계층의 관점에서 부모 클래스는 자식 클래스의 일반화(generalization)이고 자식 클래스는 부모 클래스의 특수화(specialization)다.
    • 두 번째 용도는 코드 재사용이다. 재사용을 위해 상속을 사용할 경우 부모 클래스와 자식 클래스가 강하게 결합되기 때문에 변경하기 어려운 코드를 얻게 될 확률이 높다.

     

    ⚈ 상속을 사용하는 일차적인 목표는 코드 재사용이 아니라 타입 계층을 구현하는 것이어야 한다. 동일한 메시지에 대해 서로 다르게 행동할 수 있는 다형적인 객체를 구현하기 위해서는 객체의 행동을 기반으로 타입 계층을 구성해야 한다.

     


    01 타입

    ⚈ 프로그래밍 언어 관점에서 타입은 연속적인 비트에 의미와 제약을 부여하기 위해 사용된다.

     

    ⚈ 객체지향 패러다임 관점의 타입

    • 객체지향 프로그래밍에서 타입을 정의하는 것은 객체의 퍼블릭 인터페이스(=객체가 수신할 수 있는 메시지의 집합)를 정하는 것과 동일하다.
    • 동일한 퍼블릭 인터페이스를 가지는 객체들은 동일한 타입으로 분류할 수 있다.
    • 어떤 객체들이 내부 상태는 다르지만 동일한 퍼블릭 인터페이스를 공유한다면 이들은 동일한 타입으로 분류된다.
    • 객체를 바라볼 때는 항상 객체가 외부에 제공하는 행동에 초점을 맞춰야 한다. 객체의 타입을 결정하는 것은 내부의 속성이 아니라 객체가 외부에 제공하는 행동이다.

     


    02 타입 계층

    ⚈ 슈퍼타입과 서브타입

    • 슈퍼타입(supertype) : 더 일반적인 타입.
    • 서브타입(subtype) : 더 특수한 타입
    • 프로그래밍 언어 타입은 객체지향 언어 타입과 절차적 언어 타입의 슈퍼타입. 객체지향 언어 타입은 클래스 기반 언어 타입과 프로토타입 기반 언어 타입의 슈퍼타입.

     

    객체지향 프로그래밍과 타입 계층

    • 객체의 타입을 결정하는 것은 퍼블릭 인터페이스
    • 퍼블릭 인터페이스 관점에서 슈퍼타입 : 서브타입이 정의한 퍼블릭 인터페이스를 일반화시켜 상대적으로 범용적이고 넓은 의미로 정의한 것
    • 퍼블릭 인터페이스 관점에서 서브타입 : 슈퍼타입이 정의한 퍼블릭 인터페이스를 특수화시켜 상대적으로 구체적이고 좁은 의미로 정의한 것
    • 서브타입의 인스턴스는 슈퍼타입의 인스턴스로 간주될 수 있다.

     


    03 서브클래싱과 서브타이핑

    상속을 이용해 타입 계층을 구현한다는 것은 부모 클래스가 슈퍼타입의 역할을, 자식 클래스가 서브타입의 역할을 수행하도록 클래스의 관계를 정의한다는 것을 의미한다.

     

    언제 상속을 사용해야 하는가?

    • 상속의 올바른 용도는 타입 계층을 구현하는 것
    • 이하 두 질문에 모두 '예'라고 답할 수 있는 경우에만 상속을 사용하라.
    • 1. 상속 관계가 is-a 관계를 모델링하는가?
    • 2. 클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가? (행동 호환성)

     

    ☆ is-a 관계 및 행동 호환성 둘 다 앞에 '클라이언트 관점에서' 라고 붙여서 생각해야 함.

    • is-a 관계에서 어휘적인 정의가 아니라 기대되는 행동에 따라 타입 계층을 구성해야 한다. 펭귄은 새지만 클라이언트 입장에서 새의 정의에 날 수 있다는 행동이 포함된다면 is-a 관계가 아니다. 어떤 애플리케이션에서 새에게 날 수 있다는 행동을 기대하지 않고 단지 울음 소리를 낼 수 있다는 행동만 기대한다면 새와 펭귄을 타입 계층으로 묶어도 무방하다.
    public class Bird {
    	public void fly() {...}
        ...
    }
    
    public class Penguin extends Bird {
    	...
    }

     

    • 행동의 호환 여부를 판단하는 기준 또한 클라이언트의 관점이다. 클라이언트가 두 타입이 동일하게 행동하지 않을 것이라고 기대한다면 두 타입을 타입 계층으로 묶어서는 안된다.

     

    • 이런 경우 상속이 아니라 합성을 통해 인터페이스를 클라이언트의 기대에 따라 분리해 해결하는게 더 좋은 방법이다. 이처럼 변경에 의해 영향을 제어하는 설계 원칙을 인터페이스 분리 원칙(Interface Segregation Principle, ISP)라고 부른다.

     

    서브클래싱(subclassing) : 다른 클래스의 코드를 재사용할 목적으로 상속을 사용하는 경우. 자식 클래스와 부모 클래스의 행동이 호환되지 않기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 없다. (=구현 상속, 클래스 상속)

     

    서브타이핑(subtyping) : 타입 계층을 구성하기 위해 상속을 사용하는 경우. 자식 클래스와 부모 클래스의 행동이 호환되기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 있다. 이 때 부모 클래스는 자식 클래스의 슈퍼타입이 되고 자식 클래스는 부모 클래스의 서브타입이 된다. (=인터페이스 상속)

     

    행동 호환성을 만족하는 상속 관계는 부모 클래스를 새로운 자식 클래스로 대체하더라도 시스템이 문제없이 동작할 것이라는 것을 보장해야 한다.

    • 즉, 부모 클래스와 자식 클래스의 행동 호환성은 부모 클래스에 대한 자식 클래스의 대체 가능성(subtitutability)을 포함한다.
    • 행동 호환성과 대체 가능성은 올바른 상속 관계를 구축하기 위해 따라야 할 지침이라고 할 수 있고, 이 지침을 리스코프 치환 원칙이라 한다.

     


    04 리스코프 치환 원칙

    리스코프 치환 원칙(Liskov Substitution Principle, LSP)

    • "서브타입은 그것의 기반 타입에 대해 대체 가능해야 한다"
    • 클라이언트가 "차이점을 인식하지 못한 채 기반 클래스의 인터페이스를 통해 서브클래스를 사용할 수 있어야 한다"
    • '03'에서 논의한 행동 호환성을 설계 원칙으로 정리한 것이 리스코프 치환 원칙

     

    리스코프 치환 원칙에 따르면 자식 클래스가 부모 클래스와 행동 호환성을 유지함으로써 부모 클래스를 대체할 수 있도록 구현된 상속 관계만을 서브타이핑이라고 불러야 한다.

     

    자바의 Stack과  Vector는 리스코프 치환 원칙을 위반하는 전형적인 예

    • 클라이언트가 부모 클래스인 Vector에 대해 기대하는 행동을 Stack에 대해서는 기대할 수 없기 때문에 행동 호환성을 만족시키지 않기 때문이다.
    • ☆ Vector로 짜둔 코드는 전부 new Stack()으로 바꿔도 동작은 한다. 코드적으로만 보면 부모를 대신할 수 있으나, 클라이언트 입장에서 Vector에게 기대하는 행동을 Stack에게 기대할 수 없기 때문에 LSP 위반이다.

     

    정사각형은 직사각형이다.

    • 말 자체만 보면 맞지만, 코드적으로 본다면 리스코프 치환 원칙을 위반하는 고전적인 사례 중 하나다. resize 메서드의 관점에서 Rectangle 대신 Square를 사용할 수 없기 때문에 Square는 Rectangle이 아니다. (Square는 Rectangle의 구현을 재사용하고 있을 뿐이다.). 즉, 클라이언트 관점에서 Square와 Rectangle이 다르기 때문에 대체할 수 없다.
    public class Rectangle {
        private int x, y, width, height;
    
        public Rectangle(int x, int y, int width, int height) {
            this.x = x;
            this.y = y;
            this.width = width;
            this.height = height;
        }
    
        public int getX() {
            return x;
        }
    
        public void setX(int x) {
            this.x = x;
        }
    
        public int getY() {
            return y;
        }
    
        public void setY(int y) {
            this.y = y;
        }
    
        public int getWidth() {
            return width;
        }
    
        public void setWidth(int width) {
            this.width = width;
        }
    
        public int getHeight() {
            return height;
        }
    
        public void setHeight(int height) {
            this.height = height;
        }
    }
    
    class Square extends Rectangle {
    
        public Square(int x, int y, int size) {
            super(x, y, size, size);
        }
    
        @Override
        public void setWidth(int width) {
            super.setWidth(width);
            super.setHeight(width);
        }
    
        @Override
        public void setHeight(int height) {
            super.setHeight(height);
            super.setWidth(height);
        }
    }
    
    class RectangleUser {
        private static void resize(Rectangle rectangle, int width, int height) {
            rectangle.setWidth(width);
            rectangle.setHeight(height);
            assert rectangle.getWidth() == width && rectangle.getHeight() == height;
        }
    
        public static void main(String[] args) {
            Square square = new Square(10, 10, 10);
            resize(square, 50, 100);
        }
    }

     

    리스코프 치환 원칙의 결론 : "클라이언트와 격리한 채로 본 모델은 의미 있게 검증하는 것이 불가능하다."

     

    이 책에서 설명하던 영화 관련 예시

    • 의존성 역전 원칙(DIP) : 구체 클래스인 Movie와 OverlappedDiscountPolicy 모두 추상 클래스인 DiscountPolicy에 의존한다. 상위 수준의 모듈인 Movie와 하위 수준의 모듈인 OverlappedDiscountPolicy는 모두 추상 클래스인 DiscountPolicy에 의존한다. 따라서 DIP를 만족한다.
    • 리스코프 치환 원칙(LSP) : Movie의 관점에서 DiscountPolicy 대신 OverlappedDiscountPolicy와 협력하더라도 아무런 문제가 없다. 다시 말해서 OverlappedDiscountPolicy는 클라이언트에 대한 영향 없이도 DiscountPolicy를 대체할 수 있다. 따라서 LSP를 만족한다.
    • 개방-폐쇄 원칙(OCP) : 중복 할인 정책이라는 새로운 기능을 추가하기 위해 OverlappedDiscountPolicy를 추가하더라도 Movie에는 영향을 끼치지 않는다. 다시 말해서 기능 확장을 하면서 기존 코드를 수정할 필요는 없다. 따라서 OCP를 만족한다.

     

    LSP를 준수해야만 서브타이핑 관계라고 말할 수 있다.

     

    댓글