자바에서 객체지향 프로그래밍을 할 때, 추상 클래스와 인터페이스는 매우 중요한 개념입니다. 두 개념은 모두 공통된 동작을 정의하고, 클래스 간의 다형성을 제공하는 데 사용됩니다. 그러나 이펙티브 자바에서는 가능하다면 인터페이스를 추상 클래스보다 우선해서 사용하라고 권장합니다. 이는 코드의 유연성과 재사용성을 높이기 위함입니다.
1. 추상 클래스와 인터페이스의 기본 개념
1.1 추상 클래스란 무엇인가?
추상 클래스는 인스턴스를 생성할 수 없는 클래스입니다. 주로 여러 클래스에 공통된 코드를 제공하기 위해 사용됩니다. 추상 클래스는 다른 클래스가 상속받아 사용할 수 있는 공통 메서드를 정의하거나, 반드시 구현해야 할 추상 메서드를 포함할 수 있습니다.
예를 들어, 동물(Animal)이라는 추상 클래스를 정의하고, 이를 상속받아 구체적인 동물(예: 개, 고양이)을 구현할 수 있습니다. 이때 동물 클래스에는 모든 동물이 공유하는 메서드(예: 숨쉬기, 이동하기 등)를 정의하고, 개별 동물에 따라 달라지는 메서드(예: 소리내기 등)를 추상 메서드로 선언할 수 있습니다.
위 코드에서 breathe() 메서드는 공통적으로 구현된 메서드이며, makeSound() 메서드는 하위 클래스에서 반드시 구현해야 하는 추상 메서드입니다.
1.2 인터페이스란 무엇인가?
인터페이스는 클래스가 구현해야 할 메서드의 집합을 정의합니다. 인터페이스 자체는 동작을 구현하지 않으며, 오로지 메서드 시그니처만 정의합니다. 자바 8부터는 디폴트 메서드와 정적 메서드를 가질 수 있게 되었지만, 기본적으로는 메서드 정의만을 포함하는 것이 원칙입니다.
인터페이스는 다중 상속을 지원합니다. 즉, 한 클래스가 여러 인터페이스를 구현할 수 있습니다. 이를 통해 클래스는 여러 역할을 수행할 수 있으며, 코드의 유연성을 높일 수 있습니다.
Interface
interface Animal {
void makeSound();
}
2. 추상 클래스의 제한사항
2.1 단일 상속의 제약
추상 클래스의 가장 큰 단점은 단일 상속만을 허용한다는 점입니다. 자바에서 한 클래스는 오직 하나의 클래스만을 상속받을 수 있습니다. 이는 다중 상속이 허용되지 않기 때문에 발생하는 제약입니다. 만약 두 개 이상의 클래스에서 공통된 기능을 상속받아야 하는 경우, 추상 클래스는 적합하지 않을 수 있습니다.
Example
abstract class Animal {
abstract void makeSound();
}
abstract class Machine {
abstract void operate();
}
class RobotDog extends Animal, Machine { // Error: 다중 상속은 불가능합니다.
void makeSound() {
System.out.println("Beep Boop");
}
void operate() {
System.out.println("Operating...");
}
}
위 예시처럼, 자바에서는 다중 상속이 허용되지 않으므로 하나의 클래스가 두 개 이상의 부모 클래스를 상속받을 수 없습니다. 이는 코드의 재사용성과 유연성을 제한하는 요소가 될 수 있습니다.
2.2 계층 구조의 복잡성
추상 클래스는 복잡한 계층 구조를 야기할 수 있습니다. 상속이 깊어질수록 클래스 간의 의존성이 강해지며, 이는 코드의 유지보수성을 떨어뜨릴 수 있습니다. 또한, 상위 클래스에서 변경이 발생하면 모든 하위 클래스에 영향을 미치게 되어 예기치 않은 부작용을 초래할 수 있습니다.
예를 들어, 상위 클래스에 새로운 메서드를 추가하거나 기존 메서드를 수정할 경우, 이를 상속받는 하위 클래스들에서 해당 메서드를 재정의해야 할 수 있습니다. 이로 인해 코드베이스가 커질수록 관리가 어려워질 수 있습니다.
Example
abstract class Vehicle {
abstract void startEngine();
}
class Car extends Vehicle {
void startEngine() {
System.out.println("Car engine started");
}
}
class Motorcycle extends Vehicle {
void startEngine() {
System.out.println("Motorcycle engine started");
}
}
이 예제에서, 만약 Vehicle 클래스에 새로운 메서드를 추가하면 Car와 Motorcycle 클래스도 이를 구현해야 합니다. 이러한 방식은 작은 프로젝트에서는 문제가 되지 않을 수 있지만, 큰 프로젝트에서는 관리가 어렵고 실수의 여지가 많아질 수 있습니다.
3. 인터페이스의 장점
3.1 다중 상속이 가능
인터페이스는 다중 상속을 지원합니다. 이는 하나의 클래스가 여러 인터페이스를 구현함으로써 다양한 동작을 수행할 수 있음을 의미합니다. 이를 통해 코드의 재사용성을 극대화하고, 유연한 설계를 가능하게 합니다.
Example
예를 들어, Comparable과 Serializable 인터페이스를 동시에 구현함으로써 객체가 정렬 가능하고, 직렬화될 수 있도록 만들 수 있습니다.
class Dog implements Animal, Comparable<Dog>, Serializable {
public void makeSound() {
System.out.println("Bark!");
}
public int compareTo(Dog other) {
return this.age - other.age;
}
}
이 예제에서 Dog 클래스는 Animal, Comparable, Serializable 인터페이스를 동시에 구현함으로써 다양한 역할을 수행할 수 있습니다. 이는 추상 클래스가 제공하지 못하는 유연성을 제공합니다.
3.2 유연성과 확장성
인터페이스는 추상 클래스보다 훨씬 유연하고 확장성이 높습니다. 인터페이스를 사용하면 기존의 클래스 계층 구조를 변경할 필요 없이 새로운 기능을 추가할 수 있습니다. 인터페이스에 새로운 메서드를 추가하고 이를 구현하는 클래스만 수정하면 됩니다.
또한, 인터페이스는 특정 기능을 여러 클래스에 쉽게 적용할 수 있도록 도와줍니다. 이는 코드의 재사용성과 유지보수성을 높이는 데 매우 유리합니다.
Example
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Duck implements Animal, Flyable, Swimmable {
public void makeSound() {
System.out.println("Quack!");
}
public void fly() {
System.out.println("Flying...");
}
public void swim() {
System.out.println("Swimming...");
}
}
이 예제에서 Duck 클래스는 Animal, Flyable, Swimmable 인터페이스를 모두 구현합니다. 이를 통해 오리 객체는 다양한 행동을 수행할 수 있으며, 이러한 설계는 코드의 유연성을 높입니다.
3.3 믹스인(Mixin) 인터페이스
인터페이스는 특정 동작을 여러 클래스에 추가할 수 있는 믹스인(Mixin) 형태로도 사용할 수 있습니다. 믹스인 인터페이스는 클래스가 특정 행동을 쉽게 공유하도록 돕습니다. 예를 들어, Logging 인터페이스를 만들어 여러 클래스에서 로그 기능을 쉽게 사용할 수 있습니다.
이 코드에서 AbstractFileProcessor는 FileProcessor 인터페이스를 구현하면서, 공통된 readFile과 writeFile 메서드를 제공합니다. 그러나 processFile 메서드는 파일 형식에 따라 달라질 수 있기 때문에 추상 메서드로 남겨 두었습니다.
7.2 구체적인 구현
AbstractFileProcessor를 상속받아 특정 파일 형식에 대한 처리를 구현하는 클래스를 만들 수 있습니다.