다형성과 추상화, 상속의 관계
다형성은 OOP의 4대 특성 중 하나로, 다양한 객체가 동일한 인터페이스를 통해 서로 다른 방식으로 동작할 수 있게 한다. 이 개념은 상속에 기반하고 있으며, 상속과 다형성은 서로를 지원하는 관계에 있다. 다형성이 기능의 다양성을 제공하는 반면, 상속은 이러한 다양성을 구현하기 위한 기반을 마련한다.
여기서 중요한 것이 추상화이다. 수학에서의 추상화처럼 OOP의 추상화도 복잡한 문제 해결을 위한 도구로 작용하며, 다형성과 깊은 연관이 있다. 추상화는 클래스와 객체의 공통된 특성을 뽑아내어, 이를 통해 여러 형태의 객체를 통일된 방법으로 다룰 수 있게 한다. 이러한 다형성의 강력함은 프로그래밍에서 유연한 코드 작성을 가능하게 한다.
다형성과 상속으로 생기는 새로운 문제
추상화는 막강한 도구이지만, 그로 인해 예기치 못한 문제들이 발생할 수 있다. 프로그래밍 언어 제작자들도 이러한 문제를 처음부터 예측하지 못하는 경우가 많다. 이후에는 다양한 문법과 기능이 추가되어 이러한 문제를 해결해 나가고 있다.
기존 상속과 다형성으로 구현 시 생길 수 있는 문제
기본적인 상속과 다형성을 이용한 프로그램 설계에서 발생할 수 있는 주요 문제 중 하나는 부모 클래스에서 정의한 메서드의 구현을 강제하지 않는다는 점이다. 이러한 상황은 다음과 같은 문제를 초래할 수 있다.
기능 미구현
자식 클래스가 부모 클래스의 메서드를 오버라이드하지 않으면, 해당 메서드는 부모 클래스의 기본 구현을 그대로 사용하게 된다. 이로 인해 자식 클래스의 특수한 요구 사항을 반영하지 못하게 되어, 기대한 대로 동작하지 않을 수 있다.
class Animal {
void makeSound() {
System.out.println("소리를 내다");
}
}
class Dog extends Animal {
// 메서드 오버라이드하지 않음
}
class Cat extends Animal {
// 메서드 오버라이드하지 않음
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
Animal cat = new Cat();
dog.makeSound(); // "소리를 내다"
cat.makeSound(); // "소리를 내다"
}
}
예측 불가능한 동작
다형성을 활용할 때, 부모 클래스를 참조하여 자식 클래스의 인스턴스를 처리하게 되면, 자식 클래스에서 반드시 오버라이드해야 하는 메서드를 구현하지 않았을 경우 예기치 않은 동작이 발생할 수 있다. 이는 프로그램의 안정성을 해치고, 디버깅을 더욱 어렵게 만들 수 있다.
class Bird extends Animal {
// makeSound 메서드를 오버라이드하지 않음
}
public class Main {
public static void main(String[] args) {
Animal bird = new Bird();
bird.makeSound(); // "소리를 내다" 출력
}
}
코드의 일관성 저하
메서드 구현이 강제되지 않으면, 각 자식 클래스가 해당 메서드를 구현하지 않을 수도 있다. 이로 인해 코드의 일관성이 떨어지고, 같은 기능을 수행하는 여러 클래스 간의 차이가 생길 수 있다. 이로 인해 코드의 가독성과 유지 보수성이 저하될 수 있다.
런타임 오류
부모 클래스에서 기대하는 메서드가 자식 클래스에 구현되어 있지 않을 경우, 메서드를 호출했을 때 런타임 오류가 발생할 수 있다. 이는 프로그램의 정상적인 흐름을 방해하고, 사용자에게 불편을 초래할 수 있다.
추상 클래스의 등장 배경
이러한 문제를 해결하기 위해 추상 클래스가 등장하게 되었다. 추상 클래스는 특정 메서드를 추상 메서드로 정의함으로써 자식 클래스가 반드시 구현하도록 강제할 수 있다. 이를 통해 각 자식 클래스는 고유의 동작을 정의할 수 있으며, 코드의 일관성과 안정성을 높일 수 있다.
추상 클래스란?
추상 클래스는 다음과 같은 형태로 정의된다.
<접근제어자> abstract class <클래스명> {...}
추상 클래스는 인스턴스를 만들 수 없는 클래스이다. 인스턴스를 생성할 수 있는 클래스는 구체 클래스라고 하며, 따라서 구체 클래스가 아닌 클래스를 추상 클래스라고 부른다. 추상 클래스는 다른 클래스의 부모 클래스로 활용될 수 있으며, 반드시 추상 메서드를 포함해야 하는 것은 아니다. 즉, 추상 클래스는 그 자체로는 완전한 기능을 가지지 않지만, 상속을 통해 구체 클래스가 기능을 구현할 수 있도록 한다.
abstract class Animal {
abstract void makeSound();
}
class Dog extends Animal {
void makeSound() {
System.out.println("멍멍!");
}
}
class Cat extends Animal {
void makeSound() {
System.out.println("야옹!");
}
}
추상 클래스는 객체 지향 프로그래밍에서 복잡한 구조를 단순화하고, 코드의 재사용성을 높이는 데 중요한 역할을 한다. 이를 통해 개발자는 특정 기능을 제공하는 구체 클래스를 정의할 수 있으며, 공통된 인터페이스를 통해 다양한 객체를 처리할 수 있다. 추상 클래스를 적절히 활용하면, 코드의 유지 보수성과 확장성을 크게 향상시킬 수 있다.
구체 클래스와 추상 클래스의 차이점
인스턴스를 만들 수 있는 클래스는 구체 클래스라고 불린다. 구체 클래스는 그 자체로 완전한 기능을 제공하기 때문에, 바로 사용할 수 있다. 반면에 추상 클래스는 구체적이지 않아서, 단독으로는 사용할 수 없고, 자식 클래스가 이걸 상속받아서 구체적으로 구현해야 사용할 수 있다.
추상 클래스는 크게 두 가지 목적을 가지고 있다. 첫째, 공통적인 기능을 재사용하기 위한 수단이고, 둘째, 자식 클래스가 반드시 구현해야 하는 메서드를 명시하기 위한 수단이다. 추상화는 복잡한 시스템을 더 단순하고 일관성 있게 만들 수 있도록 돕는다. 다형성의 원리도 여기서 비롯된다고 볼 수 있다.
출처
- 개체지향 프로그래밍 및 설계 - 유데미 강의