ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 5.2 메서드 오버라이딩 - 메서드 오버라이딩과 결과 타입
    프로그래밍 언어 속 타입 2022. 5. 17. 15:16

     

    이 글은 인사이트 출판사의 제안으로 작성 중인 책 『프로그래밍 언어 속 타입』 원고의 일부입니다.

     


     

    지금까지는 매개변수 타입에만 집중해 왔다. 그리고 실제로 오버로딩을 할 때는 결과 타입에 내가 원하는 아무 타입이나 사용할 수 있다. 결과 타입이 무엇이든 매개변수 타입만 서로 다르면 타입 검사기가 문제 삼지 않는다. 예를 들면, 다음과 같이 두 write 함수의 결과 타입이 달라도 괜찮다.

    String write(Cell cell, String str) { ... }
    void write(Cell cell, Int num) { ... }

    다음과 같은 코드 역시 가능하다.

    Float length(Vector v) { ... }
    String length(SparseVector v) { ... }

    메서드의 경우에도 마찬가지다.

    class Cell {
        ...
        String write(String str) { ... }
        void write(Int num) { ... }
    }
    
    class Vector {
        ...
        Int add(Vector that) { ... }
        Vector add(SparseVector that) { ... }
    }

    두 클래스 모두 타입 검사를 통과한다. 물론 add의 결과 타입이 Int인 것은 논리적으로 이상하긴 하지만, 아무튼 타입 검사를 통과하는 데 지장을 주지는 않는다.

    반면, 메서드 오버라이딩을 할 때는 결과 타입을 아무렇게나 해서는 안 된다. 자식 클래스에 정의한 메서드의 결과 타입이 부모 클래스에 원래 있는 메서드의 결과 타입의 서브타입이어야 한다. 모든 타입은 자기 자신의 서브타입이니 이 조건은 두 메서드의 결과 타입이 같은 경우도 포함한다. 이 조건을 만족하지 않으면 타입 검사를 통과하지 못한다. 앞서 메서드 오버라이딩을 처음 다룰 때 본 length의 경우 두 메서드의 결과 타입이 Int로 일치하기 때문에 타입 검사를 문제없이 통과한다. 그 다음에 본 add 역시 결과 타입이 Vector로 일치하기에 문제 될 게 없다. 더 나아가, 다음처럼 코드를 고쳐도 SparseVector가 Vector의 서브타입이므로 여전히 타입 검사를 통과한다.

    class Vector {
        ...
        Vector add(SparseVector v) { ... }
    }
    class SparseVector extends Vector {
        ...
        SparseVector add(SparseVector v) { ... }
    }

    이 코드는 벡터와 희소 벡터를 더하면 벡터가 나오고, 희소 벡터끼리 더하면 희소 벡터가 나온다는, 말이 되어 보이는 사실을 표현한다. 반면, 다음 코드는 Vector가 SparseVector의 서브타입이 아니므로 타입 검사를 통과하지 못한다.

    class Vector {
        ...
        SparseVector add(SparseVector v) { ... }
    }
    class SparseVector extends Vector {
        ...
        Vector add(SparseVector v) { ... }
    }

    물론 이런 코드를 작성할 일은 없다. 벡터와 희소 벡터를 더했을 때 항상 희소 벡터만 나올 리가 없기 때문이다. 이처럼 자식 클래스에 있는 메서드의 결과 타입이 부모 클래스에 있는 메서드의 결과 타입의 서브타입이어야 한다는 조건을 어기는 코드는 대개 논리에 반하는 코드다. 그렇기에 이 조건이 유용한 프로그램을 작성하는 데 걸림돌이 되는 경우는 드물다.

    하지만 이 조건이 내가 원하는 구현을 방해할 때도 있다. 불변인 제네릭 타입을 사용하는 경우가 대표적이다. 예를 들어, Person 클래스에 동료의 리스트를 반환하는 colleagues 메서드를 정의했다고 하자.

    class Person {
        ...
        List<Person> colleagues() { ... }
    }

    학생의 동료는 언제나 학생이기에 Person을 상속하는 Student 클래스를 정의할 때 colleagues를 다음 코드처럼 오버라이딩하고 싶을 수 있다.

    class Student extends Person {
        ...
        List<Student> colleagues() { ... }
    }

    만약 List가 공변이라면 괜찮겠지만, List가 불변인 경우에는 List<Student>가 List<Person>의 서브타입이 아니기에 위 코드가 타입 검사를 통과하지 못한다. 그러니 아쉬워도 colleagues의 결과 타입을 List<Person>으로 맞춰 주는 수밖에 없다.

    이런 다소 불편한 제약이 존재하는 이유는 동적 선택을 사용하면서도 타입 안전성을 지키기 위함이다. 이유를 이해하기 위해 다시 벡터와 희소 벡터의 예시를 보자.

    class Vector {
        ...
        SparseVector add(SparseVector v) { ... }
    }
    class SparseVector extends Vector {
        ...
        Vector add(SparseVector v) { ... }
    }

    위 클래스 정의를 허용하면 다음 코드가 타입 검사를 통과함에도 실행 중에 오류를 일으킨다.

    Vector v1 = SparseVector(...);
    SparseVector v2 = SparseVector(...);
    v1.add(v2).nonzeros ...

    타입 검사기는 v1의 타입이 Vector라는 정보만 가지고 있다. 따라서 v1.add(v2)를 검사할 때 Vector 클래스에 있는 add의 정의를 참고한다. add의 결과 타입이 SparseVector라고 적혀 있으니, 이를 믿고 v1.add(v2)의 타입이 SparseVector라고 판단한다. 그러니 v1.add(v2)가 반환한 객체의 nonzeros 필드를 읽는 것을 막을 이유가 없다. 하지만 실행 중에는 v1이 SparseVector 객체이므로 실제로 호출되는 메서드는 SparseVector 클래스에 정의된 add이다. 그리고 이 add는 Vector 객체를 반환한다. Vector 객체에는 nonzeros 필드가 없으니 nonzeros 필드를 읽으려 하면 오류가 발생할 것이다.

     

     

    이처럼 동적 선택으로 인해 실행 전에 타입 검사기가 참고하는 메서드와 실제 실행 중에 호출되는 메서드가 다를 수 있다. 타입 검사기는 정적 타입밖에 모르니 수신자의 정적 타입을 바탕으로 참고할 메서드를 정하는 데 반해, 실행 중에는 수신자의 동적 타입이 호출되는 메서드를 결정하기 때문이다. 이로 인한 문제를 막으려면, 타입 검사기가 참고한 메서드와 다른 메서드가 호출되더라도 참고한 메서드의 결과 타입이 지켜지도록 해야 한다. 따라서 자식 클래스에 있는 메서드의 결과 타입이 부모 클래스에 있는 메서드의 결과 타입의 서브타입이어야 한다는 조건이 꼭 필요하다.

     


    자바

    class Vector {
        Vector add(SparseVector v) { ... }
    }
    class SparseVector extends Vector {
        SparseVector add(SparseVector v) { ... }
    }

    C++

    class SparseVector;
    
    class Vector {
    public:
        virtual Vector *add(SparseVector *v) { ... }
    };
    class SparseVector : public Vector {
    public:
        virtual SparseVector *add(SparseVector *v) { ... }
    };

    C#

    class Vector {
        public virtual Vector add(SparseVector v) { ... }
    }
    class SparseVector : Vector {
        public override Vector add(SparseVector v) { ... }
    }

    오버라이딩할 때 결과 타입이 달라지는 것을 허용하지 않는다.

    타입스크립트

    class Vector {
        add(v: SparseVector): Vector { ... }
    }
    class SparseVector extends Vector {
        nonzeros: Array<number>;
        add(v: SparseVector): SparseVector { ... }
    }

    코틀린

    open class Vector {
        open fun add(v: SparseVector): Vector = ...
    }
    class SparseVector : Vector() {
        override fun add(v: SparseVector): SparseVector = ...
    }

    스칼라

    class Vector:
      def add(v: SparseVector): Vector = ...
    class SparseVector extends Vector:
      override def add(v: SparseVector): SparseVector = ...

    오캐멀

    class vector = object
      method add (v: sparse_vector): vector = ...
    end
    and sparse_vector = object
      inherit vector
      method nonzeros: int list = ...
      method add (v: sparse_vector): vector = ...
    end

    오버라이딩할 때 결과 타입이 달라지는 것을 허용하지 않는다.

Designed by Tistory.