ABOUT ME

-

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

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

     


     

    씨 없는 딸기 주스

    큐리 박사: 처르지, 이거 마셔 볼래?
    처르지: 와, 끝내주게 맛있네. 이게 뭐야?
    큐리 박사: 딸기 주스야. 내가 새로 만든 기계로 만들었지.
    처르지: 별걸 다 만드는구나!
    큐리 박사: 카레를 다 먹고 상큼한 과일 주스를 후식으로 먹으면 잘 어울릴 거 같더라고. 그래서 손님들한테 과일 주스를 제공할 수 있도록 기계를 만들었어.
    처르지: 좋은 생각이야.
    큐리 박사: 내 기계 구경할래?
    처르지: 좋지!

     

     

    큐리 박사: 내가 기계를 두 개 만들었거든. 얘가 그중 첫 번째로 만든 애야. 얘로는 딸기 주스만 만들 수 있어.
    처르지: 아하, 그러면 이 기계는 딸기 주스 만드는 기계라 할 수 있겠다,
    큐리 박사: 맞아. 이걸 만들고 나니까 좀 더 잘 만들 수 있겠다는 생각이 들더라고. 그래서 두 번째 기계를 만들었지. 두 번째 기계는 여기에 있어. 얘는 딸기 주스도 만들 수 있고 오렌지 주스도 만들 수 있어. 그리고 첫 번째 거보다 더 좋은 게, 이걸로 만든 딸기 주스는 씨 없는 딸기 주스야. 자동으로 딸기 씨를 다 분리해 버려서 식감이 정말 부드러운 딸기 주스를 만들어 내지.
    처르지: 훌륭해! 그럼 이 기계는 딸기 주스와 오렌지 주스를 만드는 기계라 할 수 있겠어. 딸기 주스 만드는 기계라 불러도 틀린 건 아니지만.
    큐리 박사: 너도 한번 내 기계로 주스를 만들어 볼래?
    처르지: 좋아. 해 볼래. (두 번째 기계에 다가가며) 이 딸기 주스 만드는 기계로 딸기 주스를 만들어 보겠어.

     

     

    처르지: 오, 주스가 나왔어. 어라, 씨 없는 딸기 주스가 나왔네?
    큐리 박사: 그야 당연하지. 네가 두 번째 기계를 사용했는걸?
    처르지: 그건 그렇지만, 나는 그 기계를 딸기 주스 만드는 기계라고만 불렀다고.
    큐리 박사: 그게 무슨 상관이야. 네가 뭐라 부르든 기계의 기능은 바뀌지 않아.
    처르지: 네 말이 맞아. 하지만 분명히 지난번에는 사과를 사과라 부르는 거랑 사과를 과일이라 부르는 게 달랐다고.
    큐리 박사: 그땐 그때고 지금은 지금이지.

     


    메서드 오버라이딩은 특화된 동작을 정의하는 가장 좋은 방법이다. 우선 메서드 오버라이딩을 사용하지 않은 코드를 본 뒤, 메서드 오버라이딩이 무엇이며 메서드 오버라이딩을 통해 뭘 바꿀 수 있는지 알아보자.

    다음 코드는 메서드 오버라이딩을 사용하지 않는다.

    class Vector {
        ...
        Int length() { ... }
    }
    class SparseVector extends Vector { ... }

    Vector 클래스에 length라는 이름의 메서드가 있다. 이 메서드는 앞서 본 length 함수와 동일하게 벡터의 길이를 구한다. 따라서 각 Vector 객체는 length 메서드를 가지고 있으며, 다음과 같은 코드를 통해 벡터의 길이를 계산할 수 있다.

    Vector v = Vector(...);
    v.length();

    또한, length는 SparseVector 클래스에 그대로 상속된다. 그러므로 각 SparseVector 객체 역시 동일한 length 메서드를 가지게 된다. 따라서 다음과 같은 코드를 통해 희소 벡터의 길이 역시 계산할 수 있다.

    SparseVector v = SparseVector(...);
    v.length();

    문제는 length의 동작이 Vector 클래스에서 정의한 그대로라는 것이다. 즉, 이 코드는 희소 벡터의 길이를 우리가 원하는 효율적인 방법으로 계산하지 않는다.

     

     

    이제 이 코드를 메서드 오버라이딩을 사용해 고칠 차례다. 메서드 오버라이딩은 클래스를 상속할 때 부모 클래스parent class에 정의되어 있는 메서드와 이름도 매개변수 타입도 같은 메서드를 자식 클래스child class에 새로 정의하는 것을 말한다.

    class Vector {
        ...
        Int length() { ... }
    }
    class SparseVector extends Vector {
        ...
        Int length() { ... }
    }

    SparseVector 클래스에 length 메서드를 추가했다. 이 length 메서드는 희소 벡터의 0이 아닌 원소만 고려하는, 희소 벡터에 특화된 메서드다. Vector 클래스에 이름과 매개변수 타입이 동일한 메서드가 존재하므로, 위 코드가 메서드 오버라이딩을 사용했다고 할 수 있다. 코드를 고친 후에도 여전히 각 Vector 객체는 Vector 클래스에 정의된 length 메서드를 가지고 있다. 또한, 각 SparseVector 객체 역시 length 메서드를 가진다. 하지만 SparseVector 객체의 length 메서드는 더 이상 Vector 클래스에 정의된 length가 아니다. 메서드 오버라이딩을 통해 새로운 length를 SparseVector에 정의했기 때문이다. 따라서 SparseVector 객체는 새롭게 정의한 두 번째 length를 가지게 된다.

     

     

    overriding이라는 단어의 사전적인 뜻은 “자동으로 진행되는 동작을 사람이 개입하여 중단시킨 뒤 스스로 조작하는 것”이다. 이를 바탕으로 메서드 오버라이딩을 이해하자면, “Vector에 있던 length라는 메서드가 상속을 통해 자동으로 SparseVector에도 정의되는 것을 개발자가 개입하여 막은 뒤 기존 length와는 다른 동작을 수행하는 새로운 length를 직접 정의하는 것”이라고 이해할 수 있다.

    이제 다양한 상황에서 length 메서드를 호출하면 무슨 일이 일어나는지 알아보자. 정적 타입과 동적 타입이 일치하는 경우부터 보겠다.

    Vector v = Vector(...);
    v.length();

    이 경우 v가 Vector 객체다. v가 가지고 있는 length 메서드는 Vector 클래스에 정의된 첫 번째 length 메서드이므로 첫 번째 length가 호출된다.

    SparseVector v = SparseVector(...);
    v.length();

    이 경우에는 v가 SparseVector 객체다. 이번에는 v가 SparseVector 클래스에 정의된 두 번 째 length 메서드를 가지고 있으니 두 번째 length가 호출된다.

    그렇다면 정적 타입과 동적 타입이 다른 경우에는 어떨까?

    Vector v = SparseVector(...);
    v.length();

    v가 SparseVector 객체이므로 바로 직전 예시와 마찬가지로 두 번째 length 메서드를 가지고 있다. 따라서 두 번째 length가 호출된다. 중요한 점은 v의 정적 타입이 SparseVector가 아니라 Vector라는 것이다. 즉, v.length()가 어떤 length를 호출하느냐에 영향을 주는 요소는 v의 정적 타입이 아니라 동적 타입이다. 드디어 우리가 원하던 게 이루어졌다. 이제 v.length()라고 썼을 때, v가 Vector 객체이면 첫 번째 length가 불리고, SparseVector 객체이면 두 번째 length가 불린다. v가 실제로 나타내는 값에 따라 가장 특화된 메서드가 저절로 선택되는 것이다. 이처럼 메서드 오버라이딩을 사용하면 서브타입을 위해 더 특화된 동작을 정의하고, 정적 타입에 상관없이 언제나 그 특화된 동작이 사용되도록 만들 수 있다.

    지금까지 본 내용으로부터 알 수 있는 또 다른 사실은, 메서드 오버라이딩을 사용한 경우에도 메서드 선택이 일어난다는 것이다. 메서드 오버로딩을 사용했을 때는 한 클래스에 정의된 같은 이름의 메서드들 중 하나를 선택하는 것이었다면, 이번에는 부모 클래스와 자식 클래스에 같은 이름의 메서드가 정의되어 있으니 그중 무엇을 호출할지 선택하는 것이다. 즉, 메서드 선택은 메서드 오버로딩뿐 아니라 메서드 오버라이딩까지도 모두 고려해 메서드를 선택한다.

    여기서 함수 선택과 메서드 선택의 차이가 드러난다. 함수 선택은 인자의 정적 타입만을 고려한다. 반면, 메서드 선택은 인자의 정적 타입을 고려하는 것까지는 동일하지만, 거기에 더해 수신자receiver의 동적 타입 역시 고려한다. 여기서 수신자란 메서드 호출 시에 메서드 이름 앞에 오는 객체를 뜻한다. 예를 들면 v.length()라는 코드에서 수신자는 v이다. v가 이 메서드 호출을 받아서 처리한다는 관점에서 v를 수신자라고 부른다. 그러므로 메서드 선택의 네 번째 규칙이 만들어진다. 바로 “메서드를 고를 때는 수신자의 동적 타입도 고려한다”는 것이다.* 앞서 함수 선택을 이야기할 때는, 인자의 정적 타입만 고려하여 실행 전에 함수를 미리 고른다는 특징 때문에 정적 선택이라는 용어를 사용했다. 반면 메서드 선택의 경우, 수신자의 동적 타입을 고려하여 실행 중에 메서드를 고르기 때문에 정적 선택이 아니라 동적 선택dynamic dispatch이라 불린다.

     

    * 정확히 말하면, 수신자의 동적 타입뿐 아니라 정적 타입도 영향을 미칠 수 있다. 하지만 그런 경우가 드물기 때문에 다루지 않는다.

     


    메서드 선택 규칙

    1. 인자의 타입에 맞는 메서드를 고른다.
    2. (인자의 타입에 맞는 메서드가 여럿이면) 인자의 타입에 가장 특화된 메서드를 고른다.
    3. 메서드를 고를 때는 인자의 정적 타입을 고려한다.
    4. 메서드를 고를 때는 수신자의 동적 타입도 고려한다.

     


    자바

    class Vector {
        int length() { ... }
    }
    class SparseVector extends Vector {
        int length() { ... }
    }
    
    Vector v = new SparseVector();
    v.length();

    C++

    class Vector {
    public:
        virtual int length() { ... }
    };
    class SparseVector : public Vector {
    public:
        virtual int length() { ... }
    };
    
    Vector *v = new SparseVector();
    v->length();

    메서드에 virtual 키워드를 붙여야 동적 선택이 이루어진다. virtual 키워드를 붙이지 않은 메서드를 호출할 때는 수신자조차도 정적 타입만 고려하는 정적 선택이 일어난다.

    C#

    class Vector {
        public virtual int length() { ... }
    }
    class SparseVector : Vector {
        public override int length() { ... }
    }
    
    Vector v = new SparseVector();
    v.length();

    메서드에 virtual 키워드를 붙여야 동적 선택이 이루어진다. virtual 키워드를 붙이지 않은 메서드를 호출할 때는 수신자조차도 정적 타입만 고려하는 정적 선택이 일어난다. 한편, 오버라이딩할 때는 메서드에 override 키워드를 붙여야 한다.

    타입스크립트

    class Vector {
        length(): number { ... }
    }
    class SparseVector extends Vector {
        nonzeros: Array<number>;
        length(): number { ... }
    }
    let v: Vector = new SparseVector();
    v.length();

    코틀린

    open class Vector {
        open fun length(): Int = ...
    }
    class SparseVector : Vector() {
        override fun length(): Int = ...
    }
    
    val v: Vector = SparseVector()
    v.length()

    자식 클래스에서 오버라이딩하는 것을 허용할 메서드에는 open 키워드를 붙여야 하며, 오버라이딩할 때는 메서드에 override 키워드를 붙여야 한다.

    스칼라

    class Vector:
      def length(): Int = ...
    class SparseVector extends Vector:
      override def length(): Int = ...
    
    val v: Vector = SparseVector()
    v.length()

    오버라이딩할 때는 메서드에 override 키워드를 붙여야 한다.

    오캐멀

    class vector = object
      method length: int = ...
    end
    class sparse_vector = object
      inherit vector
      method nonzeros: int list = ...
      method length: int = ...
    end
    
    let v: < length: int; .. > = new sparse_vector in
    v#length
Designed by Tistory.