
개발을 할 때 가장 신경써야 하는 것 중 하나가 중복을 제거하여 변경을 쉽게 만드는 것이다. 객체지향 프로그래밍의 장점 중 하나는 코드를 재사용하여 중복을 제거하기에 용이하다는 것인데, 이를 위한 방법에는 크게 상속과 합성 두 가지가 있다.
상속은 상위 클래스에 중복 로직을 구현해두고 이를 물려받아 코드를 재사용하는 방법이다. 흔히 상속은 Is-a 관계라고 많이 불린다.
class Animal {
void eat() {
System.out.println("Eating...");
}
}
class Dog extends Animal {
void bark() {
System.out.println("Barking...");
}
}
public class InheritanceExample {
public static void main(String[] args) {
Dog myDog = new Dog();
myDog.eat(); // 상속받은 메서드 호출
myDog.bark(); // 자식 클래스의 메서드 호출
}
}
class Engine {
void start() {
System.out.println("Engine starting...");
}
}
class Car {
Engine engine;
Car(Engine engine) {
this.engine = engine;
}
void drive() {
engine.start();
System.out.println("Car is moving...");
}
}
public class CompositionExample {
public static void main(String[] args) {
Engine myEngine = new Engine();
Car myCar = new Car(myEngine);
myCar.drive(); // 합성을 통한 기능 조합
}
}
- 결합도는 하나의 모듈이 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 정도이다.
- 객체지향 프로그래밍에서는 결합도는 낮을수록, 응집도는 높을수록 좋다.
- 객체지향의 장점 중 하나는 추상화에 의존함으로써 다른 객체에 대한 결합도는 최소화하고 응집도를 최대화하여 변경 가능성을 최소화하는 것이다.
대표적인 Vector, Stack 예제
Stack 클래스는 Vector 클래스를 상속받는다. 그래서 Stack 클래스가 제공하는 push, pop 이지만 Vector 클래스의 add 메소드 또한 외부로 노출되게 된다. 그러면서 아래와 같이 의도치 않은 동작이 실행되면서 오류를 범하게 된다.
Stack<String> stack = new Stack<>();
stack.push("1st");
stack.push("2nd");
stack.push("3rd");
stack.add(0, "4th");
assertEquals("4th", stack.pop()); // 실패!!!!
여기서 문제는 Stack 클래스의 add(int index, E element) 메서드를 사용하여 요소를 삽입하면 LIFO (Last-In-First-Out) 구조를 깨뜨리게 됩니다. 스택에서는 새로운 요소가 항상 맨 위에 추가되어야 하는데, add 메서드를 사용하면 지정된 인덱스에 요소가 삽입되어 예상과 다르게 동작합니다.
상속으로 인해 결합도가 높아지면 다음과 같은 두 가지 문제점이 발생한다.
간단한 음식점 예제
class Food {
protected int price;
public Food(int price) {
this.price = price;
}
public int getPrice() {
return price;
}
}
class Bread extends Food {
private String type;
public Bread(int price, String type) {
super(price);
this.type = type;
}
public String getType() {
return type;
}
}
public class FoodExample {
public static void main(String[] args) {
// Creating a Food object
Food food = new Food(10);
// Creating a Bread object
Bread bread = new Bread(5, "Whole Wheat");
displayFoodInfo(bread);
}
}
discount 추가
class Food {
protected int price;
protected int discount; // 코드 추가
public Food(int price, int discount) {
this.price = price;
this.discount = discount;
}
public int getPrice() {
return price - discount; // Apply discount
}
public int getDiscount() {
return discount;
}
}
class Bread extends Food {
private String type;
public Bread(int price, int discount, String type) {
super(price, discount); // 코드 수정
this.type = type;
}
public String getType() {
return type;
}
}
public class FoodExample {
public static void main(String[] args) {
// Creating a Food object
Food food = new Food(10, 2); // 코드 추가
// Creating a Bread object
Bread bread = new Bread(5, 1, "Whole Wheat"); // 코드 추가
}
}

상속은 중복을 제거하기에 아주 좋은 객체지향 기술처럼 보이고, 그에 따라 상속을 무분별하게 남발하는 경우를 자주 볼 수 있다. 하지만 상속을 이용해야 하는 경우는 상당히 선택적이며, 상속이 갖는 단점은 상당히 치명적이기 때문에 상속보다는 합성을 이용할 것을 권장한다.
- Java의 창시자인 제임스 고슬링(James Arthur Gosling)이 한 인터뷰에서 "내가 자바를 만들면서 가장 후회하는 일은 상속을 만든 점이다" 라고 말할 정도 이다.
- 조슈야 블로크의 Effective Java에서는 상속을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 상속을 금지하라는 조언을 한다.
- 따라서 추상화가 필요하면 인터페이스로
implements하거나 객체 지향 설계를 할땐 합성(composition)을 이용하는 것이 추세이다.
참조