헤드 퍼스트 자바를 읽으면서 계속 마주치는 단어, '다형성'. 무슨 느낌인지는 알겠지만 정확히 뭔지, 왜 필요한지 명확하게 설명하기는 어려웠다. 이번 기회에 다형성의 개념부터 구현 방법까지 정리해보았다.
다형성(Polymorphism)은 '여러 형태'를 의미한다. 객체지향 프로그래밍에서는 하나의 객체가 여러 타입으로 사용될 수 있는 특성을 말한다.
예를 들어, Dog 클래스가 Animal 클래스를 상속받는다면, Dog 객체는 Dog 타입으로도, Animal 타입으로도 사용할 수 있다.
Animal myAnimal = new Dog(); // Dog 객체를 Animal 타입으로 참조
이때 실제 객체는 Dog이지만, Animal 타입의 참조변수로 접근할 수 있다. 이것이 바로 다형성이다.
다형성이 없다면 어떻게 될까? 동물원 관리 시스템을 만든다고 생각해보자.
// 다형성을 사용하지 않을 때
public void feedAnimals(Dog dog, Cat cat, Lion lion) {
dog.eat();
cat.eat();
lion.eat();
}
새로운 동물이 추가될 때마다 메서드를 수정해야 한다. 하지만 다형성을 사용하면:
// 다형성을 사용할 때
public void feedAnimals(Animal[] animals) {
for(Animal animal : animals) {
animal.eat(); // 실제로는 각각의 구체적인 eat() 메서드가 호출됨
}
}
코드가 훨씬 유연해지고, 새로운 동물을 추가해도 기존 코드를 수정할 필요가 없다.
때로는 클래스의 인스턴스를 직접 만들면 안 되는 경우가 있다. Animal 클래스를 생각해보자. "동물"이라는 추상적 개념은 존재하지만, 구체적으로 어떤 동물인지 모르는 상태에서는 의미가 없다.
public abstract class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
// 구체적인 메서드
public void sleep() {
System.out.println(name + "이 잠을 잡니다.");
}
// 추상 메서드 - 반드시 하위 클래스에서 구현해야 함
public abstract void eat();
public abstract void makeSound();
}
추상 메서드는 하위 클래스에서 반드시 구현해야 하는 "계약"과 같다. 이를 통해 모든 동물이 eat()과 makeSound() 메서드를 가지도록 강제할 수 있다.
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void eat() {
System.out.println(name + "이 사료를 먹습니다.");
}
@Override
public void makeSound() {
System.out.println("멍멍!");
}
}
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void eat() {
System.out.println(name + "이 생선을 먹습니다.");
}
@Override
public void makeSound() {
System.out.println("야옹!");
}
}
자바에서 모든 클래스는 Object 클래스를 상속받는다. 명시적으로 다른 클래스를 상속하지 않으면 자동으로 Object를 상속한다.
public class MyClass { // 자동으로 extends Object
// ...
}
equals(Object obj): 객체 비교hashCode(): 해시테이블에서 사용하는 해시코드toString(): 객체의 문자열 표현getClass(): 객체의 실제 클래스 타입이 덕분에 모든 객체를 Object 타입으로 다룰 수 있다.
Object[] objects = {new Dog("멍이"), new Cat("야옹이"), "Hello"};
for(Object obj : objects) {
System.out.println(obj.toString()); // 모든 객체가 toString() 메서드를 가짐
}
Object 타입으로 캐스팅된 객체를 원래 타입으로 사용하려면 다운캐스팅이 필요하다.
Animal myAnimal = new Dog("멍이");
if(myAnimal instanceof Dog) {
Dog myDog = (Dog) myAnimal; // 안전한 다운캐스팅
// 이제 Dog의 고유 메서드 사용 가능
}
자바는 다중 상속을 지원하지 않는다. 하지만 인터페이스를 통해 다중 구현이 가능하다.
public interface Flyable {
void fly();
}
public interface Swimmable {
void swim();
}
public class Duck extends Animal implements Flyable, Swimmable {
public Duck(String name) {
super(name);
}
@Override
public void eat() {
System.out.println(name + "이 물풀을 먹습니다.");
}
@Override
public void makeSound() {
System.out.println("꽥꽥!");
}
@Override
public void fly() {
System.out.println(name + "이 날아갑니다.");
}
@Override
public void swim() {
System.out.println(name + "이 헤엄칩니다.");
}
}
이제 Duck 객체는 Animal, Flyable, Swimmable 타입으로 모두 사용할 수 있다.
Duck duck = new Duck("도널드");
Animal animal = duck;
Flyable flyer = duck;
Swimmable swimmer = duck;
일반 클래스를 사용하는 경우:
추상 클래스를 사용하는 경우:
인터페이스를 사용하는 경우:
다형성은 객체지향 프로그래밍의 핵심 개념이다. 같은 타입으로 다양한 객체를 다룰 수 있게 해주어 코드의 유연성과 재사용성을 크게 향상시킨다. 추상 클래스와 인터페이스는 이런 다형성을 구현하는 강력한 도구들이며, 상황에 맞게 적절히 선택해서 사용하는 것이 중요하다.
다형성을 제대로 이해하고 활용하면, 변화에 유연하게 대응할 수 있는 견고한 코드를 작성할 수 있다.