본문 바로가기
Java

[Java] 상속 vs 컴포지션

by 곰민 2022. 11. 12.
728x90

Java에서 상속보다는 컴포지션(composition)을 활용하는 것을 권장하는 경우가 많습니다.
왜 상속보다 컴포지션을 권장하는지 어떤 상황에서는 상속을 사용하는 게 더 나은지 알아보도록 하겠습니다.

 

상속(inheritance)과 컴포지션(composition)


effective java item 18장을 보면 상속보다는 컴포지션을 사용하기를 권장한다.
왜 그럴까?
이번 장에서의 상속은 클래스가 다른 클래스를 확장하는 구현 상속을 의미한다.
인터페이스가 다른 인터페이스를 확장하는 인터페이스 상속과는 무관하다.

 

  • class와 object들의 관계를 설정하는 데 사용되는 두가지에대해서 알아보자.
    • 상속은 한 클래스를 다른 클래스에서 derive 즉 파생 시킨다.
      • ex) extend 받은 확장된 클래스가 파생됨
    • 컴포지션은 parts 즉 클래스를 구성하는 부분의 합으로 정의한다
      • ex) 클래스 필드 내에 private or public 필드로 클래스의 인스턴스를 참조하게 하고
        해당 클래스를 구성하는 부분의 합으로 정의됨.
        클래스의 구성요소로 쓰인다는 뜻에서 composition이라고 한다.
  • 상속 관계에서 상위 클래스 또는 슈퍼 클래스를 변경하면 코드 손상의 위험이 있기 때문에 상속을 통해서 생성된 class와 objects는 밀접하게 결합되어 있다(tightly coupled)
    즉 상위 클래스는 릴리스마다 내부 구현이 달라질 수 있으며 그 여파로 코드 한 줄 건드리지 않은 하위 클래스가 오작동할 수도 있다.
    • 상위 클래스 설계자는 확장을 충분히 고려하고 문서화를 잘해두어야 한다
    • 모두 같은 프로그래머가 통제하는 같은 패키지 안에서라면 변화하는 상위 클래스에 맞추어서 하위 클래스에 대한 수정도 유연하기 때문에 안전한 편이다.
    • 하지만 상위 두 가지가 지켜지지 않는 경우에는 위와 같이 문제가 발생할 수 있다.
  • 컴포지션의 경우 컴포지션을 통해 생성된 클래스와 객체는 느슨하게 결합되어(loosely coupled) 코드를 손상시키지 않고 구성 요소들을 바꿀 수 있다.

 

Java에서 상속을 사용하는 경우


상위 클래스와 하위 클래스가 “ is a ” 관계인 경우. “is a kind of” 가 좀 더 명확하다
• A cat is an animal , A cat is a kind of an animal 
• A car is a  vehicle, A car is a kind of a vehicle 
해당 관계에서의 서브클래스, 하위 클래스는 상위 클래스의 specialized version이다
즉 원본 버전과 동일하나 좀 더 구체화한, 좀 더 확장한, 좀 더 특별한 버전 
슈퍼 클래스, 상위 클래스를 상속하는 것은 코드 재사용의 예시라고 볼 수 있다. 
상속관계를 구성할 때는 subclass 가 superclass의 specialized version 인지 아닌지 체크하는 게 중요하다. 
간단한 예시로 Vehicle과 Car의 예시를 보도록 하자.

 

예시

class Vehicle {

    String brand;
    String color;
    double weight;
    double speed;

    void move() {
        System.out.println("The vehicle is moving");
    }
}

public class Car extends Vehicle {
    String licensePlateNumber;
    String owner;
    String bodyStyle;

    public static void main(String... inheritanceExample) {
        System.out.println(new Vehicle().brand);
        System.out.println(new Car().brand);
        new Car().move();
    }
}

 

Java에서 컴포지션을 사용하는 경우


one object가 또 다른 object를 “has” or “is part of” 하는 경우 컴포지션을 사용할 수 있다.
A car has a battery (a battery is part of a car).
A person has a heart (a heart is part of a person).
A house has a living room (a living room is part of a house).
house 예시가 좋은 예가 되어준다.

 

예시

public class CompositionExample {

    public static void main(String... houseComposition) {
        new House(new Bedroom(), new LivingRoom());
        // The house now is composed with a Bedroom and a LivingRoom
    }

    static class House {

        Bedroom bedroom;
        LivingRoom livingRoom;

        House(Bedroom bedroom, LivingRoom livingRoom) {
            this.bedroom = bedroom;
            this.livingRoom = livingRoom;
        }
    }
    static class Bedroom { }
    static class LivingRoom { }
}
  • house에는 침실과 거실이 있기 때문에 bedroom과 livingroom object를 house의 컴포지션으로 사용이 가능하다.

 

예시로 다시 한번 확인해 보자


상속에 대한 예시가 맞을까?

import java.util.HashSet;

public class CharacterBadExampleInheritance extends HashSet<Object> {

    public static void main(String... badExampleOfInheritance) {
        BadExampleInheritance badExampleInheritance = new BadExampleInheritance();
        badExampleInheritance.add("Homer");
        badExampleInheritance.forEach(System.out::println);
    }
  • 매우 적합하지 않다
    • HashSet을 상속받아놓고 BadExampleInheritance에 대한 인스턴스를 생성 후 사용한다.
    • 즉 하위 클래스인 CharacterBadExampleInheritance는 사용하지 않은 많은 메서드를 상속받으므로 혼란스럽고 유지하기 어려운 tightly coupled code 즉 결합도가 높은 코드가 생성된다.
    • 당연히 자세히 보면 “is a”, “is a kind of ”관계도 성립 안됨.

 

컴포지션으로 바꾼다면?

import java.util.HashSet;
import java.util.Set;

public class CharacterCompositionExample {
    static Set<String> set = new HashSet<>();

    public static void main(String... goodExampleOfComposition) {
        set.add("Homer");
        set.forEach(System.out::println);
    }
  • 위 코드 시나리오에서 컴포지션을 사용하면 CharacterCompositionExample는 HashSet을 상속하지 않고 HashSet의 메서드 중 단지 두 가지만 사용하게 된다.
  • 그 결과 단순하고 이해하기 쉽고 유지하기 쉬운 less coupled code 즉 결합도가 낮은 코드가 생성된다.

 

JDK에서 확인할 수 있는 상속에 대한 좋은 예시


예시

class IndexOutOfBoundsException extends RuntimeException {...}
class ArrayIndexOutOfBoundsException extends IndexOutOfBoundsException {...}
class FileWriter extends OutputStreamWriter {...}
class OutputStreamWriter extends Writer {...}
interface Stream<T> extends BaseStream<T, Stream<T>> {...}
  • IndexOutOfBoundsException 이 RuntimeException을 상속받은 예시와 같이 하위 클래스는 상위 클래스의 specialized version 임을 확인할 수 있다.

 

Java 상속에서의 Method Overriding

상속이 정말 잘 작동하려면 새로운 서브 클래스에서 상속받은 동작중 일부를 변경할 수도 있어야 한다.
  • sound를 specialize 하는 예시
class Animal {
    void emitSound() {
        System.out.println("The animal emitted a sound");
    }
}
class Cat extends Animal {
    @Override
    void emitSound() {
        System.out.println("Meow");
    }
}
class Dog extends Animal {
}

public class Main {
    public static void main(String... doYourBest) {
        Animal cat = new Cat(); // Meow
        Animal dog = new Dog(); // The animal emitted a sound
        Animal animal = new Animal(); // The animal emitted a sound
        cat.emitSound();
        dog.emitSound();
        animal.emitSound();
    }
}
  • Animal을 클래스 타입으로 선언했지만 Cat이라는 인스턴스화를 시킬 때에는 meow라는 sound를 얻게 된다.

 

상속은 강력하지만 취약점이 존재한다.
상황에 맞춰서 상속 또는 컴포지션을 잘 활용하자.

 

참조


https://www.infoworld.com/article/3409071/java-challenger-7-debugging-java-inheritance.html

 

Java inheritance vs. composition: How to choose

Compare inheritance and composition, the two fundamental ways to relate Java classes, then practice debugging ClassCastExceptions in Java inheritance.

www.infoworld.com

http://www.yes24.com/Product/Goods/65551284

 

이펙티브 자바 Effective Java 3/E - YES24

자바 플랫폼 모범 사례 완벽 가이드 - Java 7, 8, 9 대응자바 6 출시 직후 출간된 『이펙티브 자바 2판』 이후로 자바는 커다란 변화를 겪었다. 그래서 졸트상에 빛나는 이 책도 자바 언어와 라이브

www.yes24.com

 

728x90

댓글