다형성, 영어로는 polymorphism, 한자로는 多形性이라고 합니다. 먼저 용어의 의미를 정확히 이해해 볼까요. ploymorphism은 poly + morphism의 합성어로 poly는 ‘다양한’, ‘여러’ 의 뜻인데, polyglot은 여러 언어를 할줄 아는 사람을 뜻합니다. morphism은 사상(寫像) 베낄 '사' 자에 모양 '상' 자 입니다. 거울에 비친 상이라고 생각할 수 있는데, 형태를 뜻하는 morphology를 생각하면 쉽게 연상이 될 것입니다.
다형성은 한자로는 多形性인데, 있는 그대로 의미를 해석해도 다양한 형태의 특성이라고 생각할 수 있습니다. 우리는 형태라는 말을 타입, 자료형으로 이미 배웠기 때문에, 다양한 자료형을 갖는 특성이 됩니다.
이렇게 용어의 의미를 정확히 이해해야 코드를 작성하고, 해석할 때 도움이 됩니다. 개념을 정확히 정리하고 넘어가세요.
객체지향 프로그래밍 언어에서의 다형성에 대한 개념을 이해하려면 자료형과 상속 관계를 이해하고 있어야 하는데, 간단히 한마디로 정리하면,
“하위클래스 객체를 상위 클래스 자료형으로 변환이 가능하다”
우리가 변수와 자료형을 배울 때 작은 범위의 자료형은 큰 범위의 자료형으로는 자동 형변환이 되고, 큰 범위의 자료형을 작은 범위의 자료형을 변환하려면 강제 형변환을 해줘야 한다고 했습니다. int는 double로 자동 형변환이 되지만, double을 int로 형변환하려면 강제 형변환을 해야 하는 것입니다.
int a = 10;
double b = a; // 자동형변환
double c = 10.5;
int d = c; // 에러
//강제형변환
int d = (int)c;
위 예에서 변수 a는 int 자료형이므로 변수 b에 대입이 됩니다. b에 들어가 있는 값을 출력해 보면 10.0이 출력되게 됩니다.
이번엔 double 자료형의 c 변수에 10.5를 넣고 정수 d에 담으려고 하면 에러가 나게 됩니다. c 앞에 (int)를 넣어 강제형변환을 해줘야 합니다다. 그리고 나서 d를 출력해보면 10이 출력되고, 소수점 이하 0.5는 사라지게 됩니다. 이렇게 되는 이유는 double 자료형이 int 자료형보다 범위가 크기 때문입니다. 이 범위가 넓다라는 개념을 상속관계에도 적용해 보겠습니다.
앞의 클래스 상속 예제들에서 봤던 상속관계의 클래스 들인데 Parent 클래스를 상속받는 Child 클래스와 Phone 클래스를 상속받는 SmartPhone 클래스가 있었습니다. 부모(상위) 클래스는 자식(하위) 클래스보다 상위에 있습니다. 더 넓은 범위인 것이죠. 부모님은 자식보다 마음이 넓으신것처럼 클래스도 부모 클래스가 범위가 더 넓습니다.
이 상속관계에서의 범위를 잘 기억하고, 예제를 확인해보도록 하겠습니다. 앞에서 만들었던 예제파일과 클래스명이 겹치므로 패키지를 하위에 하나 더 만들어서 진행하겠습니다.
Parent.java
package example.poly;
public class Parent {
String name;
void walk() {
System.out.println("부모가 걷는다.");
}
void run() {
System.out.println("부모가 달린다.");
}
}
Child.java
package example.poly;
public class Child extends Parent {
String name;
// 재정의 메서드
void run() {
System.out.println("자식이 달린다.");
}
// 추가된 메서드
void eat() {
System.out.println("자식이 먹는다.");
}
}
PolySample.java
package example.poly;
public class PolySample {
public static void main(String[] args) {
Child c = new Child();
c.run();
// 부모클래스의 자료형으로 선언 (자동형변환)
Parent p = new Child();
p.run(); // 재정이된 메서드가 실행
// p.eat(); // 에러
}
}
실행 결과
자식이 달린다.
자식이 달린다.
부모(상위) 클래스는 Parent 클래스, 이 클래스를 상속받는 클래스는 Child 클래스이며, run() 메서드를 재정의하고 있습니다.
PolySample 클래스를 확인해보겠습니다. Child 클래스 객체를 생성해서 run() 메서드를 실행하고 있습니다. 앞에서도 배웠던 자식(하위) 클래스에서 부모클래스에게 물려받은 메서드를 그대로 사용하지 않고, 다시 정의해서 만든 재정의 메서드입니다.(오버라이딩) 그래서 출력결과는 정상적으로 "자식이 달린다" 라고 출력되었습니다.
아래 자료형(타입)은 Parent 인데 객체는 Child() 생성자를 통해 생성된 Child 객체입니다. Parent 클래스는 Child 클래스의 부모 클래스이기 때문에 더 넓은 범위의 자료형이라 형변환 코드 없이 자동으로 형변환 된 것을 알 수 있습니다. 이 p라는 객체는 Child 객체이지만, Parent 타입으로 변환된 것입니다. 이것이 바로 다형성이 적용된 것입니다. Child 객체는 Child 자료형이 될수도, Parent 자료형이 될 수가 있습니다. 이번엔 반대로 적용해보도록 하겠습니다.
package example.poly;
public class PolySample2 {
public static void main(String[] args) {
Parent p = new Child();
p.run();
// 자식클래스의 자료형으로 변환 (강제형변환)
Child c = (Child)p;
c.eat();
}
}
실행 결과
자식이 달린다.
자식이 먹는다.
PolySample2 클래스는 Parent 자료형의 p 변수에 Child 객체를 대입하고. run() 메서드를 실행하면 “자식이 달린다.”가 출력됩니다. 이 객체는 다시 자식클래스로 형변환이 가능한데, 이번엔 Parent 자료형을 Child 자료형으로 변환해야 합니다. 넓은 범위 자료형을 좁은 범위 자료형으로 변환되어야 하겠죠? 그래서 Child 로 강제형변환을 적용했습니다. Child 자료형이기 때문에 Child 클래스에만 존재하는 eat() 메서드를 사용할 수 있게 된것입니다.
그럼 왜 다형성이라는 개념이 필요할까요. 그냥 따로 자료형을 지정하면 될 것 같은데, 부모 클래스의 자료형으로 사용하는 것인지 잘 이해가 되지 않을 것입니다.
쉬운 예로, 조립 컴퓨터를 생각해볼까요? 여러 부품들이 모여 컴퓨터로 만들어지게 되는데, 메인보드가 있고, 여기에 그래픽카드를 꽂아 사용합니다. A사의 그래픽카드를 사용하다가 더 성능이 좋은 그래픽카드를 사용하려면 다른 회사의 그래픽카드를 사서 메인보드로 교체하면 됩니다. 다른 회사의 다른 모델의 그래픽카드라 하더라도 메인보드에 꽃기만 하면 됩니다.
만약 A사의 그래픽카드만 사용가능하다면 그래픽 기능을 업그레이드하기 위해 컴퓨터 부품 전체를 새로 구매해야 되는데, 우리는 그래픽카드라는 동일한 개념을 상위에 두고 A사의 그래픽 카드도, B사의 그래픽 카드도 메인보드 입장에서는 그냥 그래픽카드일 뿐입니다. 그래픽카드가 하는 일도 같고, 꽂는 위치도 같습니다. 이렇게 소프트웨어에서도 하드웨어 처럼 원하는 부품만 교체하듯이 개발할 수 있도록 만든 개념이 바로 객체 지향 프로그래밍이고, 이 개념을 적용하기 위해 상속, 오버라이딩, 다형성 개념을 이용하는 것입니다.
그래픽카드라는 부모클래스에 AMD, Nvidia라는 자식 클래스의 상속관계를 코드로 작성해보겠습니다.
GraphicCard.java
package example.poly;
public class GraphicCard {
int memory;
public void process() {
System.out.println("그래픽 처리");
}
}
Amd.java
package example.poly;
public class Amd extends GraphicCard {
public void process() {
System.out.println("AMD 그래픽 처리");
}
}
Nvidia.java
package example.poly;
public class Nvidia extends GraphicCard {
public void process() {
System.out.println("Nvidia 그래픽 처리");
}
// toString() 메서드 재정의(오버라이딩)
public String toString() {
return "Nvidia";
}
}
Computer.java
package example.poly;
public class Computer {
public static void main(String[] args) {
GraphicCard gc = new GraphicCard();
gc.process(); // 원래 그래픽카드 process
gc = new Amd();
gc.process();
gc = new Nvidia();
gc.process();
}
}
실행 결과
그래픽 처리
AMD 그래픽 처리
Nvidia 그래픽 처리
Amd 클래스, Nvidia 클래스는 모두 GraphicCard 클래스를 상속받고 있는 클래스입니다. Computer.java 에서 GraphicCard 객체를 생성해서 process() 메서드를 실행하는 코드인데, 이 GraphicCard 자료형의 gc 변수에 Amd 객체를 생성해서 대입하고, process() 메서드를 호출했다. 그 아래는 gc 변수에 Nvidia 객체를 생성해서 대입하고 process() 메서드를 호출하는 코드입니다.
실행결과는 두 메서드 모두 재정의한 메서드이기 때문에 자식 클래스의 메서드가 실행된 것을 알 수 있습니다. 지금 이 Computer 클래스 예제는 비교하기 위해 한꺼번에 출력했지만, 객체지향 프로그래밍의 핵심 개념인 부품 교체하듯이 코딩하게 되면,
GraphicCard gc = new GraphicCard();
gc.process();
이 코드를
GraphicCard gc = new Amd();
gc.process();
또는,
GraphicCard gc = new Nvidia();
gc.process();
형태로 변수의 타입은 그대로고, 객체 생성하는 연산자 부분만 업그레이드된 새로운 부품으로 교체하는것처럼 간단히 수정할 수 있게 됩니다.
뒤에서 배울 인터페이스 부분에서도 같은 개념으로 다시 나오게 되니, 어렵더라도 우선, 지금은 자료형의 범위와 자동형변환, 강제형변환에 대한 개념으로 정리하고 넘어가세요.
객체 변수에 다양한 타입의 값을 대입할 때도 다형성 개념을 적용하지만, 메서드의 매개변수에서도 다형성 개념을 많이 사용합니다. 메서드 입장에서는 다양한 자료형을 매개변수로 받기 위해서 메서드를 정의할 때 매개변수의 자료형을 상위 클래스 타입으로 지정하는 것입니다.
만약 Game이라는 클래스에서 display() 메서드가 앞에서 만들었던 GraphicCard 자료형의 변수를 매개변수로 받는다고 생각해보겠습니다.
Game.java
package example.poly;
public class Game {
void display(GraphicCard gc) {
gc.process();
}
}
만약 다른 클래스에서 이 display() 메서드를 호출하려면,
Game g = new Game();
GraphicCard gc = new GraphicCard();
g.display(gc);
이렇게 구현이 될 것입니다.
그럼, 만약 위 소스의 display() 메서드의 매개변수에 GraphicCard 타입 객체 말고 다른 타입의 객체도 넘겨주려면 어떻게 해야 할까요?
물론 Amd, Nvidia 객체를 모두 받을 수 있도록 display() 메서드를 오버로딩해도 되겠죠?
void display(GraphicCard gc) {
gc.process();
}
void display(Amd gc) {
gc.process();
}
void display(Nvidia gc) {
gc.process();
}
그러면, Game 클래스에서 GraphicCard, Amd, Nvidia 어떤 객체를 매개변수로 넘겨줘도 정상적으로 실행이 될 것입니다. 하지만, 다형성 개념을 이용하면 맨 위의 첫번째 매개변수가 GraphicCard 자료형인 메서드 하나만 있어도, 세가지 모두 처리할 수 있게 됩니다. 기존 GraphicCard 클래스와 Amd, Nvidia 클래스가 있는 example.poly 패키지 안에 Computer2.java 파일을 만들어보겠습니다.
Computer2.java
package example.poly;
public class Computer2 {
public static void main(String[] args) {
Game g = new Game();
GraphicCard gc = new GraphicCard();
g.display(gc);
Amd gc2 = new Amd();
g.display(gc2);
Nvidia gc3 = new Nvidia();
g.display(gc3);
}
}
실행 결과
그래픽 처리
AMD 그래픽 처리
Nvidia 그래픽 처리
모두 Game 클래스의 display() 메서드를 사용하고 있지만, GraphicCard 객체, Amd 객체, Nvidia 객체 어떤 객체가 와도 정상적으로 실행되며, 해당 객체의 process 메서드가 호출되고 있는 것을 알 수 있습니다.
이제 약간 감이 오시나요? 매개변수도 변수입니다. 이 변수가 선언할 때 Game 클래스에서 process 메서드의 매개변수가 GraphicCard 자료형으로 정의해놨기 때문에, 이 메서드를 호출할 때 매개변수는 GraphicCard 클래스의 하위 클래스들이 자동형변환이 일어난 것입니다.
GraphicCard gc = new Amd();
GraphicCard gc = new Nvidia();
결국, 이렇게 형변환이 일어난것이라 생각하면 됩니다.
만약 모든 클래스를 매개변수로 받고 싶으면, 메서드를 정의할 때 매개변수 자료형을 Object로 선언하면 됩니다.
package example.poly;
public class ObjectSample {
public static void main(String[] args) {
allObject(new GraphicCard());
allObject(new Amd());
allObject(new Nvidia());
allObject("안녕");
}
public static void allObject(Object obj) {
System.out.println(obj.toString());
}
}
실행 결과
example.poly.GraphicCard@7291c18f
example.poly.Amd@7cc355be
Nvidia
안녕
위 예제의 allObject() 메서드의 매개변수 자료형은 Object입니다. 매개변수로 GraphicCard, Amd, Nvidia 클래스의 객체도 모두 가능하고, 심지어 “안녕” 문자열도 가능합니다. 문자열은 String 클래스의 객체이기 때문입니다.
어떻게 이렇게 코딩이 가능할까요? 바로 Object는 모든 클래스의 최상위 클래스이기 때문입니다.