앞 글(다형성 기초)에서 다형성의 기초를 다뤘다. 이번 글에서는 왜 다형성을 활용해야 하는지, 그리고 추상 클래스, 인터페이스를 통해 어떻게 실무적인 제약과 확장을 설계할 수 있는지 정리한다.
다형성 활용 전: 중복 코드의 문제
동물 울음소리를 출력하는 프로그램을 작성한다고 해보자.
public class Dog {
public void sound() { System.out.println("멍멍"); }
}
public class Cat {
public void sound() { System.out.println("냐옹"); }
}
public class Caw {
public void sound() { System.out.println("음매"); }
}
public class AnimalSoundMain {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Caw caw = new Caw();
System.out.println("동물 소리 테스트 시작");
dog.sound();
System.out.println("동물 소리 테스트 종료"); // Cat, Caw도 동일한 코드 반복
}
}
문제점
- 새로운 동물이 추가될 때마다 중복 코드가 증가한다.
- 메서드, 배열로도 중복 제거를 할 수 없다. → 타입이 모두 달라서 한 곳에 모을 수 없기 때문이다.
다형성 도입: Animal 부모 클래스
public class Animal {
public void sound() { System.out.println("동물 울음 소리"); }
}
public class Dog extends Animal {
@Override
public void sound() { System.out.println("멍멍"); }
}
public class Cat extends Animal {
@Override
public void sound() { System.out.println("냐옹"); }
}
public class Caw extends Animal {
@Override
public void sound() { System.out.println("음매"); }
}
- 활용 예시
private static void soundAnimal(Animal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
soundAnimal(new Dog());
soundAnimal(new Cat());
soundAnimal(new Caw());
- 다형적 참조(Animal animal = new Dog()) + 오버라이딩 덕분에 중복이 제거되고 확장성이 확보된다.
배열과 for문으로 확장
Animal[] animalArr = {new Dog(), new Cat(), new Caw()};
for (Animal animal : animalArr) {
soundAnimal(animal);
}
- Animal 배열에 자식들을 담을 수 있다.
- for문과 함께 쓰면 새로운 동물이 추가되어도 soundAnimal()은 변경할 필요가 없다.
추상 클래스: 제약 추가
문제점
- Animal 자체를 new 할 수 있다. → 현실적으로 불필요한 부분이다.
- 자식이 sound()를 깜빡하고 오버라이딩 안 하면 예상과 다른 동작이 발생할 수 있다.
- 해결책 = 추상 클래스 + 추상 메서드
public abstract class AbstractAnimal {
public abstract void sound(); // 반드시 오버라이딩
public void move() { System.out.println("동물이 움직입니다."); }
}
- 자식 클래스는 반드시 sound() 구현을 해야한다.
public class Dog extends AbstractAnimal {
@Override
public void sound() { System.out.println("멍멍"); }
}
- 즉, 추상 클래스를 통해 실수를 방지함과 동시에 올바른 설계를 하도록 강제하는 것이다.
순수 추상 클래스 → 인터페이스
모든 메서드가 추상 메서드라면?
public interface InterfaceAnimal {
void sound();
void move();
}
- 인스턴스 생성이 불가능하다.
- 모든 메서드가 반드시 구현되어야 한다.
- 클래스 상속과 달리 다중 구현이 가능하다.
public class Dog implements InterfaceAnimal {
public void sound() { System.out.println("멍멍"); }
public void move() { System.out.println("개 이동"); }
}
- 인터페이스는 순수 추상 클래스와 같지만, 규약(제약) 역할에 더 충실하다.
- 모든 메서드가 추상 메서드라면 추상 클래스 대신 인터페이스를 쓰는 게 맞다.
- 그리고 인터페이스는 클래스와 달리 여러 개를 동시에 구현할 수 있다.
인터페이스 다중 구현
자바는 클래스 다중 상속은 불가하지만, 인터페이스는 다중 구현이 가능하다.
public interface InterfaceA {
void methodA();
void methodCommon();
}
public interface InterfaceB {
void methodB();
void methodCommon();
}
public class Child implements InterfaceA, InterfaceB {
public void methodA() {
System.out.println("Child.methodA");
}
public void methodB() {
System.out.println("Child.methodB");
}
public void methodCommon() {
System.out.println("Child.methodCommon");
}
}
- 이때 다이아몬드 문제는 발생하지 않는다.
- 구현은 자식이 직접 정의하기 때문이다.
클래스 + 인터페이스 혼합 활용
public abstract class AbstractAnimal {
public abstract void sound();
public void move() {
System.out.println("동물이 이동합니다.");
}
}
public interface Fly {
void fly();
}
public class Bird extends AbstractAnimal implements Fly {
public void sound() {
System.out.println("짹짹");
}
public void fly() {
System.out.println("새 날기");
}
}
- AbstractAnimal을 상속해 기본 동물 기능이 제공된다.
- Fly 인터페이스 구현해 나는 능력 추가했다.
- 다형성 덕분에 soundAnimal(), flyAnimal()처럼 분리된 책임 처리가 가능하다.
정리하자면...
- 다형성의 힘: 중복 제거 + 확장성 + 유지보수성
- 추상 클래스: 직접 생성 불가능, 반드시 오버라이딩할 메서드를 강제한다.
- 인터페이스: 규약을 제공, 다중 구현이 가능 → 클래스보다 더 강력한 제약이다.
- 좋은 프로그램 = 적절한 제약 → 실수를 원천 차단하고 협업 효율성을 높인다.
- 다형성 + 추상화(추상 클래스·인터페이스)는 실무에서 "확장에 열려 있고, 변경에는 닫힌" 객체지향 설계 원칙(OCP)을 실현하는 강력한 무기이다.
'java' 카테고리의 다른 글
| Java 상속과 다이아몬드 문제(Diamond Problem) (0) | 2025.09.16 |
|---|---|
| Java 다형성(Polymorphism) 기초 정리 (0) | 2025.09.15 |
| Java 상속(Inheritance) 정리 (0) | 2025.09.15 |
| Java final 키워드 정리 (2) | 2025.09.15 |
| Java 메모리 구조와 static 정리 (0) | 2025.09.15 |