ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 오브젝트 6 - 다형성, 서브타이핑, 일관된 협력
    책책책 책을 읽읍시다/프로그래밍 2023. 2. 14. 23:29

    다형성


    다형성의 의미와 종류

    다형성(polymorphism)이라는 단어는 그리스어에서 '많은'을 의미하는 'poly'와 '형태'를 의미하는 'morph'의 합성어로 '많은 형태를 가질 수 있는 능력'을 의미한다. 컴퓨터 과학에서는 다형성을 하나의 추상 인터페이스에 대해 코드를 작성하고 이 추상 인터페이스에 대해 서로 다른 구현을 연결할 수 있는 능력으로 정의한다[Czanecki00]. 간단하게 말해서 다형성은 여러 타입을 대상으로 동작할 수 있는 코드를 작성할 수 있는 방법이라고 할 수 있다.

    객체지향 프로그래밍에서 사용되는 다형성은 아래와 같이 4가지로 분류할 수 있다.

    • 유니버설(Universal
      • 매개변수(Parametric) : 제네릭 프로그래밍과 관련이 높은데 클래스의 인스턴스 변수나 메서드의 메개변수 타입을 임의의 타입으로 선언한 후 사용하는 시점에 구체적인 타입으로 지정하는 방식을 가리킨다. 자바의 List 인터페이스는 컬렉션에 보관할 요소의 타입을 임의의 타입 T로 지정하고 있으며 실제 인스턴스를 생성하는 시점에 T를 구체적인 타입으로 지정할 수 있게 하고 있다. 따라서 List 인터페이스는 다양한 타입의 요소를 다루기 위해 동일한 오퍼레이션을 사용할 수 있다.
      • 포함(Inclusion) : 메세지가 동일하더라도 수신한 객체의 타입에 따라 실제로 수행되는 행동이 달라지는 능력을 의미한다. 포함 다형성은 서브타입(Subtype) 다형성이라고도 부른다. 포함 다형성은 객체지향 프로그래밍에서 가장 널리 알려진 형태의 다형성이기 떄문에 특별한 언급 없이 다형성이라고 할 때는 포함 다형성을 의미하는 것이 일반적이다.
    • 임시(Ad hoc)
      • 오버로딩(Overloading) : 일반적으로 하나의 클래스 안에 동일한 이름의 메서드가 존재하는 경우
      • 강제(Coercion) : 언어가 지원하는 자동적인 타입 변환이나 사용자가 직접 구현한 타입 변환을 이용해 동일한 연산자를 다양한 타입에 사용할 수 있는 방식을 가리킨다. 자바의 이항 연산자인 '+'가 정수에 대해서는 덧셈으로 동작하지만 문자열이 하나라도 끼게 되면 연결 연산자로 동작하는 것이 대표적인 예이다.

    self vs. super

    super 참조의 용도는 부모 클래스에 정의된 메서드를 실행하기 위한 것이 아니다. super 참조의 정확한 의도는 '지금 이 클래스의 부모 클래스에서부터 메서드 탐색을 시작하세요'다. 만약 부모 클래스에서 원하는 메서드를 찾지 못한다면 더 상위의 부모 클래스로 이동하면서 메서드가 존재하는지 검사한다.

    이것은 super 참조를 통해 실행하고자 하는 메서드가 반드시 부모 클래스에 위치하지 않아도 되는 유연성을 제공한다. 그 메서드가 조상 클래스 어딘가에 있기만 하면 성공적으로 탐색될 것이기 떄문이다.

    부모 클래스의 메서드를 호출하는 것과 부모 클래스에서 메서드 탐색을 시작하는 것은 의미가 매우 다르다. 부모 클래스의 메서드를 호출한다는 것은 그 메서드가 반드시 부모 클래스 안에 정의돼 있어야 한다는 것을 의미한다. 그에 비해 부모 클래스에서 메서드 탐색을 시작한다는 것은 그 클래스의 조상 어딘가에 그 메서드가 정의돼 있기만 하면 실행할 수 있다는 것을 의미한다.

    이처럼 super 참조를 통해 메세지를 전송하는 것은 마치 부모 클래스의 인스턴스에게 메세지를 전송하는 것처럼 보이기 때문에 이를 super 전송(super send)이라고 부른다.

    self 참조는 메세지를 수신한 객체가 무엇이냐에 따라 메서드 탐색을 위한 문맥이 동적으로 바뀐다. 메세지를 받은 인스턴스로부터 탐색을 시작해서 상속 계층 최상위에 있는 Object 클래스까지 훑어본다.

     

    서브클래싱과 서브타이핑


    사람들은 상속을 사용하는 두 가지 목적에 특별한 이름을 붙였는데 서브틀래싱과 서브타이핑이 그것이다.

    • 서브클래싱(subclassing): 다른 클래스의 코드를 재사용할 목적으로 상속을 사용하는 경우를 가리킨다. 자식 클래스와 부모 클래스의 행동이 호환되지 않기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 없다. 서브클래싱을 구현 상속(implementation inheritance) 또는 클래스 상속(class inheritance)이라고 부리기도 한다.
    • 서브타이핑(subtyping): 타입 계층을 구성하기 위해 상속을 사용하는 경우를 가리킨다. 영화 예매 시스템에서 구현한 DiscountPolicy 상속 계층이 서브타이핑에 해당한다. 서브타이핑에서는 자식 클래스와 부모 클래스의 행동이 호환되기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 있다. 이때 부모 클래스는 자식 클래스의 슈퍼타입이 되고 자식 클래스는 부모 클래스의 서브타입이 된다. 서브타이핑을 인터페이스 상속(interface inheritance)이라고 부르기도 한다.

    상속을 사용하는 목적에 따라 위 두가지로 나뉜다. 자식 클래스가 부모 클래스의 코드를 재사용할 목적으로 상속을 사용했다면 서브클래싱이다. 부모 클래스의 인스턴스 대신 자식 클래스의 인스턴스를 사용할 목적으로 상속을 했다면 서브타이핑이다. 나쁜 설계의 대부분은 서브클래싱에서 나온다. 

    서브타이핑 관계가 유지되기 위해서는 서브타입이 슈퍼타입이 하는 모든 행동을 동일하게 할 수 있어야 한다. 즉, 어떤 타입이 다른 타입의 서브타입이 되기 위해서는 행동 호환성(behavioral substitution)[Riel96, Jacobson92, Taivalsaari96]을 만족시켜야 한다. 행동 호환성을 만족하는 상속 관계는 부모 클래스를 새로운 자식 클래스로 대체하더라도 시스템이 문제없이 동작할 것이라는 것을 보장해야 한다. 다시 말해서 자식 클래스와 부모 클래스 사이의 행동 호환성은 부모 클래스에 대한 자식 클래스의 대체 가능성(substitutability)을 포함한다.

    리스코프 치환 원칙

    리스코프가 정의한 올바른 상속관계의 특징은 아래와 같다.

    여기서 요구되는 것은 다음의 치환 속성과 같은 것이다. S형의 각 객체 o1에 대해 T형의 객체 o2가 하나 있고, T에 의해 정의된 모든 프로그램 P에서 T가 S로 치환될 때, P의 동작이 변하지 않으면 S는 T의 서브타입이다

    한마디로 정리하면 "서브타입은 그것의 기반 타입에 대해 대체 가능해야 한다[Martin 2002a]"는 것으로 클라이언트가 "차이점을 인식하지 못한 채 기반 클래스의 인터페이스를 통해 서브클래스를 사용할 수 있어야 한다[Hunt99]"는 것이다. 리스코프 치환 원칙에 따르면 자식 클래스가 부모 클래스와 행동 호환성을 유지함으로써 부모 클래스를 대체할 수 있도록 구현된 상속 관계만을 서브타이핑이라고 불러야 한다.

    자바의 Stack과 Vector는 리스코프 치환 원칙을 위반하는 전형적인 예다. 클라이언트가 부모 클래스인 Vector에 대해 기대하는 행동을 Stack에 대해서는 기대할 수 없기 때문에 행동 호환성을 만족시키지 않기 때문이다.

     

    일관성 있는 협력


    대부분의 사람들은 유사한 요구사항을 구현하는 코드는 유사한 방식으로 구현될 것이라고 예상한다. 하지만 유사한 요구사항이 서로 다른 방식으로 구현돼 있다면 요구사항이 유사하다는 사실 자체도 의심하게 될 것이다. 이 코드가 정말 유사한 요구사항을 구현한 것이라면 왜 이렇게 다른 방식으로 구현한 것일까? 유사한 요구사항을 구현하는 서로 다른 구조의 코드는 코드를 이해하는 데 심리적인 장벽을 만든다.

    결론은 유사한 기능을 서로 다른 방식으로 구현해서는 안 된다는 것이다. 일관성 없는 설계와 마주한 개발자는 여러 가지 해결 방법 중에서 현재의 요구사항을 해결하기에 가장 적절한 방법을 찾아야 하는 부담을 안게 된다.

    유사한 기능은 유사한 방식으로 구현해야 한다. 객체지향에서 기능을 구현하는 유일한 방법은 객체 사이의 협력을 만드는 것뿐이므로 유지보수 가능한 시스템을 구축하는 첫걸음은 협력을 일관성 있게 만드는 것이다.

    유사한 기능에 대해 유사한 협력 패턴을 적용하는 것은 객체지향 시스템에서 개념적 무결성(Conceptual Integrity)[Brooks95]을 유지할 수 있는 가장 효과적인 방법이다. 개념적 무렬성을 일관성과 동일한 뜻으로 간주해도 무방하다. 시스템이 일관성 있는 몇 개의 협력 패턴으로 구성된다면 시스템을 이해하고, 수정하고, 확장하는 데 필요한 노력과 시간을 아낄 수 있다. 따라서 협력을 설계하고 있다면 항상 기존의 협력 패턴을 따를 수는 없는지 고민하라. 그것이 시스템의 개념적 무결성을 지키는 최선의 방법일 것이다.

    저자는 개념적 무결성(Conceptual Integrity)이 시스템 설계에서 가장 중요하다고 감히 주장한다. 좋은 기능들이긴 하지만 서로 독립적이고 조화되지 못한 아이디어들을 담고 있는 시스템보다는 여러 가지 다양한 기능이나 갱신된 내용은 비록 빠졌더라도 하나로 통합된 일련의 설계 아이디어를 반영하는 시스템이 훨씬 좋다[Brooks95].

    댓글

Designed by Tistory.