[JAVA] 다형성의 의미와 예시

Re_Go·2024년 7월 2일
0

JAVA

목록 보기
35/37
post-thumbnail
post-custom-banner

1. 다형성이란?

다형성이란 객체 지향 프로그래밍 중에서 중요한 개념들 중 하나로 상속 관계에 있는 클래스 간에 어느정도 자유로운 형변환을 지원함으로서 상황에 맞게 유동적인 타입 변환에 의한 멤버 사용이 가능한, 객체 지향의 특성과도 같은데요. 말만 들었을 때는 뭔지 모르시겠죠? 저도 처음에는 몰랐어요 ㅎ.... 아무튼 우리가 이걸 이해할려면 자동 타입 변환(업캐스팅)강제 타입 변환(다운 캐스팅)을 이해해야 하는데요. 이걸 이해하면 다형성을 좀 더 이해할 수 있으리라 생각됩니다.

구체적으로 말하자면 다형성(多形性) 이란 상황에 따라 이런 타입도 될 수 있고, 저런 타입도 될 수 있음을 의미할텐데요. 우리가 클래스의 인스턴스를 다른 타입으로 형 변환 할 경우에 기존에 가지고 있던 고유의 멤버는 사용을 하지 못합니다. 즉 그때마다, 상황에 따라 다시 형 변환을 고유한 멤버를 사용하는 것이라고 할 수 있겠습니다.

그럴려면 우선 아래의 코드 예시를 토대로 업캐스팅과 다운캐스팅에 대해서 알아보도록 할까요?

class Animal{
	public void move() {}
}

class Dog extends Animal{

	@Override
	public void move() {
		System.out.println("강아지가 움직입니다.");
	}
	
	public void run() {
		System.out.println("강아지가 달립니다.");
	}
}

2. 클래스 간 상속에서의 타입 관계

위의 예시로 설명해 보겠습니다. Animal 클래스의 인스턴스를 생성할텐데요. 근데 하나는Animal 클래스의 인스턴스로, 또 하나는 Dog 클래스의 인스턴스로 생성해 봅시다.

// Animal 클래스의 인스턴스 생성
Animal animal1 = new Animal();
// Dog 클래스의 인스턴스 생성
Dog dog = new Dog();

두 인스턴스의 타입을 알아보기 위해서는 instanceof 메서드로 확인을 해보면 되는데요. 그럼 instanceof로 두 인스턴스의 타입을 각각 확인해 보겠습니다.

System.out.println(animal1 instanceof Dog ); // false
System.out.println(animal1 instanceof Animal ); // true
System.out.println(dog instanceof Dog ); // true
System.out.println(dog instanceof Animal ); // true

Animal 인스턴스는 그렇다 치더라도 Dog 인스턴스는 Animal 클래스의 인스턴스라고 나오네요?
그렇습니다. 상속을 받은 클래스의 인스턴스를 생성할 때는 그 상속을 받은 클래스의 인스턴스가 되기도 한다는 사실...!

이유는 상속 받은 하위 클래스가 상위 클래스를 참조할 수 있으므로(공유 자원이나 오버라이딩 등) 타입 또한 하위 클래스이면서 상위 클래스로 지정되는 것이죠.

2. 자동 타입 변환 (업 케스팅)

위에서도 말씀 드렸다 싶이 자바에서는 하위 클래스가 상위 클래스를 상속 받을 때 타입도 자연스럽게 상위 클래스의 타입으로 지정되는데요.

위에서도 확인할 수 있었듯이 Animal 클래스를 상속 받는 동물들 클래스가 있다고 할 때 instanceof 연산자를 이용하여 비교를 해보면 두 관계는 true로 도출되는 걸 확인할 수 있었습니다. 그리고 여기서 실험을 하나 해보겠습니다.

Dog dog1 = new Dog();
Animal dog2 = new Dog();

둘 다 Dog의 인스턴스로 생성을 했는데 담은 변수의 타입이 각각 다르네요? 그럼 위 두 변수의 인스턴스 여부도 비교해 볼까요?

System.out.println(dog1 instanceof Dog ); // true
System.out.println(dog1 instanceof Animal ); // true
System.out.println(dog2 instanceof Dog ); // true
System.out.println(dog2 instanceof Animal ); // true

위의 코드에서도 확인할 수 있듯이 dog1와 dog2 둘 다 Dog 타입이면서 Animal 타입으로 취급되는데요. 그 중 dog2는 Dog 클래스의 인스턴스를 Animal 타입의 변수에 할당하는, 타입을 상위로 지정해 준 상황이고요. 그리고 우리는 이것을 자동 타입 변환(업 캐스팅) 이라고 부르는 것입니다.

업 캐스팅을 하는 조건은 상속 받은 직계 상위 클래스가 그 조건인데요. 해당 예제에서는 Animal을 Dog가 상속 받고 있으므로 Dog의 직계 상위 클래스인 Animal 클래스로 타입 변환이 가능했던 것이죠.

그럼 이 상태에서 Dog 클래스의 고유 메서드인 run을 실행시켜 볼까요?

dog1.run(); // 정상 실행
dog2.run(); // 오류 발생 : The method run() is undefined for the type Animal

오잉? 이상하네요? 분명 Dog 클래스에는 run 메서드가 작성되어 있는데 dog2에는 그 메서드를 불러올 수 없네요? 그렇습니다. 비록 dog1과 dog2의 타입은 Dog (instanceof로 비교)지만, 자동 타입 변환, 그러니까 하위 클래스의 인스턴스가 상위 클래스 타입으로 형변환을 일으킬 때는 본인의 타입이 가지고 있던 고유한 필드 및 메서드를 사용하지 못하다는 것입니다.

쉽게 말하면 부모님 집으로 가는데 내꺼는 다 놓고 와서 사용할 수 없는 상황인 것이죠.

3. 강제 타입 변환


그렇다면 위의 예시 처럼 자동 타입 변환 된 변수의 고유했던 메서드를 사용하지 못하느냐? 그건 또 아닙니다. 강제 타입 변환을 해주면 되는데요! 자바에서는 아래의 에러 예시와 같이 하위 클래스의 객체가 상위 클래스 타입으로의 변환은 가능하지만 상위 클래스의 객체가 하위 클래스로의 형 변환은 허용하지 않습니다.

Animal anotherAnimal = new Animal();
Dog anotherDog = (Dog) anotherAnimal; // 다운캐스팅 - ClassCastException 발생

그러나 다운 캐스팅을 허용하지 않는건 또 아닌게, 원래 있던 클래스 타입에서 바로 다운 캐스팅을 하는건 안되더라도, 업캐스팅을 했다가 다운 캐스팅을 하는 건 됩니다. 아래의 예제와 같이 말이죠!

Animal dog2 = new Dog();
Dog newDog = (Dog) dog2;

위의 Dog 인스턴스는 생성될 때 Animal 타입의 dog2 변수에 할당되었는데요. 이를 업 캐스팅이라고 부른다고 했죠? 근데 이 변수를 다시 Dog 타입으로 형 변환 한 뒤 Dog 타입의 newDog 변수에 할당해 주었습니다. 이것이 바로 위에서 아래로 내려온 상황, 다운 캐스팅 인 것이죠.

이러한 다운 캐스팅은 마치 타입스크립트의 타입 호환성 과도 연결되는 부분인데요. 물론 자바에서도 위와 같은 상황에서 상위 클래스 타입에서 하위 클래스 타입으로 역 타입 변환을 지원하는데, 이를 강제 타입 변환 이라고 부르는 것이죠.

근데 이렇게만 본다면 "아니 그냥 형 변환이고 자시고 할 것 없이 그냥 Dog 타입 인스턴스를 형변환 안하고 고유 멤버를 쓰면 되지 않냐?", 라고 생각하시며 자동 타입 변환이나 강제 타입 변환을 쓸 이유가 없을 것 같죠?

하지만 다형성 또한 객체 지향 언어의 주요한 강점 중 하나인 만큼, 이러한 다형성에 의한 형 변환도 쓸데 없이 나오진 않았겠죠? 아래의 예제 코드를 살펴 보겠습니다.

4. 업 케스팅 사용 예시 (쇼핑 예제)

물건 클래스

아래의 코드에서 물건 예제는 크게 세 가지(Product, Tv, Computer)인데요. Product 클래스는 물건의 고유한 이름과 가격, 이 필드에 대한 getter를 설정했고요. 나머지 Tv와 Computer는 Product를 상속만 받고 있는 상황이죠.

class Product{
	private String model; 
	private int price;
	
	public Product() {
	
	}
	
	public Product(String model, int price) {
		this.model = model;
		this.price = price;
	}

	public String getModel() {
		return model;
	}

	public int getPrice() {
		return price;
	}
	
}

class Tv extends Product{
	public Tv() {};
	public Tv(String model, int price) {
		super(model, price);
	};
}

class Computer extends Product{
	public Computer() {};
	public Computer(String model, int price) {
		super(model, price);
	};
}

손님 클래스

다음은 Customer(손님) 클래스 입니다. 필드는 각각 아래와 같이 구성되어 있는데 Product[] cart 배열 보이시죠? 길이를 10(개수 10개) 만큼 잡고, 여기에 각각 물건을 담을 겁니다.

그리고 buy(물건을 사는 행위), output (지금까지 산 물건을 출력하는 행위) 메서드를 지정해 주었는데요.

class Customer{
	private int money;
	private int startMoney;
	private Product[] cart = new Product[10];
	private int numOfProduct = 0;
	private int totalBuyMoney;

	public Customer() {
	}

	public Customer(int money) {
		this.money = money;
		this.startMoney = money;
	}
	
	public void buy(Product item) {
		if(this.money >= item.getPrice()) {
			this.cart[numOfProduct] = item;
			this.money -= item.getPrice();
			this.numOfProduct++;
		}else {
			System.out.println("소지 금액이 부족합니다.");
		}
	}
	
	public void output() {
		System.out.println("----------구매 상품----------");
		for(int i = 0; i < numOfProduct ; i++) {
		    System.out.println(
		            "구매 상품 : " + this.cart[i].getModel() + "원" +  "\n" +
		            "구매 금액 : " + this.cart[i].getPrice() + "원"
	        );
	        this.totalBuyMoney += this.cart[i].getPrice();
		}
		System.out.println(
				"총 구매 금액 : " + this.totalBuyMoney + "원" + "\n" + 
				"남은 금액: " + this.money + "원"
		);
	}
}

이때 buy 메서드를 보시면 무엇을 받기로 되어있을까요? 네. Product 타입의 변수를 받기로 되어있습니다. 그리고 buy 메서드를 사용할 때 넘겨주는 매게변수는 다음과 같이 지정할 수 있는데요.

// 손님 생성
Customer vip1 = new Customer(10000000);
// Tv 인스턴스를 넘겨준다.
vip1.buy(new Tv("올레드 TV", 2500000));

위의 buy 메서드로 넘겨주는 값은 Tv 인스턴스입니다. 즉 위의 내용을 풀어보자면 buy 매서드에 값을 할당하는 과정은 다음과 같이 풀어볼 수 있는데요.

Product item = new Tr("올레드 Tv", 2500000));

이거 어디서 많이 보셨죠? 네. 업 캐스팅입니다! 그럼 왜 이렇게 해야하냐? 우리가 생성한 Product 타입의 배열에 담을 때 타입은 Product 여야 하니까 타입을 Product로 설정한 것이죠.

그리고 buy 메서드의 다음 코드에서 처럼 각 클래스가 상속 받고 있는 Product 클래스의 멤버들을 사용하여 배열에 차곡 차곡 담아 활용할 수 있는 것이죠.

5. 다운 캐스팅 사용 예시

다운 캐스팅 예시 입니다. 코드를 보시면 대충 감이 오실텐데요. Animal을 상속 받고 있는 각 클래스들은 오버라이드 한 메서드와 고유한 메서드를 동시에 가지고 있는데요.

class Animal{
	public void move() {}
}
class Dog extends Animal{
	@Override
	public void move() {
		System.out.println("강아지 움직입니다.");
	}
  	public void run() {
		System.out.println("강아지가 달립니다.");
	}
}
class Dolphin extends Animal{
	@Override
	public void move() {
		System.out.println("돌고래 헤엄친다.");
	}
  	public void swim() {
		System.out.println("돌고래가 헤엄을 칩니다.");
	}
}
class Eagle extends Animal{
	@Override
	public void move() {
		System.out.println("독수리 움직이고");
	}
	public void fly() {
		System.out.println("독수리가 날아 오릅니다.");
	}
}

그 아래의 코드는 main에서 직접 실행하고 Animals 타입의 배열에 각 자식 인스턴스들을 담고 있는데요. 업 캐스팅이죠? 이때 for문을 돌려 오버라이드 받아 재정의 한 공통의 메서드인 move는 각각 출력하는게 가능했습니다.

public static void main(String[] args) {

		Animal[] animals = new Animal[3];

		// 업캐스팅 -> Animal animals = new Dog()와 같음
		animals[0] = new Dog();
		animals[1] = new Dolphin();
		animals[2] = new Eagle();

  		// for문을 돌려 공통으로 가지고 있는 move는 호출 가능
		for (int i = 0; i < animals.length ; i++) {
			animals[i].move();
		}

반면 자식이 고유하게 가지고 있는 메서드는 호출하는게 불가능 한데요.

// 부모 클래스는 fly() 메소드가 없기 때문에 호출 불가
	animals[2].fly();

이때의 해결 방법으로 필요한게 바로 다운 캐스팅인 것이죠!


		// 조건문을 붙여 animals 배열의 특정 인스턴스가 Eagle 인스턴스와 같다면 객체를 생성하고 다운캐스팅 해주거나, 그냥 객체 생성 없이 다운캐스팅을 해주는 코드
		if(animals[2] instanceof Eagle) {
			// 다운 캐스팅
			// 1. Eagle 객체 생성
			Eagle eagle = (Eagle)animals[2];
			eagle.fly();

			// 2. Eagle 객체 생성 없이 자원 사용
			((Eagle)animals[2]).fly();
		}
	}
profile
인생은 본인의 삶을 곱씹어보는 R과 타인의 삶을 배워 나아가는 L의 연속이다.
post-custom-banner

0개의 댓글