객체지향 원리 - 추상화, 캡슐화, 일반화(상속), 다형성


1. 추상화

추상화란 어떤 영역에서 필요로 하는 속성이나 행동을 추출하는 작업을 의미한다. 추상화로 우리는 우리가 집중하고 싶은 부분을 골라낼 수 있다.

추상화를 좀 더 길게 풀어 설명하면 다음과 같다.

추상화는 사물들의 공통된 특징, 즉 추상적 특징을 파악해 인식의 대상으로 삼는 행위다. 추상화가 가능한 개체들은 개체가 소유한 특성의 이름으로 하나의 집합(class)를 이룬다. 따라서 추상화한다는 것은 여러 개체들을 집합으로 파악한다는 것과 같다. 추상적 특성은 집합을 구성하는 개체들을 ‘일반화’하는 것이므로 집합의 요소들에 보편적인 것이다.

즉 여러 자동차들이 있을 때 승용차/승합차를 나누듯이 구체적인 사물들의 공통적인 특징을 파악해서 이를 하나의 개념으로 다루는 수단이 추상화다. 추상화는 객체지향 프로그래밍에서 매우 중요하다. 추상화가 없다면 각각의 개체를 구별해야 할 것이다. 핵심은 각 개체의 구체적인 개념에 의존하지 말고 추상적 개념에 의존해야 변경이 유연하게 이루어질 수 있다는 점이다.

2. 캡슐화

나도 정말 많이 느끼는 건데, 이미 개발해놓은 것에 대한 요구사항이 바뀌어서 다시 개발해야 할 때가 좋지 않다. 하지만 소프트웨어 개발에서 요구사항의 변경은 매우 당연하다.

소프트웨어 공학에서 요구사항 변경에 대처하는 설계 원리로는 응집도결합도가 있다.

  1. 응집도 : 크래스나 모듈 안의 요소들이 얼마나 밀접하게 관련되어 있는지
  2. 결합도 : 어떤 기능을 실행하는데 다른 클래스나 모듈들에 얼마나 의존적인지

캡슐화는 결합도를 낮게 유지할 수 있게 하는 객체지향 설계 원리이다. 캡슐화는 정보 은닉을 통해 높은 응집도, 낮은 결합도를 갖게 한다. 정보 은닉이란 알 필요가 없는 정보는 외부에서 접근하지 못하게 하는 것이다. 정보 은닉이 필요한 이유는 소프트웨어는 결합이 많을 수록 문제가 많이 발생하기 때문이다. 한 클래스의 변경이 다른 여러 클래스에 영향을 미치게 되는 것과 같은 말이다.

3. 일반화 관계

일반화는 또 다른 캡슐화

일반화 관계는 객체지향 프로그래밍 관점에서는 상속 관계라고 한다. 따라서 속성/기능의 재사용만 강조해서 사용하는 경우가 많지만, 이는 한정적인 시각으로 바라보는 것이므로 여기에서는 일반화 관계라는 말을 사용한다. “일반화”는 여러 개체들이 가진 공통된 특성을 부각시켜 하나의 개념이나 법칙으로 성립시키는 과정이다.

일반화 관계는 외부 세계에 자식 클래스를 캡슐화하는 개념으로 볼 수 있고, 이때 캡슐화 개념은 한 클래스 안에 있는 속성/연산들의 캡슐화에 한정되지 않고 일반화 관계를 통해 클래스 자체를 캡슐화 하는 것으로 확장된다.

많은 사람들이 일반화 관계를 속성/기능의 상속, 재사용을 위해 존재한다고 생각하는데, 그렇지 않다고 한다. 일반화 관계는 ‘is a kind of’관계가 성립해야 한다고 했는데, 만약 해당 관계가 성립하지 않는 클래스 a, b가 있고 b 클래스가 a 클래스의 일부 기능만 재사용하고 싶을 때는 위임을 사용하는 것읻 좋다. 이 Delegation은 자신이 직접 기능을 실행하지 않고 다른 클래스의 객체가 기능을 실행하도록 위임하는 것이다. 따라서 일반화 관계는 클래스 사이의 관계이지만 위임은 객체 사이의 관계다. 아래는 위임을 사용해 일반화를 대신하는 과정이다.

  1. 자식 클래스에 부모 클래스의 인스턴스를 참조하는 속성을 만들고, this로 초기화한다.
  2. 서브 클래스에 정의된 각 메서드에 1번에서 만든 위임 속성 필드를 참조하도록 변경한다.
  3. 서브 클래스에서 일반화 관계 선언을 제거하고 위임 속성 필드에 슈퍼 클래스의 객체를 생성해 대입한다.
  4. 서브 클래스에서 사된 슈퍼 클래스의 메서드에도 위임 메서드를 추가한다.
public class MyStack<String> extends ArrayList<String> {
  //private ArrayList<String> arList = this; //1
  private ArrayList<String> arList = newArrayList<String>(); //3
  
  public void push(String element) {
    arList.add(element); //2
  }
  
  public String pop() {
    return arList.remove(arList.size() - 1)); //2 //4
  }
  
  public boolean isEmpty() { //4
    return arList.isEmpty(); 
  }
  
  public int size() { // 4
    return arList.size();
  }
}

집합론 관점으로 본 일반화 관계

일반화 관계는 수학에서 배우는 집합론과 밀접한 관계가 있다. 하나의 집단 A를 집합 A1, A2, A3로 나눴을 때 다음 관계가 성립해야 한다.

  • A = A1 U A2 U A3
  • A1 n A2 n A3 = 공집합

일반화 관계에서 제약 조건도 같다. 제약이 {disjoint, complete}가 되는데, {disjoint} 제약은 자식 클래스 객체가 동시에 두 클래스에 속할 수 없다는 의미, {complete} 제약은 자식 클래스의 객체에 해당하는 부모 클래스의 객체와 부모 클래스의 객체에 해당하는 자식 클래스의 객체가 하나만 존재한다는 의미이다.

특수화는 일반화의 역관계, 부모 클랫스에서 자식 클래스를 추출하는 과정이다. 특수화가 필요한 경우는 어떤 속성이나 연관 관계가 특정 자식 클래스에서만 관련이 있고 다른 자식 클래스에서는 관련이 없는 경우이다.

위에서는 집단 A를 3개의 집합 A1, A2, A3로 나눴는데, 이렇게 나누면서 동시에 집단 A를 B1, B2로 나눠야 할 수도 있다. 이런 분류 기준을 변별자라고 하며 일반화 관계를 표시하는 선 옆에 변별자 정보를 표시한다. 이럴 경우 예를 들어 회원을 ‘구매액’, ‘지역 주민’ 변별자에 따라 분류하면 한 회원은 VIp 등급에 속하면서 동시에 지역 주빈 클래스에도 속하게 된다. 이렇게 한 인스턴스가 동시에 여러 클래스에 속할 수 있는 것을 다중 분류라 하고 <<다중>>이라는 스테리오 타입을 사용해 표시한다. 참고로 동적 분류는 한 클래스의 인스턴스가 다른 클래스의 인스턴스로 할당될 수 있음을 의미하며 <<동적>>이라는 스테리오 타입을 사용해 표현한다.

일반적으로 각 변별자에 따른 일반화 관계가 완전히 독립적일 때는 별다른 문제가 없지만, 항상 변경될 사항을 감안하고 설계해야 한다. 원래는 VIP 회원에게만 쿠폰이 주어지다가 홍보를 위해 지역 주민일 때도 쿠폰을 주겠다 하면 일반 고객도 쿠폰을 받을 수 있게 된다. 이를 처리하는 한 가지 방법으로는 모든 분류 가능한 조합에 대응하는 클래스를 만드는 방법이 있다.

4. 다형성

객체 지향에서 다형성은 서로 다른 클래스의 객체가 같은 메세지를 받았을 때 각자의 방식으로 동작하는 능력이다. 다형성은 일반화 관계와 함께 자식 클래스를 개별적으로 다룰 필요 없이 한 번에 처리할 수 있게하는 수단을 제공한다.

다형성과 일반화 관계는 코드를 간결하게 할 뿐 아니라 변화에도 유연하게 대처할 수 있게 한다.

5. 피터 코드의 상속 규칙

피터 코드는 상속의 오용을 막기 위해 상속의 사용을 엄격하게 제한하는 규칙들을 만들었다. 아래의 5가지 규칙 중 하나라도 만족하지 않는다면 상속을 사용하면 안된다.

  1. 자식 클래스와 부모 클래스 사이는 “역할 수행” 관계가 아니어야 한다.
  2. 한 클래스의 인스턴스는 다른 서브 클래스의 객체로 변환할 필요가 절대 없어야 한다.
  3. 자식 클래스가 부모 클래스의 책임을 무시하거나 재정의하지 않고 확장만 수행해야 한다.
  4. 자식 클래스가 단지 일부 기능을 재사용할 목적으로 유틸리티 역할을 수행하는 클래스를 상속하지 않아야 한다.
  5. 자식 클래스가 ‘역할’, ‘트랜잭션’, ‘디바이스’등을 특수화 해야 한다.

예를 들어 사람이라는 슈퍼 클래스, 운전자, 회사원이라는 서브 클래스가 2개 있을 때 규칙에 위배되는지 보겠다.

  1. ‘운전자’는 어떤 순간에 ‘사람’이 수행하는 역할의 하나, ‘회사원’도 어떤 순간에 ‘사람’이 수행하는 역할의 하나다. 규칙에 위배된다.
  2. ‘운전자’는 어떤 시점에 ‘회사원’이 될 수 있고 그 반대도 마찬가지다. 규칙에 위배된다.
  3. 각 클래스의 속성/연산이 정의되지 않았기 때문에 알 수 없다.
  4. 기능만 재사용할 목적으로 상속 관계를 표현하지는 않았으므로 규칙을 준수한다.
  5. 슈퍼 클래스가 역할, 트랜잭션, 디바이스를 표현하지 않았으므로 준수.

따라서 이 관계에서는 상속보다는 집약/연관 관계를 사용해 클래스 사이의 관계를 표현하는 것이 좋다.

dddd