클래스 상속의 논리적 오류

 

 자바에서는 추상 클래스라고 불리우는 클래스가 있는데 이 추상 클래스는 폴리모피즘을

이용하는 설계를 하는데 있어서 유용한 도구입니다. 어떤 경우에 추상클래스가 사용되는지

생각해 보겠습니다.

 

스타크래프트에 등장하는 유닛에 대한 설계를 상속을 이용해서 했는데, StarcraftUnit클래스,ZergUnit클래스,ProtossUnit클래스,TerranUnit클래등에 있어서 생각해 볼 문제가

있습니다. StarcraftUnit클래스를 최상의 클래스로 하고 ZergUnit클래스,ProtossUnit클래스,

TerranUnit클래스가 StarcraftUnit클래스를 상속하도록 했습니다.

그 아래 3종족의 개별 유닛들은 ZergUnit클래스,ProtossUnit클래스,TerranUnit클래스를

상속하도록 하는 계층구조로 설계했었습니다.

이러한 계층구조를 가지는 개념들을 상속을 이용해서 설계하게 되면 객체들의 관계의 명확성, 프로그램의 효율성이나 클래스 재 사용성을 고려할 때 적절한 설계입니다.

 하지만 이렇게 클래스를 이용한 상속을 막상구현하고 보니 원하지 않는 이상한 개념이

생기게 되었습니다.

스타크래프트 유닛들에 대한 상속을 이용한 설계에 따르면 스타크래프트 유닛은

StarcraftUnit이라는 클래스로 ,프로토스 유닛은 ProtossUnit이라는 클래스로 만들어져

있습니다.

앞서 생성자에 대해서 언급했지만 모든 일반 클래스는 생성자를 갖고 있습니다.

new 키워드와 함께 생성자를 호출하면 객체를 만들어 낼수 있다는 뜻입니다.

이것을 가정했을 때 StarcraftUnit클래스의 경우는 예제 4 - 23처럼 객체를 만들

수 있습니다.

 

             StarcraftUnit unit = new StarcraftUnit("100","질럿");

 예제 4 - 23

 

다시 말하면 StarcraftUnit 객체를 만들수 있고, ProtossUnit객체를 만들 수 있는 겁니다.

하지만 분명히 이것은 논리가 안됩니다. 왜 그런지 찬찬이 들여다 보겠습니다.

여러분이 스타크래프트 게임을 할 때 만들어 내는 질럿은 실제로 존재하는 객체가 맞습니다.

그래서 new 키워드와 생성자가 합해져서 객체를 만들어 내는 과정은 자연스럽고, 꼭 있어

야 합니다. 질럿은 구체적으로 게임 속에서 전투도 하고 이동도하는 존재하는 인스턴스이기

때문입니다.  하지만 스타크래프트 유닛 이라는 객체를 만들 수 있을까요?

상당히 곤란할 겁니다. 왜냐하면 질럿은 실제로 존재하는 객체,곧 인스턴스이지만 ,

스타크래프트 유닛은 머리 속에 있는 개념일 뿐이니까요.

, StarcraftUnit은 게임에 등장하는 실제 객체들의 공통된 특징을 골라서 개념화 한 것이지

게임에 참여하는 여러분이 직접 만들어내는 객체가 아니지요.

실제로 만들어지는 것은 질럿이지 StarcraftUnit 객체가 만들어 지는 것은 아닙니다.

정확하게 말한다면 만들어진 질럿이 스타크래프트 유닛인 것은 맞지만 스타크래프트 유닛을

만드는 것은 아니지요. 이것은 옳지 않은 개념입니다.

이런 이유로 스타크래프트 유닛,프로토스유닛,저그유닛,테란유닛등의 개념을 일반 클래스

로 만들면 안되는 것입니다. 왜냐하면 일반클래스로 만들면 new 키워드와 생성자를 함께

사용하면 해당 객체를 만들어 낼수 있으니까요. 어쨋거나 말입니다.

 

자바에는 스타크래프트의 예에서 처럼 상속(여기서는 폴리모피즘을 기준으로

생각합니다.)을 이용하여 클래스를 설계하면 여러가지 장점을 가지게 되지만 본의아니게

올바르지 않게 설계가 되버립니다. 객체를 만들지 않아야 할 클래스들까지 객체를 만들 수

있게 되니까요. 클래스를 상속하는 것 자체에 오류가 있는 것은 아니지만 그 내용을 곰곰히

생각해보면 논리적으로 오류가 됩니다. 그리고 그렇게 설계한 것은 추후에 그 만큼 고생을

시킵니다. ProtossUnit객체가 프로그램에서 막 튀어 나오니까요. 전혀 예상하지 못한

개념의 객체인데 말입니다.

 

 

추상클래스의 특징

 

추상클래스의 특징을 아주 간단하게 정리하면 첫째 추상클래스는 일반클래스가 가지는

형태적인 특성을 갖고 있습니다. 멤버변수를 가질수 있고, 멤버 메소드, static 변수,

static 메소드,생성자를 가질 수 있습니다. 형식적으로 보아서는 일반 크래스와 크게

다르지 않습니다. 다만 한가지 class 앞에 abstract 키워드가 추가 되는 것은

유별나게 다른점입니다.

둘째 추상클래스는 인터페이스와 마찬가지로 메소드를 선언하기만 하고 메소드를 구현

하지 않고 버틸 수 있습니다.

일반 클래스의 경우 멤버 메소드를 선언만 하고 구현하지 않으면 컴파일이 되지 않습니다만

추상클래스의 경우는 마치 인터페이스와 같이 멤버 메소드를 선언만 하고 실제 구현하지

않아도 됩니다.

셋째 추상클래스는 절대로 new 키워드와 함께 어울릴 수 없습니다.

이것은 인터페이스와 꼭 같은 성질로 추상클래스를 이용해서 객체를 생성할 수는 없다는

뜻입니다.

 

스타크래프트 유닛들을 나타내는 클래스를 상속을 이용해서 설계했을 때 얻을 수 있는 장점

은 아실 겁니다. 다만 그렇게 했더니 논리적으로 모순된 내용, 스타크래프트 유닛객체를

만들수 있게 되고, 프로토스 유닛을 만들수 있게 되는 오류가 생겼습니다.

이런 경우에 추상클래스를 이용하면 논리적인 오류없이 완벽하게 상속을 구성할 수

있습니다. 이제부터 추상클래스의 진가를 풀어보겠습니다.

 

 

스타크래프트 유닛의 계층구조 II

 

스타크래프트의 예를 논리적인 오류없이 재구성 한다면 그림 4 - 6과 같을 것입니다.

 

사용자 삽입 이미지
 

그림 4 - 6 [starcraft interface]

 

StarcraftUnit 인터페이스

 

 그림 4 - 6을 보면 StarcraftUnit은 인터페이스로 설계되었습니다.

StracraftUnit인터페이스로 설계한 것은 스타크래프트 유닛은 개념속에만 존재합니다.

실제 형체를 가진 객체가 아니기 때문입니다. 그렇게 때문에 인터페이스에는 어떤 프로그램

코드가 들어 갈 수 없습니다. 다만 메소드의 형태만 선언할 뿐입니다.

실제 프로그램 코드가 들어가는 부분은 인터페이스를 구현한 클래스입니다.

 

일반적으로 폴리포피즘 기반하에 상속을 전개할 때 클래스 설계의 기본은 상속에

참여하는 모든 클래스에서 공통적으로 수행 하는 메소드를 모아서 인터페이스로 구성합니다.

상속에 참여하는 클래스들은 모두 인터페이스를 구현하도록 강제 되므로 결국 클래스들이

인터페이스에서 지정한 메소드을 모두 가지게 되고 이런 이유로 클래스들이 형태적으로

모두 비슷한 유형을 갖게 됩니다.

StarcraftUnit인터페이스에서는 스타크래프트 유닛이라면 모두 가져야 할 공통의 메소드를

지정했는데 유닛의 이름을 구하는 String getName(), 유닛의 체력을 구하는

int getStrength(), 유닛의 종족을 구하는 String getClan(), 유닛의 설명을 구하는 String

getDescription() 4개 메소드 입니다.

일단 StarcraftUnit 인터페이스 설계자의 생각은 이 4개의 메소드는 상속에 참여하는 어떤

클래스로부터 생성된 객체라 하더라도 기본적으로 제공해야하는 성질의 정보로 생각한

것입니다. 객체의 메소드를 호출해서 정보를 알 수 있도록 하고자 함입니다.

이와 같이 인터페이스는 상속을 이루는 클래스들의 가장 꼭대기에 위치하고, 모든 객체가

가지고 있어야할 가장 기본적인 메소드들을 선언합니다.

그림 4 - 6에서는 그림 4 - 5와 달리 StarcraftUnit 클래스가 아니라 StarcraftUnit

인터페이스로 설계했습니다. 이렇게 클래스 대신에 인터페이스로 설계하면, 인터페이스는

new 키워드와 절대 함께 살 수 없기 때문에 프로그램상에 혹시 있을지 모르는

스타크래프트 유닛을 생성하는 논리적으로 오류인 프로그램 코드는 원천적으로 존재할 수

없습니다.

 

package polymorphism;

/**

* 스타크래프트에 등장하는 유닛의 메소드 내용입니다.

*/

public interface StarcraftUnit {

             public int getStrength();                   

             public String getName();                  

             public String getDescription();

             public String getClan();                   

}

예제 4 - 24 StarcraftUnit.java

 

 

 

추상클래스(Abstract Class) ZergUnit, ProtossUnit, TerranUnit

 

 저그 유닛, 프로토스 유닛,테란 유닛의 공통점은 모두 스타크래프트 유닛이라는 것입니다.

모두가 스타크래프트 유닛이므로 체력을 구하거나, 유닛의 이름을 구하거나, 종족의 이름을

구하거나, 유닛의 개요을 구할 수 있어야 합니다.

그러나 각각 종족의 특성에 따라 서로 다른점은 종족의 이름이 당연히 다르고,프로토스

 유닛의 경우는 쉴드 변수가 별도로 있어서 다른 종족과 유별난 차이가 있습니다.

스타크래프트에 등장하는 유닛에 대한 클래스를 설계하고자 하는 설계자는 여기서 아주

난감한 문제를 만나게 됩니다.

우선 저그 유닛, 프로토스 유닛, 테란 유닛등은 어쨋거나 스타크래프트 유닛이므로

유닛의 체력, 유닛의 이름, 유닛이 속한 종족, 유닛의 개요등을 알 아 낼수 있어야

합니다. 즉 모든 스타크래프트 유닛클래스는 StarcraftUnit 인터페이스에서 선언한 메소드를

모두 구현해야만 합니다.  하지만 저그 유닛인 Hydra Zergling, Lurker등은 모두 공통점이

있습니다. 내용적으로 같은 종족인데다가 형태적으로는 체력,종족등 공통의 멤버변수를

가지고 있습니다. , 저그 유닛들간에 반복되는 많은 공통점이 존재한다는 것입니다.

당연히 테란유닛들간에도 반복되는 공통점이 존재하고, 프로토스 유닛간에도 마찮가지

입니다.

 

예를 들면 Hydra클래스 설계자, Lurker클래스 설계자들은 StarcraftUnit 인터페이스를

구현해야 하는데,Hydra클래스와 Lurker클래스는 구현을 하는데 있어서 매우 많은 공통점을

갖고 있습니다.

이런 공통점을 개념화 해서 이용하지 않고 Hydra클래스,Lurker클래스를 각각 만들어낸다면

Hydra클래스,Lurker클래스에는 반복되는 코드를 갖게 되고, 전체적으로 매우 비효율적

입니다.

 

이때 등장하는 것이 바로 추상클래스입니다. 추상클래스는 그 자체를 이용해서는 객체를

만들수 없으므로 ZergUnit 객체라는 논리적으로 엉뚱한 객체는 프로그램에서 원천적으로

만들어지지 않으며 저그 유닛들간의 공통점을 추상클래스에 포함시키고, 모든 저그유닛

클래스에서 추상클래스를 상속한다면, 저그유닛 클래스를 설계할 때 만다 반복되어야

하는 코드를 피할 수 있습니다.

 

추상클래스(Abstract Class)의 성격은 인터페이스와 일반클래스의 중간쯤 됩니다.

형태적으로는 구별하는 추상클래스는 class 앞에 키워드 abstract라고 붙이면 추상클래스가

됩니다. 일단 추상클래스가 되고 나면 new 라는 키워드와 함께 살 수 없습니다.

그러니까 추상클래스형 객체는 존재하지 않는 것입니다. 자바 버추얼머신이 추상클래스형

객체를 만들어 주지 않습니다. 이와 같이 new 키워드와 함께 살수 없어서 객체를 만들수

없는 부분은 인터페이스와 많이 닮았습니다.

 하지만 일반 클래스가 가질 수 있는 멤버변수, 멤버메소드를 모두 가질수 있습니다.

때문에 스타크래프트 유닛의 예를 설계하기에는 아주 적적한 클래스입니다.

 

즉 인터페이스는 공통의 개념을 설계하기에 가장 적합한 곳이고 추상클래스는 공통의 개념

을 구현하기에 아주 적절한 곳입니다.

일반클래스와 같이 멤버변수, 멤버메소드를 자유롭게 가질 수 있고, new 키워드와 함께

사용할 수 없으므로 추상클래스형 객체가 만들어지지 않기 때문에 논리적 오류가 발생할

가능성이 전혀 없게 됩니다.

 

package polymorphism;

/**

* 스타크래프트에 등장하는 저그유닛입니다.

*/

public abstract class ZergUnit implements StarcraftUnit {

             /**

             * 종족이름

             */

    private String clan;       

    /**

    * 체력

    */   

             protected int strength;                      

 

             /**

             * 체력이 strength, 종족은 "저그"인 유닛

             */

    public ZergUnit(int strength) {

        this.strength = strength;   // 생성자에서 가장먼저 호출

        clan = "저그";

    }

    public String getClan() {

        return clan;

    }

             public int getStrength() {

                           return strength;

             }

             /**

             * ZergUnit 추상클래스에서는 유닛의 이름을 알 수 없음

             */

             public abstract String getName();      

             /**

             * ZergUnit 추상클래스에서는 유닛의 개요을 알 수 없음

             */

    public abstract String getDescription();

}

예제 4 - 25 ZergUnit.java

 

예제 4 - 25 ZergUnit 클래스를 보면 먼저 abstract라는 키워드가 class앞에 있습니다.

이는 ZergUnit 클래스를 추상클래스로 정의한다는 뜻이고, ZergUnit 클래스는 절대로

new 키워드와는 함께 살 수 없습니다.

 

스타크래프트의 계층적 유닛구조를 생각해 보면 ZergUnit StarcraftUnit으로써

StarcraftUnit인터페이스에서 선언된 모든 메소드를 구현할 의무가 있습니다.

그래서 abstract class ZergUnit 다음에 implements라는 키워드를 쓰고, 구현하고자 하는

StarcraftUnit 인터페이스를 적었습니다.

이 한줄의 의미는 상당히 중요한 것입니다. 실제로 클래스를 설계할 때, 이 한 줄을

제대로 만들기 위해서는 상당한 수준의 계획과 분석 노력이 필요합니다.

 

ZergUnit clan 이라는 종족명을 유지하는 멤버변수를 만들고 private으로 지정했습니다.

대신 String getClan()이라는 메소드를 통해 종족의 이름을 읽을 수는 있게하고, 고칠 수는

없게 했습니다. 이는 아주 적절한 조치라고 생각합니다. 왜냐하면 한번 만들어진

저그유닛의 종족명을 바꾸어야 할 이유가 전혀 없기 때문입니다. 그리고 모든 저그 유닛의

종족명은 저그이기 때문에 ZergUnit클래스에 멤버변수 clan을 선언하고 아예 값을

정합니다. 왜냐하면 ZergUnit 클래스를 상속하는 스타크래프트 유닛은 어떤 유닛이건 간에

종족은 저그이니까요. ZergUnit클래스에서 정보를 알고 결정할 수 있습니다.

종족을 의미하는 clan변수를 private으로 지정해서, ZergUnit 클래스를 상속한 클래스에서

clan변수를 직접 읽고,쓰지 못하도록 했습니다. 다만 메소드를 String getClan()메소드를

제공해서 종족에 대한 정보를 읽어낼수 있수 있게 했습니다.  스타크래프트 유닛이

저그라는 것을 ZergUnit클래스에서 결정지으면 이후에 바뀌어야 할 이유가 없기 때문입니다.

논리적으로 매우 타당하지요.

하지만 저그 유닛이 가지고 있는 체력은 strength라는 멤버변수에 유지되고 있는데 이를

protected로 선언했습니다. 이는 ZergUnit클래스를 상속하는 모든 개개의 유닛 클래스에서

마음껏 strength를 읽고,쓰게 하도록 허락한다는 뜻입니다.

예를 들어 저그 유닛을 상속한 Hydra클래스에서는 체력변수 strength를 읽거나 쓰고자

할 때 strength 멤버변수를 직접 참조해서 읽고,쓰수 있으면, 메소드 String getStrength()

통해서 체력을 읽을 수도 있습니다. 물론 Hydra클래스 설계자는 체력변수를 읽고,

쓸때 메소드를 호출하는 것보다 직접 읽고,쓴다면 무척이나 편하고 좋겠지요.

 

 예제 4 - 25 ZergUnit클래스는 StarcraftUnit인터페이스를 구현한다고 선언했습니다.

일반적으로 인터페이스를 구현하다고 선언한 클래스는 인터페이스에서 지정한 메소드를

반드시 구현해야 합니다. 만약 인터페이스에서 선언한 메소드중 하나라도 구현하지 않는

다면 그 클래스는 컴파일 조차 되지 않기 때문에 사용할 수 없습니다.

하지만 여기에 예외가 있는데 바로 추상클래스입니다.

추상클래스는 인터페이스를 구현한다고 선언하고도 메소드를 구현하지 않고 버틸 수 있는데

이렇게 버티고자 할때는 반드시 abstract라는 키워드를 메소드에 붙여주어야 합니다.

abstract 메소드는  메소드 내용이 없다는 것을 표시하기 위해서 메소드의 내용블록을 { }

로 둘러싸는 것이 아니라 ;로 끝을 냅니다.

하지만 인터페이스를 구현한다고 선언한 클래스는 어찌되었건 인터페이스에서 선언한

메소드를 모두 구현해주어햐 합니다. 다만 추상클래스는 인터페이스의 메소드를 모두

구현하지 않고, 미루어 놓은 것이기 때문에, 추상클래스를 상속하는 클래스는 언젠가

추상클래스가 구현하지 않고 미루어 놓은 메소드를 구현해야 합니다.

예를 들어 Hydra클래스는 ZergUnit추상클래스를 상속했고, 추상 클래스가 아니므로

ZergUnit추상클래스가 미루어 놓은 메소드를 반드시 구현해 주어야 하겠습니다.

 

ZergUnit 추상클래스에는 총 4개의 메소드가 있는데 그 중 2개의 메소드는 구현해

놓았고, 2개의 메소드는 미루어 놓았습니다.

ZergUnit 추상클래스에서 구현해 놓은 메소드는 ZergUnit클래스를 상속하는 스타크래프트

유닛 클래스에서는 구현해야할 필요가 없습니다. ZergUnit클래스에서 구현해 놓은 것을

얻어 쓰면 되니까요.

하지만 논리적으로 봤을 때 ZergUnit 추상클래스에서 해당되는 유닛의 이름을 알수가

있을까요? 저그유닛은 약 20여종류가 되는데, ZergUni t추상 클래스가 가진 공통된 개념은

각 유닛에게 모두 이름이 있다는 것을 알고 있을 뿐이지, 각 유닛의 구체적 이름을 알 수는

없습니다.  , 각 유닛별로 이름을 구하는 메소드는 존재한다는 것을 알지만 , 그 이름이

무엇인지는 모르기 때문에 ,이름을 구하는 메소드를 여기서는 구현할 수는 없습니다.

ZergUnit 추상클래스가 이름을 구하는 메소드를 구현하지 않고 미룸으로써 ZergUnit

추상 클래스를 상속하는 모든 저그유닛 클래스가 이름을 구하는 메소드를 스스로 구현

하도록 강제 했습니다. 이러한 설계의 원칙 때문에 ZergUnit 추상클래스를 상속하는 모든

클래스는 반드시 String getName()을 구현해야합니다.

여기에 참 매력이 있습니다. Hydra 클래스는 자신의 이름이 “Hydra”인 것을 알 수

있습니다. Hydra 클래스에서는 당연히 String getName()메소드를 아주 쉽게 구현할 수

있습니다. Zergling 클래스에서도 마찮가지 이겠지요. , ZergUnit 추상클래스를 상속한

개별 클래스에서는 각자 자신에게 가장 적합한 정보를 가지고 메소드를 정확하게 구현할

수 있다는 것입니다

ZergUnit 추상 클래스에서 String getDescription() 메소드 또한 구현하지 않고 미뤘습니다.

ZergUnit 추상 클래스에서는 String getDescription()을 구현할 수 있습니다.

왜냐하면 String getName()과는 달리 ZergUnit 추상클래스에서 저그 유닛에 대한 일반적인

내용를 구현 할 수 있기 때문이지요. 예를 들면 저그 종족입니다.”라는 개요를 정의하면

됩니다.  여기서 말하고 싶은 것은 설계자의 의도입니다.

ZergUnit 추상클래스 설계자는 ZergUnit 추상클래스를 상속하는 모든 클래스에서

개요정보를 ZergUnit 추상클래스를 상속한 클래스 자신에게 가장 알맞는 정확한 정보를

구현하라고 강제하고 싶었던 것입니다.

추상 클래스에서 구현하지 않고 미뤄놓았던 메소드를 추상클래스를 상속한

일반 클래스에서는 반드시 구현을 해야 되기 때문입니다.

만약 ZergUnit 추상 클래스에서 String getDecription()을 구현했다면 Hydra 클래스등에서는

String getDescription()을 구현하지 않아도 됩니다.

상속을 받은 기본 클래스에 있는 메소드는 자신에게도 있는 것이니까요.

 

ZergUnit추상클래스에서 String getDescription()을 구현했고, Hydra클래스에서

getDescription()을 구현하지 않았다고 가정하겠습니다.

            

ZergUnit unit = new Hydra();

             unit.getDescription();          // 실제는 여기서는 Hydra의 개요정보를 얻고 싶은 것입니다.

예제 4 - 26

 

unit.getDescription()  ZergUnit 추상클래스에서 구현한 메소드가 호출 될 것입니다.

클래스 설계자가 애초에 원했던 것은 Hydra의 개요일텐데, 논리적으로 조금 맞지

않겠습니다. 설계 구조상으로는 아무 문제가 없지만 , 논리적으로 정확하게 설계자가

원하는 대로 구현하도록 강제하기 위해서 ZergUnit추상클래스 설계자는

String getDescription()메소드도 abstract 키워드를 붙여서 메소드를 구현하지 않고

미뤘습니다.  Hydra클래스로 하여금 반드시 String getDescription()을 구현하도록 하고

싶었던 겁니다.  필자도 옳은 판단이라고 생각합니다.

 

 

package polymorphism;

/**

* 스타크래프트에 등장하는 저그유닛 히드라입니다.

*/

public class Hydra extends ZergUnit {

             private String name;

             public Hydra() {

                           super(80);

                           this.name = "히드라";

             }

             public String getName() {

                           return name;

             }

             public String getDescription() {

                           return "이름은 히드라, 종족은 저그 종족입니다.럴커로 변태할 수 있어요.";

             }

}

예제 4 - 27 Hydra.java

 

Hydra클래스에서는 ZergUnit 추상클래스를 상속하고, ZergUnit추상클래스에서 구현하지

않은 두개의 메소드를 모두 구현했습니다.

이로써 Hydra 클래스는 StarcraftUnit인터페이스를 구현하고, ZergUnit클래스를 상속한

클래스가 되었습니다. 폴리모피즘적인 얘기를 한다면 Hydra객체는 Hydra 클래스형

레퍼런스와 결합할 수 있고,ZergUnit 클래스 레퍼런스와도 결합할 수 있으며 StarcraftUnit

인터페이스형의 레퍼런스와도 결합할 수 있습니다.

 

 

package polymorphism;

 

public class UnitDescription {

 

             public void description(StarcraftUnit unit) {

                           System.out.println(unit.getDescription());

             }

            

             public static void main(String[] args) {

                           UnitDescription desc = new UnitDescription();

                           desc.description(new Hydra());

             }

}

예제 4 - 28 UnitDescription.java

 

 

 

 

이와 같이 폴리포피즘 적인 설계를 할 때 추상클래스를 도입하게 되면 표면적 설계상으로도

좋은 설계가 되지만 논리적으로도 오류를 없앨 수 있는 좋은 답이 되겠습니다.

 


사용자 삽입 이미지

그림 4 - 7 [ run unitdescription]

Posted by
,