📌 Goal
- 자바 객체지향 프로그래밍에서 다형성이 가지는 의미와 장점을 이해할 수 있다.
- 참조변수의 타입 변환에 대한 내용을 이해하고, 업캐스팅과 다운캐스팅의 차이를 설명할 수 있다.
- instanceof 연산자를 언제 어떻게 활용할 수 있는 지 이해하고 설명할 수 있다.
- 코딩 예제를 실제로 입력해보면서 다형성이 실제로 어떻게 활용되는 지 이해할 수 있다.
📌 다형성(Palymorphism)
하나의 객체가 여러 가지 형태를 가질 수 있는 성질을 의미
자바 프로그래밍에서 다형성은 한 타입의 참조변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것을 의미한다. 구체적으로 말하자면, 상위 클래스 타입의 참조변수를 통해서 하위 클래스의 객체를 참조할 수 있도록 허용한 것이라고 말할 수 있다.
아래 예시를 살펴보자.
//참조변수의 다형성 예시
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("나는 당신의 여자친구입니다.");
}
}
public class FriendTest {
public static void main(String[] args) {
Friend friend = new Friend(); // 객체 타입과 참조변수 타입의 일치
BoyFriend boyfriend = new BoyFriend();
Friend girlfriend = new GirlFriend(); // 객체 타입과 참조변수 타입의 불일치
friend.friendInfo();
boyfriend.friendInfo();
girlfriend.friendInfo();
}
}
// 출력값
나는 당신의 친구입니다.
나는 당신의 남자친구입니다.
나는 당신의 여자친구입니다.
위의 예시에서 참조변수 friend와 boyfriend 모두 각각 Friend와 BoyFriend라는 타입과 일치하는 참조변수 타입을 사용하는 것을 확인할 수 있다. 하지만 그 다음 라인을 확인해보면 GirlFriend클래스의 인스턴스를 생성하고 그것을 Friend타입의 참조변수 girlfriend에 할당하고 있다. 원래라면 타입을 일치시키기 위해 GirlFriend를 참조변수의 타입으로 지정해주어야 하지만, 그러지 않고 상위 클래스 Friend를 타입으로 지정해주고 있다.
이 경우, 상위 클래스를 참조변수의 타입으로 지정했기 때문에 자연스럽게 참조변수가 사용할 수 있는 멤버의 개수는 상위 클래스의 멤버의 수가 된다. 이것이 앞서 설명했던 "상위 클래스 타입의 참조변수로 하위 클래스의 객체를 참조하는 것"이자 다형성의 핵심적인 부분이라 할 수 있다.
❗️하위 클래스 타입의 객체가 상위 클래스 타입의 클래스 객체를 참조하는 것은 불가능하다.
아래의 예시를 살펴보자.
public class FriendTest {
public static void main(String[] args) {
// 객체 타입과 참조변수 타입의 일치 -> 가능
Friend friend = new Friend();
BoyFriend boyfriend = new BoyFriend();
// 객체 타입과 참조변수 타입의 불일치 -> 가능
Friend girlfriend = new GirlFriend();
// GirlFriend friend1 = new Friend(); -> 하위클래스 타입으로 상위클래스 객체 참조 -> 불가능
friend.friendInfo();
boyfriend.friendInfo();
girlfriend.friendInfo();
}
}
예시에서 확인할 수 있듯이 상위 클래스인 Friend타입으로 하위 클래스 GirlFriend를 참조하는 것은 가능하지만, 그 반대로 하위 클래스 GirlFriend의 타입으로 상위 클래스 객체 Friend를 참조하는 것은 불가능하다. 그 이유는 실제 객체인 Friend의 멤버 개수보다 참조변수 friend1이 사용할 수 있는 멤버 개수가 더 많기 때문이다.
좀 더 구체적으로 설명하면, 실제 참조하고 있는 인스턴스의 멤버를 기준으로 참조 변수의 타입의 멤버가 실제 인스턴스의 멤버 수보다 작은 것은 실제 사용할 수 있는 기능을 줄이는 것이기에 허용되지만, 그 반대의 경우는 참조하고 있는 인스턴스에 실제로 구현된 기능이 없어 사용이 불가하기 때문이다.
메서드 오버라이딩과 메서드 오버로딩 또한 다형성의 한 예시라 할 수 있다.
메서드 오버라이딩과 메서드 오버로딩이 둘 다 같은 이름의 메서드를 재사용 또는 덮어쓰기로 다르게 사용한다는 점을 기억할 때, 앞서 "하나의 객체가 여러 가지 형태를 가질 수 있는 성질"라 정의할 수 있었던 다형성의 의미와 궤를 같이하는 것이라 이해할 수 있다.
📌 참조변수의 타입 변환
사용할 수 있는 멤버의 개수를 조절하는 것을 의미
✔️ 타입 변환을 위한 세가지 조건
- 서로 상속관계에 있는 상위 클래스 - 하위 클래스 사이에만 타입 변환이 가능하다.
- 하위 클래스 타입에서 상위 클래스 타입으로의 타입 변환(업캐스팅)은 형변환 연산자(괄호)를 생략할 수 있다.
- 반대로 상위 클래스에서 하위 클래스 타입으로 변환(다운캐스팅)은 형변환 연산자(괄호)를 반드시 명시해야 한다.
아래 예제를 살펴보자.
public class VehicleTest {
public static void main(String[] args) {
Car car = new Car();
Vehicle vehicle = (Vehicle) car; // 상위 클래스 Vehicle 타입으로 변환(생략 가능)
Car car2 = (Car) vehicle; // 하위 클래스 Car타입으로 변환(생략 불가능)
MotorBike motorBike = (MotorBike) car; // 상속관계가 아니므로 타입 변환 불가 -> 에러발생
}
}
class Vehicle {
String model;
String color;
int wheels;
void startEngine() {
System.out.println("시동 걸기");
}
void accelerate() {
System.out.println("속도 올리기");
}
void brake() {
System.out.println("브레이크!");
}
}
class Car extends Vehicle {
void giveRide() {
System.out.println("다른 사람 태우기");
}
}
class MotorBike extends Vehicle {
void performance() {
System.out.println("묘기 부리기");
}
}
위의 예시를 보면 먼저 Vehicle클래스가 있고, 이로부터 각각 상속을 받아 만들어진 Car와 MotorBike클래스가 있다. 먼저 Car클래스의 인스턴스 객체 car를 생성하고, 그 객체를 가리키는 참조변수 vehicle의 타입을 Vehicle로 지정하여 참조변수의 타입변환을 실행했다. 그 후 반대로 vehicle를 하위 클래스 타입인 Car 로 타입 변환하여 참조변수 car2에 할당했다.
이처럼 상속관계에 있는 클래스 간에는 상호 타입변환이 자유롭게 수행될 수 있다. 다만 하위 클래스를 상위 클래스 타입으로 변환하는 경우 타입 변환 연산자(괄호)를 생략할 수 있는 반면, 그 반대의 경우는 타입 변환 연산자를 생략할 수 없다는 점에서만 차이가 있다
한편, Car클래스와 MotorBike클래스는 상속관계가 아니므로 타입 변환이 불가하여 에러가 발생하는 것을 확인할 수 있다.
📌 instanceof 연산자
참조변수의 타입 변환, 즉 캐스팅이 가능한 지 여부를 boolean 타입으로 확인할 수 있는 자바의 문법요소
캐스팅 가능 여부를 판단하기 위해서는 두 가지, 즉 ‘객체를 어떤 생성자로 만들었는가’와 ‘클래스 사이에 상속관계가 존재하는가’를 판단해야한다. 프로젝트 규모가 커지고, 클래스가 많아지면 매번 이러한 정보를 확인하는 것이 어려워진다. 이를 해결하기 위해 자바는 instanceof라는 연산자를 제공한다.
참조_변수 instanceof 타입
만약 참조_변수 instanceof 타입을 입력했을 때 리턴 값이 true가 나오면 참조 변수가 검사한 타입으로 타입 변환이 가능하며, 반대로 false가 나오는 경우에는 타입 변환이 불가능하다.
만약에 참조 변수가 null인 경우에는 false를 반환한다. 구체적인 예시를 살펴보자.
public class InstanceOfExample {
public static void main(String[] args) {
Animal animal = new Animal();
System.out.println(animal instanceof Object); //true
System.out.println(animal instanceof Animal); //true
System.out.println(animal instanceof Bat); //false
Animal cat = new Cat();
System.out.println(cat instanceof Object); //true
System.out.println(cat instanceof Animal); //true
System.out.println(cat instanceof Cat); //true
System.out.println(cat instanceof Bat); //false
}
}
class Animal {};
class Bat extends Animal{};
class Cat extends Animal{};
위의 예시를 보면 Animal클래스가 있고, Bat과 Cat클래스가 각각 Animal클래스를 상속받고 있다. 그리고 각각 객체를 생성하여 Animal타입의 참조변수에 넣고 instanceof키워드를 사용하여 형변환 여부를 확인한다. Cat객체를 예로 들어보면, 생성된 객체는 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;
}
먼저, 커피의 가격 정보를 담고있는 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 package2;
public class PolymorphismEx {
public static void main(String[] args) {
Customer customer = new Customer();
customer.buyCoffee(new Americano());
customer.buyCoffee(new CaffeLatte());
System.out.println("현재 잔액은 " + customer.money + "원 입니다.");
}
}
class Coffee {
int price;
public Coffee(int price) {
this.price = price;
}
}
class Americano extends Coffee {
public Americano() {
super(4000); // 상위 클래스 Coffee의 생성자를 호출
}
public String toString() {return "아메리카노";}; //Object클래스 toString()메서드 오버라이딩
};
class CaffeLatte extends Coffee {
public CaffeLatte() {
super(5000);
}
public String toString() {return "카페라떼";};
};
class Customer {
int money = 50000;
void buyCoffee(Coffee coffee) {
if (money < coffee.price) { // 물건 가격보다 돈이 없는 경우
System.out.println("잔액이 부족합니다.");
return;
}
money = money - coffee.price; // 가진 돈 - 커피 가격
System.out.println(coffee + "를 구입했습니다.");
}
}
// 출력값
아메리카노를 구입했습니다.
카페라떼를 구입했습니다.
현재 잔액은 41000원 입니다.
위의 코드 예제에서 앞서 설명드린 내용은 제외하고 실제 객체를 생성하여 아메리카노와 카페라떼 한 잔을 구입하는 코드만 한번 간단히 살펴보자. 앞서 객체지향 설계의 다형성을 활용하여 buyCoffee()메서드의 매개변수로 Coffee타입을 전달해주었다. 이제 객체를 생성하고 참조변수를 사용할 때 Coffee클래스를 상속받기만 하면 buyCoffee(Coffee coffee)메서드의 매개변수로 전달할 수 있다.
이렇게 자바의 다형성을 잘 활용하면 많은 중복되는 코드를 줄이고 보다 편리하게 코드를 작성하는 것이 가능하다. 이 외에도 여러 종류의 객체를 배열로 다룰 수 있는 등 다형성은 객체지향 프로그래밍에서 없어서는 안되는 매우 중요한 역할을 담당한다.
댓글