2466 단어
12 분
[ OOP ] 상속(inheritance)과 컴포지션(composition)

객체지향 프로그래밍(OOP)에서 상속과 컴포지션은 필수적인 개념이다. 상속은 오래전부터 OOP의 핵심으로 여겨졌고, 한때는 상속 없이는 객체지향이 아니라고 할 정도로 중요하게 다뤄졌다. 하지만 시간이 흐르면서 상속 대신 컴포지션이 더 적합하다는 의견도 많아졌다.

그럼에도 상속과 컴포지션은 단순히 한쪽이 더 우월한 개념이라기보다는, 상황에 따라 다르게 접근해야 하는 도구들이다. 이 글에서는 상속과 컴포지션을 비교하고, 언제 어떤 방식을 선택해야 하는지 알아볼 것이다.



상속이란?#

상속은 기존 클래스(부모 클래스)를 기반으로 새로운 클래스(자식 클래스)를 만드는 방법이다. 자식 클래스는 부모 클래스의 모든 속성과 메서드를 그대로 물려받으며, 필요에 따라 새로운 기능을 추가할 수 있다. 이를 통해 코드를 재사용하고 중복을 줄이는 것이 상속의 목적이다.

예를 들어, “학생(Student)” 클래스는 “사람(Person)” 클래스를 상속받아 이름, 나이와 같은 공통 속성을 물려받고, 여기에 학번 같은 학생 고유의 속성을 추가할 수 있다.

// 부모 클래스 Person
class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    void introduce() {
        System.out.println("이름: " + name + ", 나이: " + age);
    }
}

// 자식 클래스 Student
class Student extends Person {
    String studentId;

    Student(String name, int age, String studentId) {
        super(name, age);
        this.studentId = studentId;
    }

    @Override
    void introduce() {
        super.introduce();
        System.out.println("학번: " + studentId);
    }
}

public class Main {
    public static void main(String[] args) {
        Student student = new Student("홍길동", 20, "123456");
        student.introduce();  // 부모 클래스의 메서드도 사용 가능하고, 자식 클래스에서 확장된 메서드도 동작함
    }
}

이 코드에서 Student 클래스는 Person 클래스를 상속받아 nameage 같은 속성을 물려받고, 추가로 studentId 속성을 확장하고 있다. 부모 클래스의 introduce 메서드를 오버라이드하여 학번도 출력할 수 있다.

상속은 OOP의 또 다른 중요한 개념인 다형성을 구현하는 기반이 된다. 부모 클래스에서 정의한 메서드를 자식 클래스가 자신만의 방식으로 재정의(오버라이딩)함으로써, 다양한 객체들이 같은 이름의 메서드를 각기 다르게 동작하게 할 수 있다.


상속 vs 컴포지션#

상속과 컴포지션은 둘 다 코드를 재사용하는 방법이다. 상속이 부모-자식 관계로 코드를 물려주는 방식이라면, 컴포지션은 여러 개체를 조합해서 새로운 기능을 만드는 방식이다. 상속은 is-a 관계에 적합하고, 컴포지션은 has-a 관계에 적합하다.

예를 들어, “자동차(Car)“는 “엔진(Engine)“을 가지고 있다(has-a). 이런 관계는 컴포지션이 자연스럽다

// 엔진 클래스
class Engine {
    void start() {
        System.out.println("엔진이 시작됩니다.");
    }
}

// 자동차 클래스 (컴포지션 사용)
class Car {
    private Engine engine;

    Car() {
        engine = new Engine();
    }

    void startCar() {
        engine.start();
        System.out.println("자동차가 출발합니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.startCar();  // 자동차가 엔진을 사용하여 동작
    }
}

이 코드에서는 Car 클래스가 Engine 클래스를 포함(컴포지션)하여 Car는 엔진의 기능을 내부적으로 사용한다. Car는 **엔진을 가지고 있다(has-a)**라는 관계를 표현한다.



상속 vs 컴포지션: 언제, 왜 사용해야 할까?#

프로그래밍에서 상속과 컴포지션은 객체 간의 관계를 설계할 때 중요한 도구다. 하지만 언제 상속을 써야 하고, 언제 컴포지션을 써야 할까? 두 개념을 선택할 때 고려해야 할 몇 가지 상황과 그 이유를 정리해보자.


기계적인 차이 때문에 하나를 골라야 할 때#

이건 주로 성능과 관련된 부분인데, CPU와 메모리 간의 데이터 전송에서 병목현상이 생길 수 있다. CPU는 데이터를 빠르게 처리하기 위해 캐시 메모리를 사용하는데, 여기서 상속과 컴포지션이 다르게 작동할 수 있다.


상속 모델#

Inheritance Model

상속을 사용한 객체는 하나의 덩어리처럼 캐시 메모리에 들어가기 때문에 한 번에 필요한 데이터를 모두 불러올 가능성이 크다. 이 경우, 데이터를 빠르게 접근할 수 있어 성능이 더 좋을 수 있다.


컴포지션 모델#

Composition Model

반면 컴포지션을 사용하면 객체가 여러 부품으로 나뉘어 있고, 각 부품이 따로 메모리에 로딩될 가능성이 있다. 부품이 많을수록 캐시 히트율이 낮아져 성능이 떨어질 수 있다.

또한, 메모리 할당과 해제 측면에서도 차이가 있다. 상속은 객체가 생성되고 삭제될 때 딱 한 번의 메모리 할당과 해제가 이루어진다. 하지만 컴포지션은 각 부품마다 추가적인 메모리 할당과 해제가 발생하기 때문에, 부품 수가 많아질수록 성능에 부정적인 영향을 줄 수 있다.


용도 때문에 상속을 선택해야 할 때#

다형성을 구현할 때는 상속을 피할 수 없다. 다형성(polymorphism)은 부모 클래스의 메서드를 자식 클래스에서 오버라이드해 재정의하는 패턴인데, 이건 상속으로만 구현 가능하다. 컴포지션은 객체의 기능을 확장하거나 재정의하는 데 한계가 있기 때문에, 다형성이 필요한 경우라면 상속을 선택해야 한다.

// 동물 클래스
class Animal {
    void sound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

// 고양이 클래스 (상속 및 다형성 적용)
class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("고양이가 야옹합니다.");
    }
}

// 강아지 클래스 (상속 및 다형성 적용)
class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("강아지가 멍멍합니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myCat = new Cat();
        Animal myDog = new Dog();

        myCat.sound();  // 고양이가 야옹합니다.
        myDog.sound();  // 강아지가 멍멍합니다.
    }
}

이 예시에서 CatDog 클래스는 Animal 클래스를 상속받아 각각의 sound 메서드를 오버라이드하였다. 이런 경우에는 상속만 사용 가능하다.


관리의 효율성을 고려할 때#

관리 측면에서도 상속과 컴포지션은 차이가 있다. 상속은 계층 구조를 만들기 때문에, 이 구조가 깊지 않은 경우 관리가 비교적 쉽다. 부모 클래스에서 변경된 사항이 자식 클래스에 자동으로 반영되기 때문에, 일관성을 유지하기가 좋다.

하지만 상속 계층이 너무 깊어지면 오히려 복잡해진다. 이런 경우, 컴포지션이 더 유리할 수 있다. 컴포지션은 독립적인 부품을 조립하는 방식이기 때문에, 상속 구조가 깊어질수록 발생할 수 있는 복잡한 의존성을 줄여준다.


일반적인 경우#


is-a 관계 vs has-a 관계#

상속과 컴포지션을 언제 사용할지 판단하는 데 있어서, 가장 기본적인 원칙은 “is-a” 관계와 “has-a” 관계를 이해하는 것이다.

  • is-a 관계: 상속을 쓸 때는 자식 클래스가 부모 클래스의 일종인 경우에 사용한다. 예를 들어, “고양이”는 “동물”이다. 이런 경우 고양이는 동물의 특성을 상속받아 사용하는 것이 적절하다. 즉, 고양이는 동물의 특성을 재사용하거나 확장하면서도, 동물이라는 큰 카테고리 안에 속하게 된다.
  • has-a 관계: 컴포지션을 사용할 때는 객체가 다른 객체를 ‘소유’하고 있는 경우를 나타낸다. 예를 들어, “자동차”는 “엔진”을 가진다. 자동차가 엔진의 일종이 아닌 것처럼, 엔진은 자동차의 일부분일 뿐이다. 이런 경우, 자동차는 엔진을 부품으로 가지고 있으면서도, 엔진과 자동차는 독립적으로 존재할 수 있다.

이 원칙을 기억하면 객체 간의 관계를 더 명확하게 설계할 수 있다. 상속은 is-a 관계일 때 적합하고, 컴포지션은 has-a 관계를 표현할 때 적절하다.

상속과 컴포지션 중 하나를 선택할 때 중요한 것은 두 개념이 서로 배타적이지 않다는 점이다. 기본적으로 is-a 관계에서는 상속을, has-a 관계에서는 컴포지션을 사용하는 것이 자연스럽다. 하지만 이 가이드라인이 절대적인 것은 아니다. 상황에 맞게 상속과 컴포지션을 조합하여 사용함으로써 더 유연하고 효율적인 설계를 할 수 있다.




출처

  • 개체지향 프로그래밍 및 설계 - 유데미 강의
[ OOP ] 상속(inheritance)과 컴포지션(composition)
https://blog-full-of-desire-v3.vercel.app/posts/oop/-oop--inheritance-composition/
저자
SpeculatingWook
게시일
2024-10-17
라이선스
CC BY-NC-SA 4.0