멈재

[JAVA] 18. 상속(IS-A) VS 구성(HAS-A) 본문

JAVA & Spring & JPA

[JAVA] 18. 상속(IS-A) VS 구성(HAS-A)

멈재 2023. 1. 2. 16:33
728x90

지난 유스콘 콘퍼런스의 OOP START! 세션을 들으면서 캡슐화, 상속의 쓰임을 새로 알게 되면서 객체 지향을 다시 꼼꼼히 알아보자는 생각에서 쓰게 되었다.

객체 지향 프로그래밍의 특징 중 하나로 코드의 재사용. 즉, 상속(inheritance)이 있다.
코드 재사용을 수행할 수 있는 방법은 두 가지가 있다.

  1. 상속의 구현(IS-A 관계)
  2. 객체 구성(HAS-A 관계)


일반적으로 흔히 아는 내용은 보통 이럴 것이다.

IS-A: ~은 ~이다.
ex) 강아지는 동물이다.

HAS-A: ~은(는) B를 가지고 있다.
ex) 자동차는 엔진을 가지고 있다.


내가 처음 이 말을 들었을 당시 되게 추상적으로 다가왔다.

~은 ~이면 IS-A 관계를....
~은 ~를 가지고 있으면 HAS-A 관계를...
.
그래서.... 왜?



자바 기본서를 볼 때 뿐만 아니라, UML 클래스 다이어그램을 정리할 때도 그랬다.
UML 다이어그램의 관계에 연관 관계(Association Relationships)의 세부 종류로 집합 관계(Aggregation Relationship, a.k.a. HAS-A)를 배울 때도 그랬다.

이미지 출처:https://bbmsk2.tistory.com/34



이정도의 배경 지식을 가지고 있다가 콘퍼런스 세션에서 아차. 싶었던 부분이 있었다.

단순히 코드를 재사용할 목적으로 상속 관계를 사용하면 안 된다.


스프링에서 줄곧 HAS-A를 써왔는데 누군가에게 코드를 재사용하는 방법이 뭐가 있냐라는 질문이 온다면 아마 상속만을 얘기했을 것이다.

그래서 상속을 왜 적절히 써야 하는 지를 알아보려 한다.

우리가 상속을 사용하는 이유는 다음과 같을 것이다.

1. 코드를 재사용함으로써 중복을 줄일 수 있다.
2. 변화에 대한 유연성이 증가한다.
3. 개발 시간이 단축된다.

하지만, 위와 같은 상속을 사용하면서 갖는 이점은 적절한 상황에 적절히 사용했을 때이다.
만약, 상속이 적절하지 않은 상황에 사용한다면 결합도가 높아져서 변화에 유연하지 않게 된다.

한 가지 예시로, 최근 보고 있는 헤드퍼스트 디자인 패턴의 1장 중 일부를 예시로 들어보겠다.

헤드퍼스트 디자인 패턴

위 이미지에 대해 간단히 설명하면 다음과 같다.

처음 요구사항에는 모든 오리들이 꽥 소리(quack())를 낼 수 있고, 헤엄(swim())을 칠 수 있는 기능과 각 오리별로 정보를 나타내는(display()) 기능을 가지고 있었다.

기존 요구사항 구현이 마치고 일정 시간이 흐른 뒤에 새로운 요구사항이 들어왔고, 그 요구사항은 실제 오리는 날 수 있어야(fly()) 한다는 것이다.

이름에도 알 수 있듯이 RubberDuck(고무 모형 오리)는 날지 못해야 한다.
그러나 위 예시처럼 상속을 사용한 경우라면 고무 모형 오리도 날아다니는 메서드를 가지게 되는 문제점이 발생하게 된다.

뿐만 아니라, 상속 관계에서 상위 클래스가 가지는 인스턴스 변수의 타입이 하나라도 바뀌게 되면 하위 클래스 전부를 변경해줘야 하는 불상사가 일어나게 된다.


이와 같은 예시가 상속이 적절하지 않은 예시가 될 것이다.

이처럼 상속은 하위 클래스가 상위 클래스에 강하게 의존, 결합하기 때문에 변화에 유연하게 대처하지 어려워진다.
만약, 상속 구조가 깊으면 깊을수록 그 문제(side effect)는 더욱 심할 것이다.


이젠 상속을 적절히 사용해야 한다는 걸 명확히 알 게 되었을 것이다.

처음에 코드를 재사용하는 방법으로 상속의 구현(IS-A)과 객체 구성(HAS-A) 두 가지 방법이 존재한다고 했던 것이 기억이 나는가?
각각에 대해 설명하기에 앞서 각각 코드로 나타내면 어떻게 되는 지를 먼저 봐보겠다.

// IS-A
public class ApplePhone {
    // ...
}

public class IPhone14 extends ApplePhone {
    // ...
}
// HAS-A
public class School {
    private Student student;
    private Teacher teacher;

    public School(Student student, Teacher teacher) {
        this.student = student;
        this.teacher = teacher;
    }
    ...
}

public class Student { ... }
public class Teacher { ... }

이처럼 상속하는 것이 아닌 School의 인스턴스 변수로 Student 클래스와 Teacher 클래스를 가지는 것이 조합(Composition)이다.

IS-A와 HAS-A를 정리하면 다음과 같다.

IS-A 관계
전적으로 상속에 기반하여 클래스 또는 인터페이스 상속의 두 가지 유형을 말하고, 단방향이다.
"A는 B 유형의 것이다"라고 하는 것과 동일하다. 예를 들어, 집은 건물이다. 그러나 건물은 집이 아닌 것처럼 말이다.

HAS-A 관계
컴포지션(HAS-A)은 단순히 다른 객체에 대한 참조인 인스턴스 변수의 사용을 의미한다.
예를 들어, 집에는 화장실이 존재하고, 차는 엔진을 가지고 있다.

그래서 컴포지션(HAS-A)을 사용한다면 다음과 같은 이점을 갖게 된다.

1. 메서드를 호출하는 방식으로 동작되기 때문에 캡슐화를 깨뜨리지 않는다.
2. Student나 Teacher 같은 기존 클래스의 변화에 영향이 적어진다.

즉, 상속의 문제점들에서 벗어날 수 있는 방법이다.

Q. 그렇다면 그냥 전부 HAS-A로 구성하는 것이 좋은 거 아닌가요?
A. X


처음에도 말했듯 상속이 적절히 사용된다면 조합보다 강력하고, 개발하기 편리해진다는 장점을 가지게 된다.
다만 상속이 적절히 사용되려면 최소 다음과 같은 조건을 만족해야 한다.

  1. 확장을 고려하고 설계한 확실한 IS-A 관계
  2. API에 아무런 결함이 없는 경우 (만약 결함이 있다면 하위 클래스까지 전파돼도 괜찮은 경우)


그렇다면 어느 상황에 IS-A 관계를 사용하는 것이 적절한가를 묻는다면 위 조건을 만족하는 상황일 것이다.

이미지 출처:https://zangzangs.tistory.com/44


위와 같은 상황이 IS-A 관계를 사용하는 것이 바람직할 것이다.


IS-A와 HAS-A의 바인딩 시점 차이도 존재한다.
상속정적 바인딩(컴파일 타임 바인딩)이고, 컴포지션(구성)동적 바인딩(런타임 바인딩)이다.




끝으로, 처음 글을 쓰게 된 의도는 누군가에게 자바에서 코드 재사용에 대해 설명할 때 상속만을 말하지 않기 위함이었다.

결국 핵심은 단지 코드 재사용만을 위해 상속을 사용하지 말라는 것이었다. 정말 코드 재사용을 원하는 것이라면 IS-A 관계를 따져보고 사용하라는 것이다.

또한, 다형성만을 위해 상속을 사용하지 않는 것이 좋다고 한다.
IS-A 관계(상속)에 적합하지 않다면, 인터페이스와 컴포지션 관계를 사용하는 것을 권장한다고 한다.



참고
https://bbmsk2.tistory.com/34
https://www.linkedin.com/pulse/inheritance-composition-is-a-vs-has-a-relationship-java-omar-ismail/
https://tecoble.techcourse.co.kr/post/2020-05-18-inheritance-vs-composition/
https://www.w3resource.com/java-tutorial/inheritance-composition-relationship.php
헤드퍼스트 디자인 패턴 https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=582754