본문 바로가기

프로그래밍/Java

상속과 컴포지션

반응형

포스트를 작성하는 목적

상속과 컴포지션(조합)의 차이점에 대해서 알아본다. 개념상으로 이러이러한 차이점이 있고, 어떻게 표현되는지, 실제 코드로는 어떻게 표현할 수 있는지 알아본다. 그리고 왜 둘의 차이점을 알아야 하는지?.. 에 대해서 알아보자. 멘토링 피드백에 따라 엄청상세하게 공부하기 보다는, 추후 프로젝트 모델링 진행 시 상속과 컴포지션을 활용해 모델링 할 수 있는 정도가 되어보자.

상속과 컴포지션(조합) 이란?

  • 상속
    • IS-A관계로 정의될 수 있으며, 부모클래스를 '확장'하는 개념이다.
    • 상속을 받은 자식클래스는, 부모클래스의 변수와 메소드에 접근이 가능하고, 메소드를 재정의 할 수 있다. (생성자, static블록은 상속되지 않으며, private선언자는 접근이 불가능하고, final선언자는 재정의가 불가능하다. )
  • 컴포지션(조합)
    • HAS-A 관계로 정의될 수 있으며, 기존 클래스가 새로운 클래스의 구성요소가 되는 것이다.
    • 기존에 존재하는 객체를 맴버변수로 이용, 새로운 객체를 구현하는 방법

상속

  1. 코드의 재사용을 통해서 중복을 줄인다.
  1. 확장성이 증가한다.
  1. 클래 간의 계층적 관계를 구성함을써 다형성을 구현할 수 있다.
  • 이렇게 좋은 상속을 컴포지션(조합)과 구분해서 써야하는 이유는 무엇일까??→ 상속은 만능이 아니기 때문이다.

상속은

'캡슐화'를 깨트리는 치명적인 단점

이 있다. 상위클래스의 변수와 메소드가 하위클래스에게 노출 되기 때문이다. 이에 따라 하위클래스는 상위클래스에게 강하게 결합되고, 의존하게 되며 상위클래스의 내부구현이 달라지면, 수정사항이 없는 하위클래스가 오동작 할 수 있다.

💡
캡슐화란? → 클래스의 내부구현을 외부로부터 숨김으로서, 내부 변수에 대한 직접적인 접근을 막는다. 클래스간의 의존도를 낮추고, 독립적으로 수행되는 모듈화를 가능케 한다.

다음 아래의 코드로 상속의 문제점을 알아보자.

/**
 * The {@code Properties} class represents a persistent set of
 * properties. The {@code Properties} can be saved to a stream
 * or loaded from a stream. Each key and its corresponding value in
 * the property list is a string.
 * <p>
 * A property list can contain another property list as its
 * "defaults"; this second property list is searched if
 * the property key is not found in the original property list.
 * <p>
 * Because {@code Properties} inherits from {@code Hashtable}, the
 * {@code put} and {@code putAll} methods can be applied to a
 * {@code Properties} object.  Their use is strongly discouraged as they
 * allow the caller to insert entries whose keys or values are not
 * {@code Strings}.  The {@code setProperty} method should be used
 * instead.  If the {@code store} or {@code save} method is called
 * on a "compromised" {@code Properties} object that contains a
 * non-{@code String} key or value, the call will fail. Similarly,
 * the call to the {@code propertyNames} or {@code list} method
 * will fail if it is called on a "compromised" {@code Properties}
 * object that contains a non-{@code String} key.

/**
     * Calls the <tt>Hashtable</tt> method {@code put}. Provided for
     * parallelism with the <tt>getProperty</tt> method. Enforces use of
     * strings for property keys and values. The value returned is the
     * result of the <tt>Hashtable</tt> call to {@code put}.
     *
     * @param key the key to be placed into this property list.
     * @param value the value corresponding to <tt>key</tt>.
     * @return     the previous value of the specified key in this property
     *             list, or {@code null} if it did not have one.
     * @see #getProperty
     * @since    1.2
     */
    public synchronized Object setProperty(String key, String value) {
        return put(key, value);
    }

Properties클래스는 HashTable을 상속받는다. 그러나 이때문에 문제가 생기는데,

Properties클래스의 key,value 값은 String만 받도록 설계되어있다. 그러나 상위클래스의 put메소드는 value값을 object로 받기 때문에, put메소드 사용시 오류가 난다. 이에따라 Properties클래스에서는 value 값을 String으로 강제하는 setProperites를 사용해야하며, 부모클래스의 put메소드를 사용할 수 없는 Properties 클래스는 LSP원칙을 벗어난 클래스이다.

동시에, put메소드의 기능을 Properties에서도 활용하기위해, put메소드의 내부구현을 보고, 그에 맞는 setProperties()라는 메소드를 새로 추가함으로써, 캡슐화도 깨지게 된것을 볼 수 있다.

(Stack과 Vector역시 마찬가지이다. 추후에 확인해보자.)

Composition(조합)

기존의 클래스를 확장(상속)하는 대신, 새로운 클래스를 만들고 private필드로 기존 클래스의 인스턴스를 참조하게 하는 방법을 통해 기능 확장을 할 수있으며, 이를 Composition(조합||구성)이라 한다.

새 클래스의 인스턴스 메소드들은 (private 필드로 참조하는) 기존 클래스의 메소드를 호출, 그 결과를 반환(컨트롤러,서비스에서의 반환값을 생각해보자!!)

한다. 이 방식을 전달(forwardig)이라 하며, 이런 메소드들은 전달 메소드(forwarding method) 라 부른다.

이런 특성을 가진 컴포지션을 활용하면, 위의 상속에서의 문제점을 보완할 수 있다. 캡슐화를 깨지게 하는 문제와, 상위클래스와 하위클래스간에 생기는 강한 결합도와 의존도역시 낮출수 있다.

다음 코드를 보며 컴포지션(조합||구성)의 장점을 이해해 보자.

public class ForwardingSet<E> implements Set<E> {
    private final Set<E> set;
    public ForwardingSet(Set<E> set) { this.set = set; }
    public void clear() { set.clear(); }
    public boolean isEmpty() { return set.isEmpbty(); }
    public boolean add(E e) { return set.add(e); }
    public boolean addAll(Collection<? extends E> c) { return set.addAll(c); }
    // ... 생략
}

인스턴스 메소드들은 보면 전부 private final로 참조하는 set의 메소드를 반환 중이다. 이것이 '포워딩 메소드' 이다. 또한 set의 내부구현을 굳이 알필요 없으며, 내부구현이 바뀌어도 ForwardingSet클래스는 신경쓰지 않아도 된다. 캡슐화가 잘 지켜진 것을 볼 수 있다.

포스트를 작성하며 알게 된점

예전부터 막연히 상속은 좋은거야 ~ 라고만 생각했었는데, 상속은 생각보다 적용하기가 어렵다는걸 다시금 깨닫는다. 그동안 실제 업무를 하면서도 상속보단 컴포지션을 많이 사용해왔던것도, 이제 명확히 알겠다. 또한 상속의 문제점을 확인하면서 LSP에 위배되는 클래스도 의도치 않게 알게되었다.

Reference

상속(Inheritance) vs 컴포지션(Composition)

EffectiveJava Item 18. 상속보다는 컴포지션을 사용해라

 

반응형

'프로그래밍 > Java' 카테고리의 다른 글

ArrayList VS LinkedList  (0) 2021.05.21
java.lang -2 System클래스와 로깅  (0) 2021.05.20
java.lang -1 WrapperClass  (0) 2021.05.18
JAVA의 역사와 JVM-2  (0) 2021.05.18
GC  (0) 2021.05.18