1. 컬렉션에서의 다형성: 왜 필요한가?
질문으로 시작해 봅시다: “실제 프로그램에서 다형성이 왜 필요할까요?”
동물원을 상상해 보세요. 기본 클래스 Animal이 있고, 그를 상속한 Dog, Cat, Cow, Parrot, 심지어 Platypus까지(오리너구리를 좋아하는 분들을 위해) 있습니다. 이들 각각은 소리를 낼 수 있으며(makeSound()), 방식은 서로 다릅니다.
각 동물마다 별도의 배열을 만들기보다, Animal 타입의 배열이나 리스트를 하나 선언하고 여기에 무엇이든 담습니다:
Animal[] animals = {
new Dog(),
new Cat(),
new Cow(),
new Parrot()
};
이제 이 배열을 순회하며 각 객체에 대해 makeSound()를 호출할 수 있습니다:
for (Animal animal : animals) {
animal.makeSound();
}
마법 같죠! 각 객체는 어떤 소리를 내야 하는지 스스로 알고 있으므로, if나 switch를 잔뜩 쓸 필요가 없습니다.
비유로 보는 예
여러 동물에게 “소리 내!”라고만 명령하면, 각 동물이 알아서 반응하는 것과 같습니다. 개는 멍멍 짖고, 고양이는 야옹 하고, 소는 — 음메 하고 웁니다. 누가 누구인지 일일이 확인하지 않고, 같은 메서드만 호출하면 됩니다.
2. 실전 예제: 직원 계층
현실(그리고 앞으로의 IT 직무)에 더 가까운 예제를 만들어 봅시다. 회사에 다양한 직원들이 있다고 합시다: 매니저, 개발자, 테스터. 모두에게 work() 메서드가 있지만, 수행 방식은 서로 다릅니다.
기본 클래스 선언
public class Employee {
public void work() {
System.out.println("직원이 일하고 있습니다...");
}
}
하위 클래스들
public class Manager extends Employee {
@Override
public void work() {
System.out.println("매니저가 회의를 진행합니다.");
}
}
public class Developer extends Employee {
@Override
public void work() {
System.out.println("개발자가 코드를 작성합니다.");
}
}
public class Tester extends Employee {
@Override
public void work() {
System.out.println("테스터가 버그를 찾습니다.");
}
}
기본 타입 배열/리스트 사용
public class CompanyDemo {
public static void main(String[] args) {
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Developer()
};
for (Employee e : team) {
e.work(); // 각 객체에 대해 "올바른" 버전의 메서드가 호출됩니다
}
}
}
실행 결과:
매니저가 회의를 진행합니다.
개발자가 코드를 작성합니다.
테스터가 버그를 찾습니다.
개발자가 코드를 작성합니다.
무엇이 장점일까요?
- “만약 이게 Developer라면 ~~~” 같은 분기 검사를 잔뜩 작성하지 않아도 됩니다.
- 새 직원(예: Designer)을 추가하려면 새 클래스를 만들고 배열에 넣기만 하면 됩니다.
- 직원 배열을 사용하는 코드는 전혀 변경되지 않습니다!
3. 다형성의 장점: 유연성과 확장성
회사에 새로운 직원 타입 — Designer가 생겼다고 해봅시다. 해야 할 일은 새 클래스를 만드는 것뿐입니다:
public class Designer extends Employee {
@Override
public void work() {
System.out.println("디자이너가 목업을 그립니다.");
}
}
이제 디자이너를 팀에 추가할 수 있습니다:
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Designer()
};
짜잔! 배열을 순회하며 work()를 호출하는 코드를 한 줄도 바꾸지 않아도, 프로그램은 곧바로 새 직원 타입과 제대로 동작합니다.
이것이 바로 확장성입니다: 코드는 새로운 객체 타입에 쉽게 적응합니다.
4. 다형성의 한계: 반대편 이야기
아쉽지만 어떤 마법에도 한계가 있습니다(그리고 RPG처럼 대가도 있지요).
기본 클래스의 메서드만 사용할 수 있음
Employee 타입의 변수로 작업할 때는 Employee 클래스에 선언된 메서드만 호출할 수 있습니다. 만약 Developer에 전용 메서드 writeCode()가 있다면, 이를 직접 호출할 수 없습니다:
Employee e = new Developer();
// e.writeCode(); // 컴파일 오류: Employee에는 이런 메서드가 없습니다!
전용 메서드를 꼭 호출해야 한다면 형변환을 해야 합니다. 하지만 이는 최후의 수단입니다. 형변환을 자주 하게 된다면, 클래스 설계를 재고해 보아야 합니다 — 기본 클래스나 인터페이스가 필요한 메서드를 포함해야 합니다.
if (e instanceof Developer) {
Developer dev = (Developer) e;
dev.writeCode();
}
하지만 이렇게 하면 애초에 추구하던 보편성과 우아함을 잃게 됩니다. 그러니 모든 하위 클래스에 정말로 필요한 메서드만 기본 클래스에 두도록 설계하세요.
5. 실습: 직원 계층 구현하기
유익하고도 재미있게, 몇 가지 직원 타입이 있는 간단한 애플리케이션을 작성하고 다형성으로 이들을 처리해 봅시다.
1단계: 기본 클래스와 하위 클래스들
// Employee.java
public class Employee {
public void work() {
System.out.println("직원이 일하고 있습니다...");
}
}
// Manager.java
public class Manager extends Employee {
@Override
public void work() {
System.out.println("매니저가 회의를 진행합니다.");
}
}
// Developer.java
public class Developer extends Employee {
@Override
public void work() {
System.out.println("개발자가 코드를 작성합니다.");
}
}
// Tester.java
public class Tester extends Employee {
@Override
public void work() {
System.out.println("테스터가 버그를 찾습니다.");
}
}
2단계: 메인 클래스
// CompanyDemo.java
public class CompanyDemo {
public static void main(String[] args) {
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Developer()
};
for (Employee e : team) {
e.work();
}
}
}
3단계: 확장성 추가
한 달 뒤 회사에 새 직원 — 디자이너가 합류했다고 가정해 봅시다. 필요한 것은 이것뿐입니다:
public class Designer extends Employee {
@Override
public void work() {
System.out.println("디자이너가 목업을 그립니다.");
}
}
이제 디자이너를 팀에 추가하면 됩니다:
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Designer()
};
마무리
핵심 코드(CompanyDemo)는 그대로입니다! 이것이 다형성의 힘입니다.
6. 다형성을 사용할 때의 흔한 실수
오류 1: 기본 타입 참조로 하위 클래스의 전용 메서드까지 호출하려 함.
초보자들은 슈퍼클래스 타입 변수로 하위 클래스의 전용 메서드를 호출하려는 경우가 많습니다. 예를 들어:
Employee e = new Developer();
// e.writeCode(); // 오류! 이런 메서드는 Employee에 정의되어 있지 않습니다.
전용 메서드를 호출하려면 형변환이 필요하지만, 그러면 보편성이 떨어집니다.
오류 2: @Override 애노테이션을 사용하지 않음.
애노테이션을 빼먹으면(예: 이름을 잘못 쓰는 등) 재정의가 아니라 새로운 메서드를 만들어 버릴 수 있습니다. 그러면 다형성이 작동하지 않고, 슈퍼클래스의 버전이 호출됩니다.
오류 3: 공통 인터페이스의 부재.
기본 클래스에 필요한 메서드가 없다면 다형성은 불가능합니다. 예를 들어 Employee에 work()가 없다면, 직원 배열을 도는 루프는 그 메서드를 모두에게 호출할 수 없습니다.
오류 4: 개방-폐쇄 원칙 위반.
새로운 직원 타입을 추가하려고 배열/리스트를 순회하는 코드를 수정해야 한다면 — 다형성을 제대로 활용하지 못하고 있는 것입니다.
GO TO FULL VERSION