Contents

상속(Inheritance) vs 합성(Composition)

객체지향 프로그래밍에서 재사용성을 높이는 방법은 크게 두 가지다. 부모의 기능을 물려받는 상속과, 필요한 기능을 가진 객체를 내부에 포함하는 합성이다. 현대 소프트웨어 설계에서는 왜 “상속보다는 합성(Composition over Inheritance)“을 권장하는지 그 이유를 파헤쳐 본다.

1. 상속 (Inheritance): “Is-A” 관계

상속은 하위 클래스가 상위 클래스의 특성을 그대로 이어받는 방식이다. ‘기본 클래스의 확장’이라는 개념으로 접근한다.

특징

  • Is-A 관계: “강아지는 동물이다(Dog is an Animal)“와 같은 논리적 계층 구조를 가진다.
  • 화이트박스 재사용: 상위 클래스의 내부 구현이 하위 클래스에 노출된다.

장점

  • 코드를 그대로 물려받으므로 초기 개발 속도가 빠르다.
  • 다형성을 통해 상위 클래스 타입으로 하위 클래스를 다룰 수 있다.

단점 (한계)

  • 강한 결합도(Tight Coupling): 부모 클래스의 변경이 모든 자식 클래스에 영향을 미친다.
  • 불필요한 기능 상속: 부모의 메서드 중 자식에게는 부적절한 기능까지 강제로 물려받게 된다.
  • 유연성 부족: 컴파일 타임에 관계가 결정되므로 실행 중에 로직을 바꾸기 어렵다.

2. 합성 (Composition): “Has-A” 관계

합성은 새로운 클래스를 만들 때, 다른 객체를 필드(인스턴스 변수)로 참조하여 기능을 사용하는 방식이다.

특징

  • Has-A 관계: “자동차는 엔진을 가지고 있다(Car has an Engine)“와 같은 포함 관계를 가진다.
  • 블랙박스 재사용: 내부 구현이 노출되지 않으며, 인터페이스를 통해서만 소통한다.

장점

  • 낮은 결합도: 포함된 객체의 내부 구현이 바뀌어도 영향을 거의 받지 않는다.
  • 실행 시점의 유연성: 런타임에 포함된 객체(구현체)를 교체함으로써 동작을 동적으로 변경할 수 있다.
  • 단일 책임 원칙: 각 클래스가 한 가지 역할에 집중할 수 있도록 잘게 쪼개기 유리하다.

단점

  • 상속에 비해 클래스 간의 관계를 파악하는 데 더 많은 코드를 읽어야 할 수 있다.
  • 객체 생성 비용이 다소 증가할 수 있다.

3. 코드 비교: 상속 vs 합성

상속 방식 (Inheritance)

부모 클래스의 결함이나 변화가 자식에게 그대로 전이된다.

1
2
3
4
5
6
7
8
class Robot {
    void move() { System.out.println("걷는다"); }
}

class CleanRobot extends Robot {
    // 걷는 기능 외에 청소 기능만 추가하고 싶지만, 
    // 부모의 'move' 방식에 강하게 결속된다.
}

합성 방식 (Composition)

이동 전략을 인터페이스로 분리하여 필요할 때 갈아 끼운다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
interface MoveStrategy { void move(); }

class Walk implements MoveStrategy { public void move() { System.out.println("걷는다"); } }
class Fly implements MoveStrategy { public void move() { System.out.println("날아간다"); } }

class Robot {
    private MoveStrategy moveStrategy; // 합성

    public Robot(MoveStrategy strategy) { this.moveStrategy = strategy; }
    
    void performMove() { moveStrategy.move(); }
}

4. 왜 ‘합성’이 대세인가?

현대 소프트웨어는 변화에 대한 유연성을 가장 중요하게 여긴다.

  • 캡슐화 파괴 방지: 상속은 부모의 내부를 자식에게 노출시키지만, 합성은 객체의 경계를 명확히 지킨다.
  • 다중 상속의 문제 회피: 대부분의 언어는 다중 상속을 금지한다. 합성을 사용하면 여러 개의 기능을 자유롭게 조합할 수 있다.
  • 테스트 용이성: 합성된 객체는 가짜 객체(Mock)로 갈아 끼우기 쉬워 단위 테스트 작성이 훨씬 수월하다.

결론: 언제 무엇을 쓸 것인가?

무조건 상속이 나쁜 것은 아니다. 하지만 다음 기준을 고려해야 한다.

  • 상속을 쓰는 경우: 명확한 계층 구조가 있고, 부모의 모든 퍼블릭 인터페이스가 자식에게도 완벽히 부합할 때(Liskov Substitution Principle).
  • 합성을 쓰는 경우: 단순히 코드의 재사용이 목적이거나, 런타임에 동적으로 기능을 바꿔야 할 때.

결국, 설계의 기본은 합성으로 잡고 상속은 꼭 필요한 경우에만 신중하게 도입하는 것이 현대적인 프로그래밍의 정석이다.