Q. Animal 클래스를 상속받아 만든 Bird 클래스가 있다.
move()는 Animal과 Bird 모두 갖고있고, fly()는 Bird만 갖고있다.
코드 1은 동작하고, 코드 2는 동작하지 않는데 언제 이 형태를 사용하는게 좋을까?
어떤 방식으로 사용해야 할까?
// 코드 1
Bird bird1 = new Bird();
bird1.fly();
// 코드 2
Animal bird2 = new Bird();
bird2.fly();
Bird bird1 = new Bird(); 의 경우 한눈에 이해가 된다.
하지만 Animal bird2 = new Bird();의 경우, 왜 이러한 형태로 사용하는지 의문이 들었다.
bird2.fly()는 컴파일 오류가 발생한다.
그 이유는 참조 변수의 타입에 있다.
참조 변수의 타입이 접근할 수 있는 멤버를 결정한다.
자바는 객체의 실제 타입(Bird)이 아닌, 참조 변수의 타입(Animal)에 따라 접근 가능한 필드, 메서드가 결정된다.
bird2는 Bird 객체를 가리키지만, Animal이 정의한 것만 사용할 수 있다.
Bird에서 정의한 것을 사용하는 방법 중 하나로 다운캐스팅을 사용할 수 있다.
업캐스팅 : 자식 클래스가 부모 클래스 타입으로 캐스팅 되는 것.
Child에만 있는 속성과 메서드는 실행하지 못한다. (단, 오버라이딩 된 메서는 가능)
Parent p = new Child(); // 업캐스팅
Child c = (Child) p; // 다운캐스팅
다운캐스팅 : 업캐스팅 되었던 객체의 자료형을 다시 Child로 바꾸는 것.
업캐스팅된 Child를 복구하여, 본인의 필드와 기능 회복하기 위해 사용하는 것이다.
예시의 경우 다음과 같이 instanceof와 다운캐스팅을 사용해 fly()를 사용할 수 있다.
단 bird2가 Bird 객체라는 확신이 있어야 한다. (ClassCastException )
Animal bird2 = new Bird();
...
if (bird2 instanceof Bird) {
((Bird) bird2).fly();
}
바람직한 설계는 아니다. 확장성 Bad.
fly(행동)를 인터페이스로 분리하거나, 전략 패턴으로 분리된 행동 클래스 주입하는 방식이 더 좋은 설계!!
또한 다음의 경우는 다운캐스팅 시 런타임 오류(ClassCastException)가 발생한다.
Parent p1 = new Child();
Child c1 = (Child) p1; // OK
c1.childMethod(); // OK

Parent p2 = new Parent();
Child c2 = (Child) p2; // ClassCastException
c2.childMethod(); // X

이처럼 업캐스팅은 Child의 속성을 사용할 수 없고, 다운캐스팅은 컴파일 오류의 위험이 있다.
그런데 왜 업캐스팅과 다운캐스팅을 사용하는 것일까??
(1) 공통 동작 일관성 처리
공통적으로 할 수 있는 부분을 만들어 간단하게 다루기 위해서이다.
하나의 타입으로 묶어서 사용하려고!
의도적으로 사용해야 하는 경우가 다수 존재한다.
상위 타입의 참조 이유 : 다형성의 핵심 이유
List<Animal> animals = List.of(new Bird(), new Dog(), new Cat());
for (Animal animal : animals) {
animal.move(); // 모든 Animal은 move()를 갖고 있으므로 가능
}
(2) 인터페이스 기반 프로그래밍
Runnable r = new Thread(() -> System.out.println("Run"));
Runnable은 인터페이스, Thread는 구현체.
실행 객체 일반화해서 관리 가능.
유연한 코드 구조, DI에서도 필수적.
(3) 디자인 패턴 활용 시
Animal animal = animalFactory.create("bird");
animal.move();
클라이언트는 실제 생석 객체 알 필요 없고, Animal이라는 인터페이스만 알고 있으면 됨!! (추상화)
다운캐스팅 없이 유연성, 타입 안정성을 유지하려면, 올바른 인터페이스 추상화가 핵심이다.
instaceof와 다운캐스팅을 사용하지 말고, 필요 기능을 인터페이스로 분리하자.
interface Flyable {void fly();}
-> 상속 계층보다 '역할(behavior)' 중시의 분리가 더 중요하다는 설계 철학을 갖자.
상위 타입 고정(업캐스팅)은 공통 기능 사용할 때 문제가 없다.
하지만, 다운캐스팅은 지양하자. 특정 하위 기능 사용시에는 인터페이스, 디자인 패턴 구조를 사용하자.
클래스보다 '행동 중심'으로 인터페이스를 추상화하자.
참고