반복되는 instanceof 검사를 없애고, 확장 가능한 코드를 만드는 법
Phase 1: 리모컨이 너무 많아 (Bad Code)
최신 스마트 홈 시스템을 개발한다고 가정해보자. TV, 에어컨, 공기청정기 등 수많은 가전제품(Device)을 한 번에 제어해야 한다. 하지만 다형성을 모르는 개발자는 제품마다 제각각인 메서드 이름을 사용한다.
// 규격이 없는 혼란스러운 상태
class TV {
void turnOnTV() { System.out.println("TV 켜짐"); }
}
class AirCon {
void startCooling() { System.out.println("에어컨 시원해짐"); }
}
이렇게 되면, 안방에 있는 모든 기계를 켜려고 할 때 지옥의 조건문 파티가 열린다.
// Bad Code: 제품이 늘어날 때마다 코드를 수정해야 한다 (OCP 위반)
Object[] devices = { new TV(), new AirCon() };
for (Object device : devices) {
if (device instanceof TV) {
// 일일이 검사하고(instanceof), 강제로 변신시켜서(Casting) 실행
((TV)device).turnOnTV();
} else if (device instanceof AirCon) {
((AirCon)device).startCooling();
}
}
문제점: 만약 '로봇청소기'를 새로 샀다면? 메인 코드를 또 열어서 `else if`를 추가해야 한다. 유지보수가 불가능한 구조다.
Phase 2: 만능 리모컨 만들기 (Solution)
해결책은 간단하다. 제조사들에게 "전원 켜는 기능은 무조건 `powerOn()`으로 통일해!"라고 강제하는 것이다. 이때 사용하는 것이 바로 추상 클래스(Abstract Class)다.
// "모든 기계는 powerOn 기능을 가져야 한다"는 법(Rule)을 제정
abstract class SmartDevice {
// 내용은 자식이 채워라 (미완성 설계도)
abstract void powerOn();
}
class TV extends SmartDevice {
@Override
void powerOn() { System.out.println("TV 켜짐"); } // 부모의 법을 따름
}
class AirCon extends SmartDevice {
@Override
void powerOn() { System.out.println("에어컨 시원해짐"); }
}
이제 메인 코드는 더 이상 제품이 TV인지 에어컨인지 궁금해할 필요가 없다. 그냥 "기계니까 켜져라!" 한 마디면 된다.
// Good Code: 조건문, 캐스팅 모두 사라짐
SmartDevice[] devices = { new TV(), new AirCon() };
for (SmartDevice device : devices) {
// 다형성(Polymorphism): 실제 객체가 누구냐에 따라 알아서 다른 동작이 실행됨
device.powerOn();
}
효과: 나중에 '로봇청소기'가 추가되어도, **메인 코드는 단 한 줄도 수정할 필요가 없다.**
Phase 3: 믿음의 법칙 (Deep Dive)
다형성을 사용할 때 꼭 알아야 할 중요한 규칙이 있다. "변수는 껍데기를 믿고, 메서드는 알맹이를 믿는다."
SmartDevice p = new TV(); // 껍데기는 SmartDevice, 알맹이는 TV
1. 변수(Field)는 껍데기를 따라간다 (Static Binding)
`p.brand`를 호출하면, 자바는 참조 변수의 타입(`SmartDevice`)에 정의된 변수를 가져온다. 아무리 실제 객체가 `TV`라도, 껍데기가 `SmartDevice`라면 부모의 값을 가져온다. (이래서 필드 오버라이딩은 피하는 게 좋다.)
2. 메서드(Method)는 알맹이를 따라간다 (Dynamic Binding)
`p.powerOn()`을 호출하면, 자바는 실제 생성된 객체(`new TV`)가 오버라이딩한 최신 메서드를 실행한다. 이것이 우리가 조건문 없이도 `TV`의 기능을 실행할 수 있는 이유다.
Conclusion
다형성은 단순히 코드를 줄이는 기술이 아니다. "변하는 것(구체적인 제품)"과 "변하지 않는 것(켜는 행위)"을 분리하여, 미래의 변경에 유연하게 대처할 수 있게 해주는 객체지향의 핵심 철학이다.
📚 함께 보면 좋은 'Antigravity & VS Code & Java' 로드맵
다형성과 추상화에 대해 알아보았습니다, Java에서 소위 말하는 제네릭이란 뭘까요?
Next Step 제네릭에 대해 알아보기
[Java] 제네릭의 역설: 유연할수록 아무것도 담을 수가 없다
'Dev Study > Java' 카테고리의 다른 글
| [JAVA & Antigravity] 인터페이스: 강한 결합을 끊고 유연함을 얻다 (3) | 2026.02.05 |
|---|---|
| [Java] 제네릭의 역설: 유연할수록 아무것도 담을 수 없다 (0) | 2026.01.28 |
| [Java] 책임은 누구에게 있는가: 상속과 super의 본질 (0) | 2026.01.21 |
| [Java] 객체지향 설계와 메모리 구조: 싱글턴, 빌더 패턴, 그리고 불변성 (0) | 2026.01.15 |
| [Java] 절차지향의 한계와 객체지향의 필요성 (5) | 2026.01.12 |