
이 내용은 Backend / OOP logic in Java 영역에서 다형성(Polymorphism)을 실제 코드 구조로 구현할 때 반드시 필요한 개념이다. 참조 자료형 캐스팅은 “객체 자체를 바꾸는 것”이 아니라, 같은 객체를 바라보는 참조 변수의 타입을 전환하는 작업이다.
Java의 자료형은 크게 두 가지로 구분된다.
기본 자료형에서 int → double처럼 바꾸는 것은 형변환(Conversion)이라고 부른다. 반면, 참조 자료형에서 객체의 타입 관점을 바꾸는 것은 캐스팅(Casting)이라고 부른다.
참조 자료형 캐스팅이란 객체의 타입을 전환하는 것처럼 보이지만, 실제로는 “참조 변수의 타입”을 바꾸는 작업이다. 객체는 그대로 존재하고, 어떤 타입으로 바라보느냐만 달라진다.
1) Upcasting (업캐스팅) 하위 클래스 타입의 객체를 상위 클래스 타입으로 변환한다. 이 과정은 암시적으로 이루어진다.
2) Downcasting (다운캐스팅) 상위 클래스 타입으로 참조하던 객체를 다시 하위 클래스 타입으로 변환한다. 이 과정은 반드시 명시적으로 작성해야 한다.
다운캐스팅은 잘못 사용하면 런타임 오류(ClassCastException)가 발생한다. 이를 방지하기 위해 사용하는 연산자가 instanceof이다.
objectA instanceof ClassA // true / false
objectA가 ClassA 타입이거나, ClassA의 하위 클래스 인스턴스라면 true, 아니면 false가 반환된다. 조건문에서 다운캐스팅 가능 여부를 검사할 때 사용한다.
업캐스팅은 다형성 구현의 핵심이다. 자식 객체를 부모 타입으로 묶어 관리할 수 있고, 오버라이딩된 메서드는 실행 시점에 자식 클래스 버전이 호출된다.
Animal animal1 = new Dog(); // Upcasting
animal1.makeSound(); // Dog의 재정의된 메서드 호출
// animal1.fetch(); // 불가능 (부모 타입에는 없음)
업캐스팅의 특징:
추가 포인트: 업캐스팅 상태에서 메서드가 “부모 버전”이 아니라 “자식 버전”으로 실행되는 이유는, 참조 변수 타입이 아니라 실제 객체 타입을 기준으로 오버라이딩 메서드를 동적 바인딩하기 때문이다.
다운캐스팅은 업캐스팅된 객체를 다시 자식 타입으로 되돌려 자식 클래스의 고유 기능을 사용하기 위해 필요하다.
Animal animal2 = new Dog(); // Upcasting
Dog dog2 = (Dog) animal2; // Downcasting
dog2.makeSound();
dog2.fetch(); // 자식 고유 메서드 사용 가능
주의 사항:
if (animal2 instanceof Dog) {
Dog dog3 = (Dog) animal2;
dog3.fetch();
}
public class Animal {
public void makeSound() {
System.out.println("동물이 소리를 냅니다.");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("개가 짖습니다.");
}
public void fetch() {
System.out.println("강아지가 공을 물어옵니다.");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("고양이가 웁니다.");
}
public void udadada() {
System.out.println("고양이가 우다다다 거립니다.");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Dog(); // Upcasting
animal.makeSound(); // "개가 짖습니다."
if (animal instanceof Dog) { // 안전 검사
Dog dog = (Dog) animal; // Downcasting
dog.fetch();
}
}
}
핵심 포인트:
이번에는 인터페이스를 활용한 구조에서 업캐스팅과 다운캐스팅이 어떻게 쓰이는지 확인한다.
public interface Power {
void on();
void off();
}
컨트롤러는 구체 클래스가 아닌 인터페이스 타입(Power)에만 의존한다. 즉, Power 타입으로 업캐스팅된 상태로 공통 기능(on/off)을 처리할 수 있다.
추가로, 장치별 고유 기능(changeMode, compute 등)을 실행하려면 instanceof로 검증 후 다운캐스팅이 필요하다.
import java.util.Scanner;
public class CentralControl {
private final Power[] deviceArray;
private final Scanner scanner = new Scanner(System.in);
public CentralControl(Power[] deviceArray) {
this.deviceArray = deviceArray;
}
public void addDevice(Power device) {
int emptyIndex = checkEmpty();
if (emptyIndex == -1) {
System.out.println("더 이상 장치를 연결할 수 없습니다.");
return;
}
deviceArray[emptyIndex] = device;
System.out.println(device.getClass().getSimpleName() + " 장치가 추가되었습니다.");
}
private int checkEmpty() {
for (int i = 0; i < deviceArray.length; i++) {
if (deviceArray[i] == null) return i;
}
return -1;
}
public void powerOn() {
for (int i = 0; i < deviceArray.length; i++) {
if (deviceArray[i] == null) {
System.out.println("슬롯 [ " + (i + 1) + " ] : 장치가 없어 전원을 켤 수 없습니다.");
continue;
}
deviceArray[i].on();
}
}
public void powerOff() {
for (int i = 0; i < deviceArray.length; i++) {
if (deviceArray[i] == null) {
System.out.println("슬롯 [ " + (i + 1) + " ] : 장치가 없어 전원을 끌 수 없습니다.");
continue;
}
deviceArray[i].off();
}
}
public void showInfo() {
for (int i = 0; i < deviceArray.length; i++) {
if (deviceArray[i] == null) {
System.out.println("슬롯 [ " + (i + 1) + " ] 번 : Empty");
} else {
System.out.println("슬롯 [ " + (i + 1) + " ] 번 : " + deviceArray[i].getClass().getSimpleName());
}
}
}
public void showInfo2() {
int slot = 0;
for (Power device : deviceArray) {
slot++;
if (device == null) {
System.out.println("슬롯 [ " + slot + " ] 번 : Empty");
} else {
System.out.println("슬롯 [ " + slot + " ] 번 : " + device.getClass().getSimpleName());
}
}
}
public void performSpecificMethod() {
for (int i = 0; i < deviceArray.length; i++) {
Power device = deviceArray[i];
if (device == null) {
System.out.println("슬롯 [ " + (i + 1) + " ] : 연결되어 있지 않습니다.");
continue;
}
if (device instanceof AirConditioner) {
((AirConditioner) device).changeMode();
} else if (device instanceof Computer) {
((Computer) device).compute();
} else if (device instanceof LED) {
((LED) device).changeColor();
} else if (device instanceof Mouse) {
((Mouse) device).leftClick();
} else if (device instanceof Printer) {
((Printer) device).print();
} else if (device instanceof Speaker) {
((Speaker) device).changeEqual();
} else {
System.out.println("슬롯 [ " + (i + 1) + " ] : 아직 지원되지 않는 기기입니다.");
}
}
}
public void deleteDevice1() {
System.out.print("삭제할 슬롯 번호 입력(1~" + deviceArray.length + "): ");
int slot = scanner.nextInt();
if (slot < 1 || slot > deviceArray.length) {
System.out.println("범위를 벗어난 입력입니다.");
return;
}
int idx = slot - 1;
if (deviceArray[idx] == null) {
System.out.println("해당 슬롯에 기기가 없습니다.");
return;
}
System.out.println(deviceArray[idx].getClass().getSimpleName() + " 기기가 삭제되었습니다.");
deviceArray[idx] = null;
}
public void deleteDevice2() {
System.out.print("삭제할 기기 이름 입력: ");
String deviceName = scanner.next();
for (int i = 0; i < deviceArray.length; i++) {
if (deviceArray[i] == null) continue;
if (deviceName.equals(deviceArray[i].getClass().getSimpleName())) {
deviceArray[i] = null;
System.out.println(deviceName + " 기기가 삭제되었습니다.");
return;
}
}
System.out.println("해당 기기가 없습니다.");
}
}
여기서의 구조:
장점
단점
참조 자료형 캐스팅은 객체 지향에서 다형성을 실질적으로 구현하기 위한 핵심 메커니즘이다.
업캐스팅은 “여러 객체를 하나의 타입으로 묶어 관리”하는 구조를 만들고, 다운캐스팅은 “필요할 때만 하위 클래스의 고유 기능을 꺼내 쓰는 도구”이다.
즉,
그리고 안전한 다운캐스팅을 위해 instanceof는 필수이다. 이 흐름을 이해하면, 이후 인터페이스 기반 설계, DI, Spring 구조까지 자연스럽게 연결된다.
추가 확장 포인트: 현재 performSpecificMethod는 if-else 체인으로 타입을 분기한다. 장치가 많아질수록 분기문이 길어지므로, 다음 단계에서는 “기기별 공통 인터페이스에 고유 동작 메서드를 추가”하거나, “Command 패턴”처럼 구조를 바꾸는 방식으로 확장할 수 있다.