추상화 기법

  • 도메인에 존재하는 개념들을 구조화하고 단순화하기 위해 다양한 추상화 기법을 사용할 수 있음
  • 주요 추상화 기법의 종류들: 분류와 인스턴스화, 일반화와 특수화, 집합과 분해
  • 객체지향의 가장 큰 장점은 동일한 추상화 기법을 프로그램의 분석, 설계, 구현 단계에 걸쳐 일관성 있게 적용할 수 있다는 점

분류와 인스턴스화

  1. 개념과 범주

    • 도로 위를 달리는 작은 승용차와 버스, 트럭들을 가리켜 ‘자동차’라고 하며, 길거리에 자라고 있는 다양한 종류의 가로수들을 일컬어 ‘나무’라고 할 수 있음
    • 개별 자동차와 나무는 완전히 동일하지 않지만 유사한 특성을 바탕으로 각각 ‘자동차’와 ‘나무’로 분류할 수 있음.
    • 이처럼 객체를 분류하고 범주로 묶는 것은 객체들의 특정 집합에 공통의 개념을 적용하는 것을 의미함
    • 자동차: 바퀴를 이용해 사람들을 한 장소에서 다른 장소로 운반하는 운송수단 (이라는 특징)
    • 나무: 푸른 잎과 갈색의 줄기를 가진 다년생 식물 (이라는 특징)
    • 세상에 존재하는 객체에 개념을 적용하는 과정을 분류라고 함.
    • 분류는 객체를 특정한 개념을 나타내는 집합의 구성 요소로 포함시킴. 이때 ‘수많은 개별적인 현상들’을 객체라고 하고, ‘하나의 개념’을 타입이라고 함.
    • 분류는 객체를 타입과 연관시키는 것
    • 객체지향의 세계에서 타입은 개념과 동의어이며 속성과 행위가 유사한 객체에 공통적으로 적용되는 관념이나 아이디어를 의미. 객체는 자동으로 타입의 인스턴스라고 함
  2. 타입

    • 객체를 타입에 따라 분류하기 위해서는 객체가 타입에 속하는지 여부를 확인할 수 있어야 함. 타입은 세 가지 관점에서의 정의가 필요.
    • 심볼: 타입을 가리키는 간략한 이름이나 명칭
    • 내연: 타입의 완전한 정의
    • 외연: 타입에 속하는 모든 객체들의 집합
  3. ex) 자동차

    • 심볼: 자동차
    • 내연: 원동기를 동력원으로 해서 주행하는 사람이나 화물을 운반하는 기계
    • 외연: 모든 자동차들의 집합 → 도메인을 분석하는 동안 이름(심볼)과 의미(내연), 객체들의 집합(외연)을 이용해 개념을 정의할 수 있음
  4. 외연과 집합

    • 타입의 외연은 타입에 속하는 객체들의 집합
    • 단일 분류: 한 객체가 한 시점에 하나의 타입에만 속하는 것
    • 다중 분류: 한 객체가 한 시점에 여러 타입에 속하는 것
    • 대부분의 객체지향 프로그래밍 언어들은 단일 분류만을 지원. 한 객체는 오로지 한 클래스의 인스턴스여야만 함
    • 동적 분류: 객체가 한 집합에서 다른 집합의 원소로 자신이 속하는 타입을 변경할 수 있는 경우
    • 정적 분류: 객체가 자신의 타입을 변경할 수 없는 경우
    • 대부분의 언어에서는 동적 분류를 구현할 수 있는 방법을 제공하지 않음.
    • 다중 분류와 동적 분류 관점에서 도메인 모델의 초안을 만든 후 실제 구현에 적합하도록 단일 분류와 정적 분류 방식으로 객체들의 범주를 재조정하는 편이 분석과 구현 간의 차이를 메울 수 있는 가장 현실적인 방법
  5. 클래스

    • 객체지향 프로그래밍 언어를 이용해 타입을 구현하는 가장 보편적인 방법은 클래스를 이용하는 것
    • 아리스토텔레스 철학에서 비롯된 현재의 객체지향 패러다임에서는 만약 객체들이 동일한 특성을 가진다면 그것들은 동일한 카테고리에 속함.
    • 객체들의 카테고리는 객체들이 공유하는 공통적인 특성에 의해 정의됨
    • 객체의 특성을 사물의 가장 핵심적이고 필수불가결한 ‘본질’과 그렇지 않은 ‘우연’적인 속성으로 나눌 수도 있음.
    • 객체지향 언어에서 클래스는 객체가 공유하는 본질적인 속성을 정의함. 동일한 범주에 속하는 객체는 모두 동일한 속성을 가져야만 한함
    • 자바스크립트와 같은 프로토타입 기반의 언어에서 분류와 인스턴스화는 프로토타입이라는 객체의 복사를 통해 이뤄짐

일반화와 특수화

  1. 범주의 계층

    • 중세 유럽의 카를로스 린네의 생물 분류법은 ‘계’라는 최상위 단계가 있는 중첩된 계층 구조였음
    • 계-문-강-목-과-속-종의 분류에서 고양이종 범주는 포유류강 범주의 하위 범주에 속함. 얼룩고양이를 실제로 알지 못하더라도 얼룩고양이에게 (포유류처럼) 척추가 있고 새끼를 낳아 기르며 젖을 먹여 새끼를 기를 것이라는 사실을 쉽게 추론할 수 있음
    • 린네의 분류 체계는 범주 간의 계층적인 구조를 가짐.
    • 린네의 계층 구조에서 계층의 상위에 위치한 범주를 계층의 하위 범주의 일반화 라고 하고, 계층의 하위에 위치한 범주는 계층의 상위 범주의 특수화 라고 함
  2. 서브타입

    • 어떤 타입이 다른 타입보다 일반적이라면 이 타입을 슈퍼타입(supertype) 이라고 하며, 어떤 타입이 다른 타입보다 좀 더 특수하다면 이 타입을 서브타입(subtype) 이라고 험. 슈퍼타입은 서브타입의 일반화이고 서브타입은 슈퍼타입의 특수화
    • 슈퍼타입은 서브타입의 일반화이고 서브타입은 슈퍼타입의 특수화
    • 아리스토텔레스의 분류법에서는 기존의 범주가 가진 속성을 새로운 범주가 포함할 경우 새로운 범주는 기존의 범주를 확장해 정의할 수 있음.
    • 이것은 객체지향의 세계에서도 동일하게 적용됨. 서브타입은 슈퍼타입이 가진 본질적인 속성과 함께 자신만의 추가적인 속성을 가짐
    • 서브타입은 슈퍼타입의 본질적인 속성을 모두 포함하기 때문에 계층에 속하는 모든 서브타입들은 슈퍼타입의 속성을 공유함.
    • 외연의 관점에서 서브타입은 슈퍼타입의 부분집합으로 표현됨. (종 < 속 < 과 < 목 < 강 < 문 < 계)
    • (린네의 분류체계에서는 ‘계’로 갈수록 슈퍼타입, ‘종’으로 갈수록 서브타입)
    • 모든 고양이는 육식동물의 집합에 포함되며, 모든 육식동물은 포유류의 집합에 포함됨. 따라서 고양이의 집합은 육식동물의 부분집합이며, 육식동물의 집합은 포유류의 부분집합
    • 어떤 타입이 다른 타입의 서브타입이 되기 위해서는 ‘100% 규칙’ 과 ‘is-a 규칙’ 을 준수해야 함
    • 100% 규칙: 슈퍼타입의 정의가 100% 서브타입에 적용돼야만 함
    • is-a 규칙: 서브타입의 모든 인스턴스는 슈퍼타입 집합에 포함돼야 함
    • 흔히 일반화 관계를 is-a 관계라고 함
    • “고양이는 육식동물이다 (O)”와 “육식동물은 고양이다 (X)” → 두 가지 범주 간의 일반화 관계를 표현함
    • 모든 고양이 인스턴스들은 육식동물에 해당함
  3. 상속

    • 일반화와 특수화 관계를 구현하는 가장 일반적인 방법은 클래스 간 상속을 사용하는 것이지만, 모든 상속 관계가 일반화 관계인 것은 아님.
    • 일반화의 원칙은 한 타입이 다른 타입의 서브타입이 되기 위해서는 슈퍼타입에 순응(conformance) 해야 한다는 것.
    • 이는 특정 기대 집합에 대해 서브타입의 슈퍼타입에 대한 대체 가능성을 의미함
    • 구조적인 순응

      • 서브타입은 슈퍼타입이 가지고 있는 속성과 연관관계 면에서 100% 일치해야 한다는 것
      • Person이 name이라는 속성을 가진다면 Person의 서브타입인 Employee 역시 name이라는 속성을 가질 것이라고 기대할 수 있음. Employee는 Person을 대체할 수 있음
    • 행위적인 순응

      • 서브타입은 슈퍼타입을 행위적으로 대체 가능해야 함. 이를 흔히 리스코프 치환 원칙이라고 함
      • Person이 getAge()라는 메시지에 대한 응답으로 나이를 반환한다면 서브타입인 Employee 역시 getAge()라는 메시지에 대한 응답으로 나이를 반환해야 함.
    • 상속은 서브타이핑(subtyping) 과 서브클래싱(subclassing) 의 두 가지 용도로 사용될 수 있음

      • 서브타이핑: 서브클래스가 슈퍼클래스를 대체할 수 있는 경우 (타입을 상속)
      • 서브클래싱: 서브클래스가 슈퍼클래스를 대체할 수 없고 단지 코드만 공유하고 있는 경우 (클래스를 상속)
      // 서브타이핑
      interface MyClass {
        String Name();
      }
      
      class A: MyClass {
        public String Name()  {
          return "Class name is A";
        }
      }
      
      class B: MyClass {
        public String Name() {
          return "Class name is B";
        }
      }
      
      public static void PrintName(MyClass obj) {
        Console.WriteLine(obj.Name());
      }
      
      static void Main(string[] args) {
        MyClass objA = new A();
        MyClass objB = new B();
        PrintName(objA);  // Class name is A
        PrintName(objB);  // Class name is B
      }
      // 서브클래싱
      class A  {
        public String Name()  {
          return "Class name is A";
        }
      }
      
      class B: A {
        public String Name() {
          return "Class name is B";
        }
      }
      
      public static void PrintName(A obj) {
        Console.WriteLine(obj.Name());
      }
      
      static void Main(string[] args) {
        A objA = new A();
        A objB = new B();
        PrintName(objA);   // Class name is A
        PrintName(objB);   // Class name is A
      }
      
      // 클래스 상속은 부모 클래스의 코드를 재사용하기 때문에 위와 같이 슈퍼클래스의 객체를 하위클래스의 객체로 대체하더라도 하위 클래스의 행동으로 대체되어 동작하지 않는다
      // 메소드 재정의(method overriding)를 통해 부모 클래스의 메소드를 대체해야 한다.
      
      class B: A {
        public override String Name() {
          return "Class name is B";
        }
      }
    • 객체지향 상속의 종류: 서브클래싱과 서브타이핑 (Subclassing & Subtyping)
    • 흔히 서브타이핑을 인터페이스 상속이라고 하고, 서브클래싱을 구현 상속이라고 함.
    • 여러 클래스로 구성된 상속 계층에서 수신된 메시지를 이해하는 기본적인 방법은 클래스 간의 위임(delegation) 을 사용하는 것
    • 어떤 객체의 클래스가 수신된 메시지를 이해할 수 없다면 메시지를 클래스의 부모 클래스로, 그리고 또 부모 클래스로 위임함
    • 프로토타입 기반 언어에서 상속은 객체와 객체 간의 관계로 이뤄지며, 메시지 역시 위임 메커니즘에 의해 처리됨.
    • 자식 객체와 부모 객체 사이 상속과 위임을 통해 관계를 연결함

집합과 분해

  1. 계층적인 복잡성

    • 복잡성은 ‘계층’의 형태를 띰
    • 단순한 형태로부터 복잡한 형태로 진화하는 데 걸리는 시간은 그 사이에 존재하는 ‘안정적인 형태’의 수와 분포에 의존함
    • 시계는 수많은 부품이 유기적으로 결합되어 부품들 간의 계층적인 형태를 취함. 시계를 짧은 시간 안에 효율적으로 조립하는 방법은 작은 부품으로 구성된 안정적인 형태의 중간 부품을 이용해서 조립하는 것. 시계는 전체적인 연쇄 계층 구조로 구성됨
    • 집합: 안정적인 형태의 부분으로부터 전체를 구축하는 행위
    • 분해: 전체를 부분으로 분할하는 행위
    • 집합은 전체의 내부로 불필요한 세부사항을 감춰주는 추상화 메커니즘이자 캡슐화 메커니즘
    • 집합과 분해는 한 번에 다뤄야 하는 요소의 수를 감소시킴으로써 인지 과부하를 방지함. 인간은 은유를 통해 쉽게 추상적인 경계를 찾으며, 인위적인 집합을 창조할 수 있음
  2. 합성 관계

    • 객체와 객체 사이의 전체-부분 관계를 구현하기 위해서는 합성 관계를 사용함
    • 주문 항목은 반드시 어떤 한 주문의 일부로 생성되는 전체-부분 관계
    • 합성 관계는 부분을 전체 안에 캡슐화함으로써 인지 과부하를 방지함. 이 모델을 다루는 사람은 주문 항목과 관련된 세부 사항을 무시하고 주문과 상품이 존재하는 것처럼 모델을 다룰 수 있으며, 필요하다면 주문 내부로 들어가 주문 항목과 관련된 세부 사항을 확인할 수도 있음
    • 상품은 주문 항목의 일부가 아님
    • 주문 항목과 상품 간에는 단순한 물리적 통로만 존재함. 이를 연관 관계라고 함
    • 합성 관계는 연관 관계보다 더 강하게 객체들을 결합함
    • 일반적으로 합성 관계로 연결된 객체는 포함하는 객체가 제거될 때 내부에 포함된 객체도 함께 제거됨
    • 주문이 제거되면 주문 항목도 함께 제거됨
    • 반면 연관 관계로 연결된 두 객체는 독립적으로 제거될 수 있음
    • 주문과 주문 항목이 제거되더라도 상품은 계속 판매됨
  3. 패키지

    • 소프트웨어의 구조를 단순화하기 위해서는 서로 관련성이 높은 클래스 집합을 논리적인 단위로 통합해야 함. 이렇게 묶는 구성 요소를 패키지(package) 또는 모듈(module) 이라고 함
    • 패키지를 이용하면 개별 클래스가 아닌 클래스의 집합을 캡슐화함으로써 전체적인 복잡도를 낮출 수 있으며, 함께 협력하는 응집도 높은 클래스 집합을 하나로 모아 패키지 경계 안에서 작업을 할 수 있음
    • 패키지는 내부의 클래스들을 추상화함

참고