■ 다형성
객체지향 프로그래밍의 기둥이자 객체지향 프로그래밍에서 가장 중요하다 할 수 있느 다형성에 대해서 알아보자. 다형성이란 하나의 객체가 여러가지 형태를 가질 수 있는 것을 의미한다. 다형성은 영어에서 "여러 개"를 의미하는 poly 와 "어떤 형태 또는 실체" 를 이야기 하는 morphism 의 합성어로 "여러가지 형태" 라는 의미를 담고있다.
자바에서의 다형성은 한 타입의 참조변수로 여러 타입의 객체를 참조 할 수 있도록 만드는 것을 의미한다. 좀 더 구체적으로 이야기 하자면 상속과 비슷하게 상위클래스에 속성을 하위 클래스에서 참조하는 것을 허용하는 것이다. 한가지 유의할 점은, 참조 변수가 사용할 수 있는 맴버의 개수는 실제 객체의 멤버의 개수보다 같거나 적어야 한다는 점이다.
앞선 상속파트에서 상위 클래스를 상속받은 하위 상속자의 멤버 변수가 자연스럽게 늘어나는 것과 비슷하다고 생각하면 보다 이해가 쉬울 것 같다.
package polymorphism;
public class polymorphismTest {
public static void main(String[] args) {
friend friend = new friend(); //객체변수와 참조변수가 일치
boyfriend boyfriend = new boyfriend();
friend girlfriend = new girlfriend(); //객체변수와 참조변수 불일치.
friend.friendInfo();
boyfriend.friendInfo();
girlfriend.friendInfo();
}
}
class friend{
public void friendInfo(){
System.out.println("나는 당신의 친구입니다.");
}
}
class boyfriend extends friend{
public void friendInfo(){
System.out.println("나는 당신의 남자친구 입니다.");
}
}
class girlfriend extends friend{
public void friendInfo(){
System.out.println("나는 당신의 여자친구입니다.");
}
}
// 결과값
나는 당신의 친구입니다.
나는 당신의 남자친구 입니다.
나는 당신의 여자친구입니다.
위 코드를 함께 봐보자
앞서 나는 지금 이 다형성을 배우기 전까지 객체와 타입이 일치하는 타입의 참조변수를 통해서 생성된 객체를 사용해왔다. 위 코드를 살펴보면 friend 는 friend 참조변수를 boyfriend는 boyfriend 참조변수를 참초하며 객체와 참조변수가 일치하는 것을 볼 수 있다. 하지만 마지막 girlfriend는 girlfriend 클래스의 인스턴스를 생성하고 그것을 friend 타입의 참조변수 girlfriend에 할당하고 있다. 원래대로라면 타입을 일치시키기위해서 girlfriend 참조변수 타입으로 지정해 주어야하지만, 그러지않고 상위클래스 friend 참조변수 타입으로 지정해주고 있다. 이것이 앞서 말했던 "상위클래스의 참조변수로 하위클래스의 객체를 참조하는 것" 이것이 자바의 다형성의 핵심인 부분이라 할 수 있다.
여기서 한 가지 기억해야할 점은 상위클래스 참조변수에서 하위클래스 객체에 접근 할 수 있지만, 그 반대의 경우는 불가능 하다는 점이다.
다음 예시를 봐보자
위에 예시에서 확인할 수 있듯이 상위클래스 friend 타입에서는 하위클래스 girlfriend 타입을 참조하는 것은 가능하지만, 그 반대에 경우인
girlfriend 하위클래스 타입에서 상위클래스 friend 타입을 참조하는 것은 불가능 하다는 것을 알 수 있었다. 그 이유는 상위클래스인 friend의 멤버 변수보다 friend1이 사용할 수 있는 멤버변수의 개수가 더 많기 때문이다.
그렇다면 다형성을 사용함으로서 얻을 수 있는 이점에는 무엇이 있을까?
가장 기본적인 핵심은 코드의 중복을 줄여 보다 편리하고 효율성있는 코드를 작성하며 동시에 오류를 줄여준다는 점이다.
앞서 내가 서술했던 오버로딩과 오버라이딩도 이 다형성에서 온 기능이라고 할 수 있다. 같은 이름의 메서드를 덮어쓰고나 다양하게 사용함으로서 새로운 메서드를 만드는 시간을 단축시켜줄 수 있으며, 비슷한 보일러플레이트 코드(반복적으로 사용하는 비슷한 코드) 를 최소화 할 수 있기 때문이다.
■ 참조변수와 타입변환
기본자료형과 형변환처럼, 참조변수도 타입변환이 가능하다.
참조변수의 타입변환은 다르게 말하면 참조변수의 멤버변수의 개수를 수정하는것을 의미하는데, 이 타입변환은 다형성을 이해하기 위해서라면 반드시 짚고 넘어가야할 챕터라고 할 수 있다.
타입변환을 위해서는 다음과 같은 조건을 충족해야한다.
1. 서로 상속관계에 있는 상위클래스 -> 하위클래스에서만 변환이 가능하다.
2.하위클래스에서 상위클래스의 타입변환(업캐스팅)은 형변환 연산자()를 생략할 수 있다.
3. 반대로 상위클래스에서 하위클래스로 형변환은 반드시 형변환 연산자()를 표기해 주어야한다.
지금 나처럼 자바에 초보인 사람들은 업캐스팅과 다운캐스팅을 이해하기보단 서로 상속관계에 있는 클래스들 사이에서만 타입변환이 가능하다는 것을 인지하는 것이 더 중요하다고 생각한다. 큰틀을 이해하고 코드를 작성해보면서 에러를 고치는 과정에서 참조변수의 타입변환에 대하여 이해하는 것이 이 챕터를 효율적으로 이해하는 방법일 거라 필자는 생각한다.
package polymorphism;
public class typecastringTest {
Another another = new Another();
Vehicle vehicle = (Vehicle) another; //상위 클래스 Vehicle 타입으로 변환(생략가능)
// Vehicle vehicle = another; 상위클래스 타입변환에서의 () 생략
Another another1 = (Another) vehicle; //하위클래스에서의 상위클래스 타입변환(업캐스팅 괄호 생략불가)
// Feat feat = (Feat) another; // -> 상속관계가 아니므로 타입변환 불가 -> 에러발생
}
class Vehicle{
String model;
String color;
int wheels;
void StartEngine(){
System.out.println("시동걸기");
}
void accelerator(){
System.out.println("속도 올리기");
}
void brake(){
System.out.println("감속");
}
}
class Another extends Vehicle{
void anotherPerson(){
System.out.println("다른 사람 태우기");
}
}
class Feat extends Vehicle{
void feat(){
System.out.println("묘기 부리기");
}
}
위 예시에서 보면 먼저 Vehicle 클래스가 있고, 이로부터 각각 상속을 받아 만들어진 Another 와 Feat 클래스가 있다. 먼저 Another 클래스의 인스턴스 객체 another을 생성하고, 그 객체를 가르키는 참조변수 vehicle의 타입을 Vehicle로 지정하여 참조변수의 타입변환을 실시 하였다. 그 후 반대로 vehicle을 하위 클래스 타입인 Another 타입으로 변환하여 참조변수 Another1에 할당하였다.
이처럼 상속관계에 있는 클래스 간에 상호 타입변환은 자유롭게 수행될 수 있다. 다만 하위클래스에서 상위클래스로 타입캐스팅을 할 때 ()를 생략할 수 있는 반면, 상위클래스에서 하위클래스로 타입캐스팅을 할 때에는 ()를 생략 할 수 없다는 점을 유의해야한다.
한편, Another 클래스와 Feat 클래스는 서로 상속관계가 아니기 때문에 타입캐스팅을 할 경우 에러가 발생하는 것을 확인할 수 있다.
■ instanceof
instanceof 키워드는 앞서 서술한 참조변수의 타입 변환, 즉 캐스팅이 가능한 지 여부를 boolean 타입으로 확인할 수 있는 자바의 문법요소이다. 캐스팅 가능 여부를 판단하기 위해서는 두 가지, 즉 ‘객체를 어떤 생성자로 만들었는가’와 ‘클래스 사이에 상속관계가 존재하는가’를 판단해야 한다.
프로젝트의 규모가 커지고, 클래스가 많아지면 매번 이러한 정보를 확인하는 것이 어려워진다. 이를 해결하기 위해 자바는 instanceof라는 연산자를 제공한다.
참조변수 instanceof 타입
package polymorphism;
public class instanceofTest {
public static void main(String[] args) {
Animal animal = new Animal();
System.out.println(animal instanceof Object); //Object 클래스는 최상위 클래스이기 때문에 true
System.out.println(animal instanceof Tiger); // animal은 Animal을 참조변수로 가지고 있기 때문에 true;
System.out.println(animal instanceof lion ); // lion은 animal을 참조변수로 가지고 있지 않기 때문에 false;
Animal tiger = new Tiger();
System.out.println(tiger instanceof Object); //true
System.out.println(tiger instanceof Animal); //true
System.out.println(tiger instanceof Tiger); //true
System.out.println(tiger instanceof lion);// false;
}
}
class Animal{};
class Tiger extends Animal{};
class lion extends Animal{};
//결과값
true
false
false
true
true
true
false
위의 예시를 보면 Animal 클래스가 있고, Lion과 Tiger 클래스가 각각 Animal 클래스를 상속받고 있다. 그리고 각각 객체를 생성하여 Animal 타입의 참조변수에 넣고 instanceof 키워드를 사용하여 형변환 여부를 확인하고 있다. Tiger 객체를 예로 들어보면, 생성된 객체는 Animal 타입으로 선언되어있지만 다형적 표현 방법에 따라 Object와 Animal 타입으로도 선언될 수 있다는 점을 확인하실 수 있다. 이렇듯 소스 코드가 길어지는 등 일일이 생성 객체의 타입을 확인하기가 어려운 상황에서 instanceof는 매우 유용한 수단이 될 수 있다.
■ 다형성의 활용 예제
이제 자바 객체지향에 다향성에 대해서 예제를 보고 이해해보도록 하자.
class Coffee {
int price;
public Coffee(int price) {
this.price = price;
}
}
class Americano extends Coffee {};
class CaffeLatte extends Coffee {};
class Customer {
int money = 50000;
}
여기 손님이 카페에 방문하여 커피를 소비하는 시나리오를 예로 들어보자. 이 예제에는 4개의 클래스가 있다. 먼저, 커피의 가격 정보를 담고있는 Coffee 클래스가 있고, 이를 상속받는 Americano 클래스와 CaffeLatte 클래스가 아직 구현부가 작성되지 않은 상태로 존재한다. 마지막으로 Customer 클래스는 커피를 구매하는 손님을 의미하며, 기본적으로 5만원의 돈을 가지고 있다고 가정해보자.
다음으로 이 5만원의 돈을 가지고 아메리카노 한 잔과 카페라떼 한 잔을 구입하는 경우를 생각해보면, 어떻게 할 수 있을까? 아마 다음과 같은 기능의 메서드를 Customer 클래스에 추가해볼 수 있을 것 같다.
void buyCoffee(Americano americano) { // 아메리카노 구입
money = money - americano.price;
}
void buyCoffee(CaffeLatte caffeLatte) { // 카페라테 구입
money = money - caffeLatte.price;
}
사야하는 커피가 무엇인지 구분하기 위해서는 매개변수로 커피에 대한 정보를 전달 받아야하기 때문에 매개변수로 각각 Americano 타입과 CaffeLatte 타입의 객체를 전달해주었다.
그런데 혹시 좀 불편하게 느껴지지 않는가? 만약에 손님이 구입해야하는 커피의 종류가 한 두개가 아니라 수 십 수 백 가지가 된다면 그 경우에는 매번 새로운 타입을 매개변수로 전달해주는 buyCoffee 메서드를 계속 추가해주어야 할 것이다. 이런 경우 앞서 배웠던 객체의 다형성을 활용하여 아래와 같이 문제를 해결할 수 있다.
void buyCoffee(Coffee coffee) { // 매개변수의 다형성
money = money - coffee.price;
}
즉 다형성이 가지는 특성에 따라 매개변수로 각각의 개별적인 커피의 타입이 아니라 상위클래스인 Coffee의 타입을 매개변수로 전달받으면, 그 하위클래스 타입의 참조변수면 어느 것이나 매개변수로 전달될 수 있고 이에 따라 매번 다른 타입의 참조변수를 매개변수로 전달해주어야하는 번거로움을 훨씬 줄일 수 있다.
이제 전체적인 코드를 보면서 다시 흐름을 이해해 보자.
package polymorphism;
public class carBuyTest {
public static void main(String[] args) {
Buy buy = new Buy();
buy.BuyCar(new Porsche());
buy.BuyCar(new Jeep());
}
}
class Car{
int price;
Car(int price){
this.price = price;
}
}
class Porsche extends Car{
public Porsche(){
super(5);
}
}
class Jeep extends Car{
public Jeep(){
super(3);
}
}
class Buy{
int money = 10;
void BuyCar(Car car){
if(money < car.price){
System.out.println("돈이 부족합니다.");
return;
}else{
System.out.println(car + "를 구매하였습니다.");
}
money = money - car.price;
System.out.println(money + "억 남았습니다.");
}
}
위의 코드 예제에서 앞서 설명드린 내용은 제외하고 실제 객체를 생성하여 포르쉐와 지프를 구입하는 코드만 한번 간단히 살펴보도록 하겠다
앞서 우리는 객체지향 설계의 다형성을 활용하여 buyCar() 메서드의 매개변수로 Car 타입을 전달해주었습니다. 이제 객체를 생성하고 참조변수를 사용할 때 Car 클래스를 상속받기만 하면 buyCar(Car car) 메서드의 매개변수로 전달할 수 있다.
하나의 간단한 예시이지만 이렇게 자바의 다형성을 잘 활용하면 많은 중복되는 코드를 줄이고 보다 편리하게 코드를 작성하는 것이 가능해진다. 이 외에도 여러 종류의 객체를 배열로 다룰 수 있는 등 다형성은 객체지향 프로그래밍의 매우 강력한 강점이라 할 수 있다.
'Programming > Cs' 카테고리의 다른 글
[Git] 깃허브 명령어 정리 (0) | 2022.09.17 |
---|---|
[OOP] 추상화 (0) | 2022.05.15 |
[지식] this vs this() (1) | 2022.05.11 |
[지식] 클래스 (Class) 와 객체 (Object) (2) | 2022.05.10 |
[OOP] 객체지향 프로그래밍 (OOP, Object oriented Programming) (2) | 2022.05.10 |