본문 바로가기

기타/객체지향 토막글

상속과 구성 - 수필 객체지향

시골의 아이들은 어렸을때 물가에서 놀면서 형들이 약간만 알려주면, 저절로 헤엄치기를 배운다. 누가 정석으로 가르쳐 준것도 아닌데 잘도 헤엄친다. 상속은 시골 아이들의 야생 헤엄치기와 같다. 객체지향에 대해 약간의 지식만 익히고 사용할 줄 알면 상속을 습관처럼 저절로 쓰기 때문이다.

아이들의 야생 헤엄치기는 물가에서 놀기는 좋다. 그러나 수영 전문가가 볼때는 비효율적인 동작의 조합으로 조잡해 보인다. 만약 아이들이 물가에서 놀기위해 헤엄치기를 한다면 모르지만 좀더 전문적으로 수영을 하고 수영 대회도 나가고 짝사랑하는 영희에게 멋진 폼을 보여줄라면 지금의 헤엄치기는 문제가 있다.

상속은 복잡하지 않은 프로그램에서 대충 사용하기는 좋다. 그러나 객체지향 전문가가 볼때 상속을 무작정 사용하면 비효율적인 코딩이 눈에 뛴다. 만약 개발자가 아마추어스럽게 대충 객체지향 공부용 프로그램을 짠다면 모르겠지만 좀더 전문적이면서 유연하고 확장성 높게 개발하려면 지금의 상속 남발은 문제가 있다.

[삽화]

상속을 쓰면 부모의 속성과 기능을 중복 없이 재사용하거나 자신의 입맛게 재정의하여 폴리모피즘을 사용할수 있다. 그래서 객체지향의 장점을 누린다. 그러나 상속의 장점을 활용하기 위해 무분별하게 상속을 사용하면 여러가지 문제점이 생긴다. 마치 스타크래프트 프로토스에서 초기 질럿(=상속)이 막강하다고 질럿만 뽑았더니 테란의 배틀크루져 부대(=최강의 디자인패턴)가 몰려와 질럿을 몰살시키는 경우와 비슷하다.

 
[그림1.2.5-2, 등산화 클래스를 통한 상속 예제]

그림과 같이 등산화 클래스와 이를 상속받는 여러 등산화들이 있다. 그런데 등산화 클래스에 변경 요청이 생겼다. 등산화 클래스에 고어텍스 기능을 추가하는 요청이었다. 개발자는 별다른 고민 없이 고어텍스 메소드를 추가했다. 그러자 등산화 클래스를 상속받은 하위 클래스는 상위 클래스의 고어텍스 메소드를 상속받게 되어 특별히 수정할일이 발생하지 않게 되었다. 역시 상속은 재사용을 편하게 지원했다.

그러나 문제가 발생하였다. 등산화 하위 클래스중 고어텍스 기능이 없는 클래스가 존재했다. 현재 고어텍스 기능이 없는 WhiteYak-A등산화 클래스가 억지로 고어텍스 기능을 상속받은 문제가 발생하였다. 개발자는 해결 방법을 고민하였고, 곧 간단하게 문제를 해결한다. 고어텍스 기능이 없는 WhiteYak-A클래스는 고어텍스 메소드를 따로 오버라이드 하면서 아무작업도 하지 않게 처리하였다. 하지만 개발자가 곰곰히 생각해보니 뭔가 찜찜하기만 하다.

public class WhiteYakAClimbingBoots extends MountainClimbingBoots {
 {  // WhiteYakA 등산화 클래스

    public void goreTexFunction() {
        // 부모 클래스에서 기본적으로 고어텍스 메소드를 정의하였지만,
        // 이 등산화 클래스는 고어텍스 기능이 없으므로, 일부러 고어텍스 기능 메소드를
        // 오버라이드 하여, 아무일도 안한다고 명시해야 한다.
        System.out.println("아무일도 안함");
    }

}

WhiteYak-A클래스처럼 이전에 만들어진 등산화 클래스는 고어텍스 기능이 없다. 따라서 그림1.2.5-2과 같은 등산화 구성이라면 고어텍스 기능이 없는 클래스를 하나하나 찾아내어 수정을 해줘야 한다. 만약 이 클래스가 수백가 넘는다면 하나하나 클래스 찾는일은 ‘짜증나는 삽질’이 될 것이었다.

더구나 고어텍스 기능이 없는 클래스의 고어텍스 메소드에 ‘아무 일도 안한다.’고 똑같이 수정한다면, 수백개의 같은 로직이 수백개의 클래스에 중복되는 것이므로 코드 중복을 최소화 한다는 프로그래밍 원칙에도 어긋난다. 상속을 통해 코드의 중복을 막으려고 했더니 코드가 중복되는 일이 발생하였다. 개발자는 혼란스러워 한다.

또한 고객은 등산화 클래스의 ‘기능 명세’는 정해진것이 아니고 종종 변경될일이 존재한다고 말했다. 만약 그런일이 생기면 개발자는 일단 ‘부모 클래스’의 기본적인 기능 명세 변경 후, 이어서 수 많은 자식 등산화 클래스를 뒤져야 한다. 그래서 자식 클래스에 맞게 기능을 재정의(=오버라이드) 하는 번거로운 수정 작업을 진행해야 한다.

[삽화]

상속의 장점은, 부모 클래스의 속성과 메소드를 재사용 하면서, 부모 클래스의 오버라이드 기능을 이용해 폴리모피즘(=다형성) 효과를 유도하는 효과에 있다.

그러나 상속은 모든 자식 클래스들이 부모 클래스의 속성과 기능을 똑같이 상속받아야 한다. 상속은 객체지향 언어에서 고정된 문법이기 때문에, 자식 클래스가 한번 상속 받은 속성과 기능은 컴파일 이후 변경이 불가능하다. 그래서 마치 한번 쓰면 지우지 못하는 볼펜처럼 프로그램 실행 후에는 유연하게 행동을 변경하지 못하고, 자식 클래스들의 일부 특성이 부모 클래스와 어긋날 경우, 프로그램 수정등의 유지보수 작업이 어려워질 것이었다.
 
[그림1.2.5-3, 등산화 구성 예제]

위와 같이 상속의 문제가 발견될 경우 구성을 사용한다. 위의 그림에서는 고어텍스 인터페이스를 만들고, 초강력 고어텍스, 고어텍스, 고어텍스 기능 제거 클래스가 구현되어 있다. 등산화 클래스에는 고어텍스 메소드가 추가되어 있고, ‘고어텍스 인터페이스’를 새로 속성으로 받아들였다.
public class MountainClimbingBoots {  // 등산화 클래스
    private GoreTex goreTex = null; // 고어텍스 인터페이스

    // 등산화 클래스가 실행될때 폴리모피즘(=다형성)을 통해 고어텍스 클래스중
    // 하나를 인자로 받을 것이다.
    public MountainClimbingBoots(GoreTex goreTex) {
        this.goreTex = goreTex;
    }

    // 고어텍스 클래스 셋팅
    public void setGoreTex(GoreTex goreTex) {
        this.goreTex = goreTex;
    }

    public void goreTexFunction() {
        // 자신이 생성될때 넘겨받을 고어텍스 클래스에게, 같은 기능을 위임하고 있다.
        goreTex.goreTexFunction();
    }
….
}

만약 K3-A등산화가 초강력 고어텍스 기능을 쓴다면, ‘실행클래스’에서 아래와 같이 쓸수 있다.
public static void main( String[] args ) throws Exception {
    …..
    // 실행 클래스는 K3A등산화를 생성할때 초강력 고어텍스 클래스를 인자로 넘겨주었다.
    MountainClimbingBoots k3aBoots = new K3AClimbingBoots(new UltraGoreTex());
    …...       
}

그런데 생산과정에서 다시 요구사항이 변경되어 K3-A등산화가 일반 ‘고어텍스’기능으로 다운그래이드 되었다면, ‘실행클래스’에서 아래와 같이 바꿀 수 있다.
public static void main( String[] args ) throws Exception {
    …..
    // 실행 클래스는 K3A등산화를 생성할때 초강력 고어텍스 클래스를 인자로 넘겨주었다.
    MountainClimbingBoots k3aBoots = new K3AClimbingBoots(new UltraGoreTex());
    …...
    // 프로그램 실행중에 일반 고어텍스 클래스를 인자로 넘겨주었다.
    k3aBoots.setGoreTex(new GeneralGoreTex());
    ....
}

또한 WhiteYak-A등산화는 저렴한 가격을 위해 고어텍스 기능을 제거했다고 한다. 아래와 같이 쓸 수 있다.
public static void main( String[] args ) throws Exception {
    …..
    // 실행 클래스는 K3A등산화를 생성할때 초강력 고어텍스 클래스를 인자로 넘겨주었다.
    MountainClimbingBoots k3aBoots = new K3AClimbingBoots(new UltraGoreTex());
    …...
    // 프로그램 실행중에 일반 고어텍스 클래스를 인자로 넘겨주었다.
    k3aBoots.setGoreTex(new GeneralGoreTex());
    ....
    // 실행 클래스는 WhiteYakA등산화를 생성할때 고어텍스 기능이 제거된 클래스를 인자로 넘겨주었다.
    MountainClimbingBoots whiteYakABoots = new WhiteYakAClimbingBoots(new NotGoreTex());
    …...
}

위와 같이 상속을 많이 썼더니 오히려 프로그램에 ‘나쁜 냄새(=코드가 지저분해짐)’가 나는 경우가 생긴다면, 구성 구조로 ‘개선(=리팩토링)’하여 객체지향에서 본래 의도하는 유연성, 확장성, 유지보수 편리성을 다시 회복할 수 있다.


상속을 많이 썼을때의 문제는 ‘그 객체의 행동’이 컴파일시에 완전히 결정되어 프로그램 실행 중에 그 행동을 바꿀 수 없고, 모든 자식 클래스가 무조건 똑같은 기능을 상속받은 결과, 자식 클래스 중 일부가 상속받지 않아야 하는 기능도 무조건 상속받아야 하는 것에 있다.

구성이란 클래스의 특정 기능을 수행하기 위해 다른 ‘객체 패밀리’의 유사하거나 같은 역할을 하는 메소드를 사용하기 위해 클래스들끼리 대등한 관계로 결합된 구조이다.  이렇게 객체를 구성하면, 기존 코드를 고치는 대신 새로운 ‘객체 그룹군’을 만들거나 이미 만들어진 ‘객체 그룹군의 자식 클래스’를 변경하여 기능의 추가, 수정을 자유롭게 제어할 수 있다.  

그래서 기존 코드를 건들었을때 발생하는 생각하지 못한 버그(=부작용)가 연쇄 작용하는 것을 막을 수 있다. 기존 코드의 수정이 아닌 확장으로, 기존 프로그램 구성을 안전하게 지키면서 어플리케이션을 개선할 수 있다.   

마치 상속이 한번 정한 정책은 수정하기 힘들고 위에서 명령한것은 억지로 따라야 하는 경직된 관료조직과 같다면, 구성은 급변하는 시장 환경에 빠르게 적응하고 직원 개인의 특성을 잘 살려주는 바람직한 기업 현장의 조직과 같다.
 

덧) 아직 고쳐쓰기가 덜 되었습니다. 예를 들면 도입부는 그럴듯하게 쓰다가 실제 설명부분에서는 풀어쓰는 부분이 덜 되었습니다. 어려운 용어의 풀어쓰기가 안되었습니다. 기타 삽화가 부족함을 이해해 주시길 바라며, 향후 책에 쓰일 원천 자료이기 때문에 펌은 불허, 링크 환영 합니다. 조언 부탁드립니다. ^ ^;