-
5.2 메서드 오버라이딩 - 메서드 선택의 한계프로그래밍 언어 속 타입 2022. 5. 17. 15:09
이 글은 인사이트 출판사의 제안으로 작성 중인 책 『프로그래밍 언어 속 타입』 원고의 일부입니다.
안타깝게도 메서드 선택이 수신자의 동적 타입을 고려하는 것만으로는 모든 문제가 해결되지 않는다. 문제는 수신자의 동적 타입만 고려하고 인자의 동적 타입은 고려하지 않는 데서 온다. 지금부터 인자의 동적 타입을 고려하지 않기 때문에 발생하는 문제를 알아보자.
두 벡터가 같은 개수의 원소를 가진다면 두 벡터를 더할 수 있다. 두 벡터의 합은 벡터의 원소들을 각각 더해서 만든 벡터다. 예를 들어 [1, 2]와 [5, 3]을 더한 결과는 [1 + 5, 2 + 3], 즉 [6, 5]다. 두 벡터의 합을 반환하는 add 메서드를 다음과 같이 정의할 수 있다.class Vector { Vector add(Vector that) { this.entries[i] + that.entries[i] ... } }
v1.add(v2)는 벡터 v1과 v2의 합을 구하는 코드다.
벡터의 길이를 구할 때와 비슷하게, 희소 벡터의 경우 0이 아닌 원소만 고려함으로써 벡터의 합을 구하는 데 걸리는 시간을 단축시킬 수 있다. 따라서, 희소 벡터를 덧셈에 사용하는 경우를 효율적으로 처리하도록 다음과 같이 특화된 메서드들을 추가하는 것이 바람직하다.class Vector { ... Vector add(Vector that) { ... } Vector add(SparseVector that) { ... } } class SparseVector extends Vector { ... Vector add(Vector that) { ... } Vector add(SparseVector that) { ... } }
Vector 클래스에는 두 개의 add가 메서드 오버로딩을 통해 정의되어 있다. 하나는 벡터와 벡터의 덧셈을 담당하고, 다른 하나는 벡터와 희소 벡터의 덧셈을 담당한다. 한편, SparseVector 클래스는 두 add를 모두 오버라이딩함으로써 두 개의 add를 추가로 정의한다. 하나는 희소 벡터와 벡터의 덧셈을 담당하고, 다른 하나는 희소 벡터와 희소 벡터의 덧셈을 담당한다.
과연 이렇게 네 개의 add를 정의하면 언제나 가장 효율적인 메서드가 호출될까?
Vector v1 = SparseVector(...); Vector v2 = Vector(...); v1.add(v2);
이 코드는 희소 벡터와 벡터의 합을 구한다. v1의 정적 타입이 Vector이기는 하지만, 메서드 선택 시 수신자의 동적 타입이 고려되므로 SparseVector 클래스에 정의된 메서드가 호출된다. 우리가 원하는 메서드가 호출되는 것이다.
지금까지는 별 문제가 없다. 하지만 다음 예시를 보자.
Vector v1 = Vector(...); Vector v2 = SparseVector(...); v1.add(v2);
이 코드는 벡터와 희소 벡터의 합을 구한다. 문제는 v2의 정적 타입이 SparseVector가 아니라 Vector라는 점이다. 메서드 선택 시 인자의 정적 타입만이 고려되므로, Vector 클래스에 정의된 두 번째 add가 아니라 첫 번째 add가 호출된다. 다시 말해, 벡터와 벡터의 합을 구하는, 덜 효율적인 메서드가 호출되는 것이다. 이는 우리가 원하는 바가 아니다.
이 예시에서 볼 수 있듯이, 두 개의 값을 받아서 처리하는 덧셈 같은 기능을 구현할 때 문제점이 드러난다. 수신자의 동적 타입만 고려하고 인자의 동적 타입은 고려하지 않는다는 특징 때문에, 인자의 정적 타입과 동적 타입이 다를 때는 개발자의 기대와 다른 메서드를 호출하게 되는 것이다.
add 메서드를 올바르게 구현할 수 있을까? 가능은 하다. 다만 쉽지는 않다. 언어 수준에서 도와주지 않기 때문에 모든 상황에 가장 효율적인 메서드가 호출되도록 개발자가 코드를 “잘” 작성해야 한다. 기본 전략은 메서드 안에서 수신자와 인자의 위치를 바꾸어 다시 한번 메서드를 호출하는 것이다. 다음은 add를 올바르게 구현한 예시다.class Vector { ... Vector add(Vector that) { return that._add(this); } Vector _add(Vector that) { ... } Vector _add(SparseVector that) { ... } } class SparseVector extends Vector { ... Vector add(Vector that) { return that._add(this); } Vector _add(Vector that) { ... } Vector _add(SparseVector that) { ... } }
클래스 밖에서 덧셈을 위해 부르는 메서드는 기존과 동일하게 add이며, 실제로 클래스 내부에서 덧셈을 구현하는 메서드는 _add이다. add는 수신자와 인자의 순서를 바꾸어 _add를 호출한다. 이 코드가 어째서 올바르게 add를 구현한 것인지는 자세히 설명하지 않겠다. 궁금한 독자는 스스로 고민해 보기 바란다. 지금까지의 내용을 잘 이해했다면 충분히 혼자서도 이해할 수 있을 것이다.
'프로그래밍 언어 속 타입' 카테고리의 다른 글
5.3 타입클래스 (0) 2022.05.17 5.2 메서드 오버라이딩 - 메서드 오버라이딩과 결과 타입 (0) 2022.05.17 5.2 메서드 오버라이딩 (0) 2022.05.17 5.1 오버로딩 - 메서드 오버로딩 (0) 2022.05.17 5.1 오버로딩 - 가장 특화된 함수 (0) 2022.05.17