다형성

다형성

다형성(Polymorphism)이란 다음과 같이 정의되어 있다.

하나의 객체가 여러 가지 타입을 가질 수 있는 성질

정의 자체가 매우 어려운 편이기 때문에 사례를 통해 이를 해석해볼 필요가 있다.

현실에서의 다형성

현실에서의 다형성 사례에 대해서 알아보기 전에 우선 자료형이 무엇인지 다시 한 번 짚어볼 필요가 있다.

자료형이란 대상의 형태를 의미한다.

100정수이며, 1.5실수이고, hello문자열이다. 기초 데이터들은 간단하게 정의를 내릴 수 있지만 객체는 어떠한지 사례를 보고 파악해보도록 한다.

첫 번째 사례

철수 : 오늘 점심 뭐 먹을래?

영희 : 오랜만에 김밥이나 먹을까?

철수와 영희의 대화에서 영희가 말한 김밥이라는 것은 어떤 형태를 말하는 것일까? 무슨말인지 이해가 가지 않는 사람이 있다면 다음 보기 중에서 영희가 먹자고 말한 김밥이 무엇인지 골라보길 바란다.

  1. 참치김밥

  2. 치즈김밥

  3. 야채김밥

  4. 삼각김밥

주어진 대화로는 어떤 김밥을 의미하는지 알 수 있는 방법이 없다. 왜냐하면 김밥이라는 형태 자체가 매우 추상적인 형태이기 때문이다. 실제로 위의 1번부터 4번 보기에 있는 김밥 중 아무것이나 김밥 대신 집어넣어도 대화를 이해하는데 아무 문제가 없다.

오랜만에 참치김밥이나 먹을까?

오랜만에 치즈김밥이나 먹을까?

오랜만에 야채김밥이나 먹을까?

오랜만에 삼각김밥이나 먹을까?

여기서 우리는 두 가지 의문점을 가질 수 있다.

  1. 왜 김밥 대신 보기에 언급된 김밥들을 넣어도 말이 되는걸까?

  2. 왜 영희는 처음부터 구체적으로 어떤 김밥을 먹자고 이야기하지 않았을까?

1번에 대한 답은 김밥이라는 형태와 참치김밥, 치즈김밥 등의 형태는 서로 상속관계이기 때문이라고 설명할 수 있다.

김밥이라는 형태가 최상위 형태이며, 하위 형태로 여러 종류의 김밥들이 위치한다. 따라서 김밥이라는 형태는 모든 하위 김밥 대신 사용할 수 있는 형태가 되므로 대화가 성립하는 것이다. 하지만 범위를 좀 더 확장해본다면 어떨까?

김밥 대신 분식이라고 이야기한다면 의미 전달이 될까?

오랜만에 분식이나 먹을까?

의미 전달이 정확히 되지 않는다. 분식에는 김밥 외에 다른 요소들이 포함되기 때문이다. 따라서, 김밥이라는 단어까지만 사용할 수 있다. 이처럼 특정 상황에 맞는 형태를 지칭함으로 인하여 하위 요소들을 모두 포함시켜 이야기할 수 있다.

2번에 대한 답은 편리하기 때문이다. 만약 모든 상황에서 명확한 형태를 이야기해야 한다고 하면 대화는 다음과 같아질 것이다.

철수 : 오늘 점심 뭐 먹을래?

영희 : 오랜만에 강남역 5번출구에 있는 김밥카페에서 파는 점심특선메뉴 피자김밥이나 먹을까?

위에 등장한 위치와 상호는 실제 존재하는 곳이 아니다

어떻게 말해도 대화가 되는 상황이라면 굳이 구체적으로 말할 필요가 있을까? 이것이 우리가 구체화된 실제 형태(피자김밥)가 아닌 추상적인 형태(김밥)로 말하게 되는 이유이다.

프로그래밍에서도 마찬가지이다. 상속 관계라는 전제 조건만 성립된다면, 우리는 자유롭게 하위 형태를 상위 형태로 표현해서 부를 수 있다.

Phone.java
public class Phone {
    
}

PolymorphismExample01에서 ab의 생성방식에 대해서 살펴볼 필요가 있다.

IPhone12 a = new IPhone12();

기존에 알고 있는 객체 생성 방식이며, 생성 형태와 참조 형태가 동일하다. 참조 형태를 보관 형태로 생각하면 이해하기가 좀 더 쉬워진다. 아이폰(IPhone12)을 만들어서 아이폰(IPhone12) 참조변수에 보관한다고 생각하면 된다. 이 경우는 구체적인 형태로 객체를 보관,관리한다고 볼 수 있다.

Phone b = new IPhone12();

생성한 형태와 참조 형태가 다르다. 아이폰(IPhone12)을 만들어서 휴대폰(Phone) 참조변수에 보관한다고 생각하면 된다. 휴대폰이 더 상위 형태라는 것을 상속 관계를 통해 확인할 수 있으므로 전혀 문제가 되지 않는다. 하위 형태의 객체를 상위 형태로 보관하는 것을 업-캐스팅(up-casting)이라고 부른다.

왜 이렇게 해야 하는가? 에 대한 답은 명확히 내리기 어렵다. 마치 철수와 영희의 대화에서 영희가 왜 김밥이라고 이야기했는지 한참을 생각해봐야 알 수 있는 것처럼 상황에 따라 적절한 형태의 참조변수를 이용하여 보관하고 쓰기 위해서라고 볼 수 있다.

두 번째 사례

우리 동네에는 "맛있는 치킨" 이라는 가게가 있다.

이 가게에서는 후라이드 치킨, 양념 치킨, 간장 치킨을 팔고 있다.

위의 사례에서 우리가 의문을 가질 수 있는 것은 왜 가게 이름을 맛있는 치킨이라고 지었을까? 라는 것이다. 실제로 판매하는 것은 후라이드, 양념, 간장 치킨이면서 간판에 맛있는 치킨이라고 해두었다면 사기이지 않을까? 실제로 간판을 이렇게 만들어야 하지 않을까?

우리 동네에는 "맛있는 후라이드 / 양념 / 간장 치킨" 이라는 가게가 있다.

이 가게에서는 후라이드 치킨, 양념 치킨, 간장 치킨을 팔고 있다.

말이 되지 않는 억지라는 것은 쉽게 파악할 수 있지만 여기에 대해서 명쾌한 설명을 하기는 쉽지 않다.

위의 사례에서도 우리는 상속 관계를 파악할 수 있다.

간판을 맛있는 치킨이라고 해도 문제가 없는 이유는 치킨이라는 상위 형태로 모든 하위 요소들을 표현할 수 있다는 것을 알고 있기 때문이다. 즉, 치킨이라는 형태가 다양한 하위 형태들을 표현할 수 있기 때문에 이는 다형성에 해당한다.

정리하면 다음과 같다.

  • 맛있는 치킨이라는 간판을 내걸어도 다형성 때문에 아무 문제가 없다.

  • 오히려 다양한 하위 요소들을 표현할 수 있으므로 더 간결해진다.

Chicken.java
public class Chicken {

}

세 번째 사례 - 업캐스팅, 다운캐스팅

유재석(劉在錫, 1972년 8월 14일 ~ )은 대한민국의 방송인, MC, 희극인, 가수이다.

출처 : 위키백과 - 유재석

첫 번째, 두 번째 사례를 통해 우리는 객체의 형태란 무엇 인지에 대해서 어느 정도 생각해봤다. 그렇다면 유느님의 자료형은 무엇일까? 상황에 따라서 평범한 사람으로 볼 수도 있고, 방송인으로 볼 수도 있고, MC로도 볼 수 있으며, 희극인/가수로도 볼 수 있다.

그렇다면 프로그래밍으로 이러한 구조를 나타낼 수는 없을까?

코드로 나타내면 다음과 같다. 가독성 향상을 위하여 상속받지 않은 인터페이스나 클래스는 배제하였고, 내부 코드는 주석으로 대체하였다.

MC.java
public interface MC { 
    //MC 고유 기능
}

아버지, 어머니는은 하나만 선택할 수 있도록 class로 구현하였고, 중복 선택이 가능한 항목들은 interface로 구현하였다.

객체 생성

유느님 객체를 생성해본다.

Yoo yoo = new Yoo();

지금 객체의 참조(보관) 형태는 Yoo 이다. 현재는 객체와 동일한 타입으로 참조하고 있으며, 이를 출근 전의 유느님으로 생각할 수 있다.

하지만 이후부터는 상황에 따라 참조 형태가 달라질 수 있다.

  1. MC를 보기 위해 출근한다.

  2. 코메디를 하기 위해 출근한다.

  3. 노래를 부르기 위해 출근한다.

  4. 방송인으로 시상식에 가기 위해 출근한다.

  5. 학부모 자격으로 학교 수업에 참관한다.

  6. 한국인 대표로 해외에 출장을 간다.

1번부터 6번까지 어떠한 일이 발생해도 유느님의 객체 자체는 변화가 없다. 하지만 어떤 번호의 일이 발생 하는지에 따라 행동의 제약이 발생할 수 있다.

1번의 경우

만약 1번의 경우라면 다음과 같이 참조타입이 변하게된다.

MC mc = (MC)yoo;

Yoo라는 하위 형태에서 MC라는 상위 형태로 변환시키는 것이 MC를 보기 위해 출근하는 것과 동일한 형태의 코드라고 볼 수 있다. MC로 출근한다고 해서 다른 사람이 되는 것은 아니기 때문에 객체는 그대로 유지되며, 단지 MC에서 허락된 기능을 제외한 다른 기능을 사용할 수 없다는 특징이 생기게 된다.

이처럼 상위 형태로 변환하는 것을 업-캐스팅(up-casting) 이라고 부른다. 실생활에서는 출근업-캐스팅을 하는 행위에 해당한다. 업-캐스팅은 특별한 제약사항이 없기 때문에 변환연산을 생략하여 다음과 같이 표현할 수 있다.

MC mc = yoo;

보관 형태만 달라지며 객체는 변하지 않는다는 사실을 잊으면 안 된다.

2번의 경우

2번의 경우라면 다음과 같이 업-캐스팅을 수행할 수 있다.

//Comedian comedian = (Comedian)yoo;
Comedian comedian = yoo;

마찬가지로 변환연산은 생략이 가능하며, 이 때 comedian 으로는 코메디언의 기능만 실행할 수 있다.

3~6번의 경우도 마찬가지이다.

3번의 경우

//Singer singer = (Singer)yoo;
Singer singer = yoo;

4번의 경우

//Talent talent = (Talent)yoo;
Talent talent = yoo;

5번의 경우

//Father father = (Father)yoo;
Father father = yoo;

6번의 경우

//Korean korean = (Korean)yoo;
Korean korean = yoo;

이렇게 하나의 객체가 다양한 상황에 맞게 업-캐스팅 처리되어 사용할 수 있으며, 이를 통해 복잡한 현실 구조를 실제와 유사하게 구현할 수 있다. 하지만 상황에 따라서는 본래의 형태인 Yoo로 되돌려야 하는 경우도 발생한다.

Yoo god = (Yoo)singer;

위에서 변환해봤던 Singer클래스 타입의 참조변수인 singer를 Yoo 클래스 타입인 god에 변환하여 대입하고 있다. 상위 형태에서 하위 형태로 변환하는 것을 다운-캐스팅(down-casting) 이라고 한다.

다운-캐스팅은 상황에 맞게 변환했던 데이터를 원래의 형태로 되돌리는 작업이다. 원래대로 되돌려야 하는 이유는 여러 가지가 있겠지만 중요한 것은 다른 형태로 변환하는 것이 아니라 원래의 형태로 되돌린다는 것이다. 따라서 원래의 형태가 없거나 원래의 형태와 다른 경우에는 변환이 불가능하다. 즉, 다운-캐스팅을 하기 전에 따져봐야 하는 요소들이 있다는 것이다.

이러한 이유로 다음 코드는 오류가 발생한다.

Yoo god = singer;

가수가 유느님만 있는것이 아니므로 singer에 들어있는 원래의 객체가 Yoo형태인지 확신할 수 없으므로 컴파일러는 불확실함에 대한 오류를 발생시킨다. 따라서 다운-캐스팅에서는 반드시 형태 변환연산자를 작성해야 한다.

다운-캐스팅에서의 불확실성 제거

세 번째 사례를 통하여 업-캐스팅(up-casting), 다운-캐스팅(down-casting)이라는 용어에 대해서 살펴봤으며, 다운-캐스팅의 경우 불확실성이 수반되어 오류 발생 가능성이 있다고 했다. 이런 불확실성을 해결하기 위해선 객체 원래의 형태에 대한 검증 절차가 필요하며, 이를 수행할 수 있는 연산이 instanceof 연산이다.

System.out.println(singer instanceof Yoo);

위 코드는 singer 라는 참조 변수가 Yoo 형태인지를 검사하여 논리를 반환한다. 따라서 확실하게 Yoo 형태일 경우만 다운-캐스팅을 수행하려면 다음과 같이 코드를 작성해야 한다.

if(singer instanceof Yoo){
    Yoo god = (Yoo)singer;
}

업-캐스팅과 다운-캐스팅을 마구 섞어서 사용할 필요는 없다. 부적절한 코드의 남용은 가독성을 저하시키며 팀원간의 스트레스만 가중시킬 뿐이다. 꼭 필요한 상황에서만 이용하여 효율성을 증가시켜야 의미가 있으며, 우리말에 "적당히" 라는 단어를 떠올리며 어떤 상황이 적당한 상황일지에 대해 끊임없이 고민하고 경험해볼 필요가 있다.

Last updated