Java에서 상속보다는 컴포지션(composition)을 활용하는 것을 권장하는 경우가 많습니다.
왜 상속보다 컴포지션을 권장하는지 어떤 상황에서는 상속을 사용하는 게 더 나은지 알아보도록 하겠습니다.
상속(inheritance)과 컴포지션(composition)
effective java item 18장을 보면 상속보다는 컴포지션을 사용하기를 권장한다.
왜 그럴까?
이번 장에서의 상속은 클래스가 다른 클래스를 확장하는 구현 상속을 의미한다.
인터페이스가 다른 인터페이스를 확장하는 인터페이스 상속과는 무관하다.
- class와 object들의 관계를 설정하는 데 사용되는 두가지에대해서 알아보자.
- 상속은 한 클래스를 다른 클래스에서 derive 즉 파생 시킨다.
- ex) extend 받은 확장된 클래스가 파생됨
- 컴포지션은 parts 즉 클래스를 구성하는 부분의 합으로 정의한다
- ex) 클래스 필드 내에 private or public 필드로 클래스의 인스턴스를 참조하게 하고
해당 클래스를 구성하는 부분의 합으로 정의됨.
클래스의 구성요소로 쓰인다는 뜻에서 composition이라고 한다.
- ex) 클래스 필드 내에 private or public 필드로 클래스의 인스턴스를 참조하게 하고
- 상속은 한 클래스를 다른 클래스에서 derive 즉 파생 시킨다.
- 상속 관계에서 상위 클래스 또는 슈퍼 클래스를 변경하면 코드 손상의 위험이 있기 때문에 상속을 통해서 생성된 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
http://www.yes24.com/Product/Goods/65551284
반응형
'Programming Language > Java' 카테고리의 다른 글
[Java] 람다(Lambda)를 소화시켜 보자 (0) | 2023.01.08 |
---|---|
[Java] Functional Interface(함수형 인터페이스) (0) | 2023.01.08 |
[Java] Equals vs Hashcode 그리고 재정의 (0) | 2023.01.08 |
[EffectiveJava] 정적 팩터리 메서드(Static Factory Method) 장단점 (1) | 2022.11.19 |
[Java] Interface(인터페이스)와 Abstract Class(추상클래스)를 비교해보자 (0) | 2022.11.19 |
댓글