본문 바로가기

기타/객체지향의 탄생(2013)

객체지향의 탄생-상속과 구성

 

  1. 상속과 구성

세상의 모든 기술은 저절로 습득되는 것이 있고, 따로 공부와 훈련이 필요한 경우도 있다. 예를 들어 당구를 치다보면 경험을 쌓으며 저절로 기술을 습득한다. 그러나 정식으로 배우지 않은 당구 기술은 한계가 있거나 아무리 오래쳐도 300넘기가 쉽지 않다. 대신 정식으로 당구를 배울 경우 처음에는 번거롭고 힘들어도 언젠가부터 실력이 빠르게 향상 될 것이다. 저절로 익혀지는 기술은 처음 배우기는 쉬우나 갈수록 효율성이 떨어지는 기술일 것이고, 따로 배워야 하는 고급 기술은 처음 배우기는 어려우나 알수록 효율성이 높아 편하게 활용하는 기술일 것이다.

 

 

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

 

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

 

 

상속은 복잡하지 않은 프로그램에서 편하게 사용하기는 좋다. 상속은 사용하기가 쉽고 객체지향 개념치고 이해하기도 편해 쉽게 이해하고 활용한다. 그러나 객체지향에 익숙한 개발자가 되면 상속을 무작정 사용하는 것이 결국 부매랑이 되어 개발자를 괴롭힐수도 있다는 것을 알게 된다.

만약 개발자가 대충 객체지향 프로그램을 짠다면 무시할수도 있겠지만 좀더 전문적이면서 유연하고 확장성 높게 개발하려면 지나친 상속 남발은 문제가 있다. 이제 어떤 문제가 있는지 알아본다.

 

 

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

 

[등산화 클래스를 통한 상속 예제]

 

그림과 같이 등산화 클래스와 이를 상속받는 여러 등산화들이 있다. 등산화 클래스에 변경 요구사항이 생겼다. 등산화 클래스에 고어텍스 기능을 추가하라는 요청이었다.

 

개발자는 별다른 고민 없이 고어텍스 메소드를 등산화 부모 클래스에 추가했다. 그러자 등산화 클래스를 상속받은 하위 클래스는 상위 클래스의 고어텍스 메소드를 상속받게 되어 특별히 수정할일이 발생하지 않게 되었다. 역시 상속은 재사용을 편하게 지원했다.

 

그러나 문제가 발생했다. 등산화 하위 클래스중 고어텍스 기능이 원래 없는 클래스가 존재했다. 현재 고어텍스 기능이 없는 WhiteYak-A등산화 클래스가 지금 클래스 구성도라면 억지로 고어텍스 기능을 상속받게 되는 문제가 발생한다.

 

개발자는 해결 방법을 고민하였고, 곧 간단하게 문제를 해결한다. 고어텍스 기능이 없는 WhiteYak-A클래스는 고어텍스 메소드를 따로 오버라이드 하면서 아무작업도 하지 않게 로직 처리하였다. 하지만 개발자가 곰곰히 생각해보니 뭔가 찜찜했다.

 

public class WhiteYakAClimbingBoots extends MountainClimbingBoots {

{ // WhiteYakA 등산화 클래스

    public void goreTexFunction() {

        // 부모 클래스에서 기본적으로 고어텍스 메소드를 정의하였지만,

        // 이 등산화 클래스는 고어텍스 기능이 없으므로, 일부러 고어텍스 기능 메소드를

        // 오버라이드 하여, 아무일도 안한다고 명시해야 한다.

        System.out.println("아무일도 안함");

    }

}

 

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

 

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

 

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

 

 

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

 

그러나 상속은 모든 자식 클래스들이 부모 클래스의 속성과 기능을 똑같이 상속받아야 한다. 상속은 객체지향 언어에서 고정된 문법이기 때문에, 자식 클래스가 한번 상속 받은 속성과 기능은 컴파일 이후 변경이 불가능하다.

 

그래서 마치 한번 쓰면 지우지 못하는 볼펜처럼 프로그램 실행 후에는 유연하게 행동을 변경하지 못한다. 그래서 자식 클래스들의 일부 특성이 부모 클래스와 어긋날 경우, 프로그램 수정, 확장등의 유지보수 작업이 어려워 진다.

 

 

[그림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());

    …...

}

 

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

 

 

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

 

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

 

위의 예제를 보면 고어텍스 기능이 필요하여 고어텍스 객체 그룹군을 새로 만들어 이 객체 그룹에 기능 구현을 대신 시켰으며, 다양한 고어텍스 기능이 요구될때마다 새로운 고어텍스 자식 클래스를 개발하여 기존의 소스를 건들지 않고 기능을 손 쉽게 확장하였다

 

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

 

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

 

덧 ) 이 객체지향의 탄생 원고는 제가 책으로 내려다가 일단 잘 안되었는데요. 이유는 비문이 많다. 단락내 주제가 중복된다. 어떤 상황 설명을 과장한다.등 입니다. 그래도 원고를 일단 블로그에 몽땅 풀어보고 언젠가 제대로 교정해서 다시 도전할 생각입니다. 비문이 많다. 단락내 주제가 중복된다. 어떤 상황 설명을 과장한다. 이점을 감안해서 읽고 객체지향을 이해하는데 도움이 되셨으면 좋겠습니다. 의견도 주셨으면 좋겠습니다. 원고 조금만 교정하면 괜찮을것 같은 출판사 관계자분의 피드백도 환영합니다. 특별한 일 없으면 매주 월수 발행 예정입니다.