[Java] 상속(inheritance)과 컴포지션(composition)에 대해서

곰민·2022년 11월 21일
2
post-thumbnail

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


이번 장에서의 상속은 클래스가 다른 클래스를 확장하는 구현 상속을 의미한다.
인터페이스가 다른 인터페이스를 확장하는 인터페이스 상속과는 무관하다.
느슨하게 결합된 코드는 더 많은 유연셩을 제공하기 때문에 상속보다는 컴포지션을 사용하는 것을 권장한다.
effective java에서도 상속보다는 컴포지션을 사용하기를 권장한다.(item 18)
하지만 권장이며 모든 프로그래밍 시나리오에 대해서 컴포지션을 사용하면 안된다.

1. class와 object의 관계를 설정하는 두 가지 방식


🚀 상속

상속은 extend를 활용하여 한 클래스를 다른 클래스에서 derive 즉 파생시킨다.
ex) animal이라는 클래스를 extend 받아 확장한 사람이라는 클래스가 파생된다.

public class human extends animal{
	//... do something
}
  • 상속 관계에서 상위 클래스 또는 슈퍼 클래스를 변경하면 코드 손상의 위험이 있기 때문에 상속을 통해서 생성된 class와 objects는 밀접하게 결합되어 있다(tightly coupled).
    • 상위 클래스는 릴리스마다 내부 구현이 달라질 수 있으며 그 여파로 코드 한 줄 건드리지 않은 하위 클래스가 오작동할 수도 있다.
  • 상위 클래스 설계자는 확장을 충분히 고려하고 문서화를 잘해두어야 한다.
    모두 같은 프로그래머가 통제하는 같은 패키지 안에서라면 변화하는 상위 클래스에 맞추어서 하위 클래스에 대한 수정도 유연하기 때문에 안전한 편이지만 상위 두 가지가 지켜지지 않는 경우에는 위와 같이 문제가 발생할 수 있다.

🚀 컴포지션

컴포지션은 parts 즉 클래스를 구성하는 부분의 합으로 정의한다
ex) 클래스 필드 내에 private or public 필드로 클래스의 인스턴스를 참조하게 하고
해당 클래스를 구성하는 부분의 합으로 정의된다.
클래스의 구성요소로 쓰인다는 뜻에서 composition이라고 한다.

public class House {
	private Bedroom bedroom;
    private LivingRoom livingRoom;
    //.. do something
}
  • 컴포지션의 경우 컴포지션을 통해 생성된 클래스와 객체는 느슨하게 결합되어(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).

🏠 예시

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의 컴포지션으로 사용이 가능하다.
    • A house has a living room (a living room is part of a 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 임을 확인할 수 있다.

정리


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

참조


https://www.infoworld.com/article/3409071/java-challenger-7-debugging-java-inheritance.html
http://www.yes24.com/Product/Goods/65551284

profile
더 나은 개발자로 성장하기 위해 퀘스트 🧙‍♂🧙‍♂ 깨는 중입니다. 관심사는 back-end와 클라우드입니다.

0개의 댓글