본문 바로가기

IT/이펙티브자바 3E

[Effective Java] 아이템 18. 상속보다는 컴포지션을 이용하라

안녕하세요 남갯입니다.


오늘은 이펙티브 자바 


아이템 18. 상속보다는 컴포지션을 이용하라


에 대해 포스팅하려고 합니다.



상속은 코드를 재사용하는 강력한 수단이지만 항상 최선은 아니고 잘못사용하면 오류를 내기 쉽다.  여기서 말하는 상속은 구현상속을 말한다.(클래스가 다른 클래스를 상속하는 행위만을 말한다)

메서드 호출과 달리 상속은 캡슐화를 깨트린다.  


자식클래스는 부모클래스에 의존하기 때문에 부모클래스의 동작을 변경함에 따라 하위클래스가 오동작을 할 수 있다. 이렇게 상위클래스 설계자가 확장을 충분히 고려하고 문서화를 안하면 변경하지 않은 하위 클래스도 변경해야 한다.


HashSet을 사용하는 프로그램이 있고 이 성능을 올리기위해 변경된 코드를 만들었다.


public class InstrumentedHashSet<E> extends HashSet<E>{
private int addCount = 0;

public InstrumentedHashSet() {
}

public InstrumentedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
}

@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}

@Override
public boolean addAll(@NonNull Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}

public int getAddCount() {
return addCount;
}
}

 이클래스는 잘 구현된것처럼 보이지만 제대로 동작하지 않는다. addAll을 통해 3개의 원소를 더했다고 해보자


InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(Arrays.asList("1","22","333"));


getAddCount를 호출하면 3개를 반환하리라 기대했지만 6을 반환한다.  HashSet의 addAll메소드는 add메소를 통해 구현되었기 때문에 2번씩 호출되게 된다. 이렇게 자기 사용(addAll에 add를 이용한것)은 플랫폼의 정책일수도 그 후에도 유지 될지도 알 수 없습니다. 그래서 InstumentHashSet의 동작도 후에는 이상해질 수 있다. 다른식으로 addAll을 재정의도 가능하지만 여러가지 문제가 존재한다.


따라서 컴포지션과 전달방식으로 다시 코드를 구현해보자.


public class InstrumentedHashSet<E> extends ForwardingSet<E>{
private int addCount = 0;

public InstrumentedHashSet() {
}

public InstrumentedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
}

@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}

@Override
public boolean addAll(@NonNull Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}

public int getAddCount() {
return addCount;
}
}



public class ForwardingSet<E> implements Set<E>{
private Set<E> s;

public ForwardingSet(Set<E> s) {
this.s = s;
}

@Override
public void clear() {
s.clear();
}

//......
}


InstrumentedSet은 HashSe의 모든 기능을 정의한 Set인터페이스를 활용해 설계되어 견고하고 아주 유연하게 된다.


위와같이 구현한 클래스는 다른 Set인스턴스를 감사고 있다는 뜻에서 InstrumentedSet 같은 클래스를 래퍼 클래스라 하며 다른 Set계측 기능을 덧 씌운다는 뜻에서 데코레이터 패턴이라고한다. 


컴포지션과 전달의 조합은 넓은 의미로 위임이라고 부른다.  엄밀히 따지면 래퍼 객체가 내부 객체에 자기 자신의 참조를 넘기는 경우만 위임에 해당한다.


데코레이터 패턴에 대한 내용은 아래의 포스팅을 참조


2019/05/07 - [IT/개발스터디 디자인패턴] - [디자인패턴] 데코레이터 패턴




핵심

상속은 강력하지만 캡슐화를 해친다. 상속은 상위클래스와 하위클래스가 순수한 is-a 관계일 때만 써야한다. is-a 관계일 때도 안심할 수만은 없는 게, 상위클래스가 확장을 고려해서 설계 되지 않았기 때문에 문제가 될 수 있다. 따라서 상속의 취약점을 해결하기위해 상속 대신 컴포지션과 전달을 사용하면 된다. 래퍼클래스로 구현할 적당한 인터페이스가 있다면 더욱 그렇다. 래퍼 클래스는 하위 클래스보다 견고하고 강력하다.


즉 상속보단 구성을 이용해서 구현하는게 좋다