ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이펙티브 자바 Item 18 - 상속보다는 컴포지션을 사용하라
    언어/Effective Java 2024. 5. 31. 10:35

     

    1. 구체 클래스 상속의 위험성 

     

    • 다른 패키지의 구체 클래스를 상속하는 일은 위험하다 (인터페이스 상속말고, 구현 상속)
    • 메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.
      • 상위 클래스에 따라 하위클래스의 동작에 문제가 생길 수 있기 때문이다.

    하위클래스가 깨지기 쉬운 이유 

    1. 상위 클래스의 메서드를 재정의 하여 하위 클래스의 로직을 방어한다
      1. 상위 클래스의 메서드를 동작을 다시 구현하는게 어렵다.
      2. 오류나 성능을 떨어뜨릴 수도 있다.
      3. 하위 클래스에서 접근 불가한 private 클래스를 써야한다면 구현이 불가능하다.
    2. 상위 클래스 릴리즈에서 새로운 메서드를 추가했을 때를 고려해야함
      1. 하위 클래스에서 허용되지 않은 원소를 추가할 지도 모른다!! 
    3. 하위 클래스에 추가한 새 메서드가, 상위 클래스 다음 릴리즈에서 같은 시그니처를 가질 때 
      1. 시그니처가 같고 반환 타입이 다르다면 컴파일조차 안된다. -> 부모를 다 따져야함 개열받음

    2. 컴포지션 설계    

     

    • 새로운 클래스를 만들고 private필드로 기존 클래스의 인스턴스를 참조한다. 
    • 컴포지션(composition) : 기존 클래스를 확장하는 대신에 새로운 클래스를 만들고, private필드로 기존 클래스의 인스턴스 참조하게 하는 설계
    • 전달(forwarding) : 새 클래스의 인스턴스 메서드들은 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환
      • 전달 메서드(forwarding method) : 새 클래스의 메서드 

     

    public class InheritSet<E> extends HashSet<E> {
    
    	private int addCount = 0;
    
    	@Override
    	public boolean add(E e) {
    		addCount++;
    		return super.add(e);
    	}
    
    	@Override
    	public boolean addAll(Collection<? extends E> c) {
    		addCount += c.size();
    		return super.addAll(c);
    	}
    
    	public int getAddCount() {
    		return addCount;
    	}
    }
    
    	//아래는 테스트를 위한 코드 4을 예상했지만 결과는 8이다.
    		InheritSet<String>stringInheritSet = new InheritSet<>();
    		stringInheritSet.addAll(Arrays.asList("한방", "두방", "세방", "네방"));
    		assertEquals(8, stringInheritSet.getAddCount());
    • HashSet을 상속받은 InhefitSet 클래스의 객체를 사용하여, 원소가 몇 개 더해졌는지 알기 위한 addCount Field를 추가하고 add(), addAll() 메서드를 사용할 때마다 증가시키기로 했다. 

    출처: https://velog.io/@sseob/inheritancevscomposition

    • 테스트가 실패한 이유를 찾아보자, HashSet은 위와 같은 상속 구조를 가지고 있다.
    • 예제 코드에서 호출하는 addAll() 메소드는 AbstractCollection 클래스의 메서드인데, 내부적으로 add()를 사용하는 방식으로 구현되어 있다. 
    • 따라서 addAll()을 호출하면 addCount에 원서 size를 더하고, super.addAll()이 add()를 호출하는데, 이 또한 오버라이딩 되어 있음으로, 현재 클래스그의 add를 호출한다. 결과적으로 4번이 중복되어 8이라는 결과를 얻는다. 

    2.1 컴포지션 사용 

    public class InstrumentedSet<E> extends ForwardingSet<E> {
    
    	private int addCount = 0;
        
        public InstrumentedSet(Set<E> set){
        	super(set);
        }
        
        @Override
        public boolean add(E e){
        	addCount++;
            return super.add(e);
        }
        
        @Override
    	public boolean addAll(Collection<? extends E> c) {
    		addCount += c.size();
    		return super.addAll(c);
    	}
    
    	public int getAddCount() {
    		return addCount;
    	}
    }
    
    public class ForwardingSet<E> implements Set<E> {
    
    	private final Set<E> set;
    	
    	public ForwardingSet(Set<E> set) {this.set = set;}
    	@Override
    	public int size() {return set.size();}
    	@Override
    	public boolean isEmpty() {return set.isEmpty();}
    	@Override
    	public boolean contains(Object o) {return set.contains(o);}
    	@Override
    	public Iterator<E> iterator() {return set.iterator();}
    	@Override
    	public Object[] toArray() {return set.toArray();}
    	@Override
    	public <T> T[] toArray(T[] a) {return set.toArray(a);}
    	@Override
    	public boolean add(E e) {return set.add(e);}
    	@Override
    	public boolean remove(Object o) {return set.remove(o);}
    	@Override
    	public boolean containsAll(Collection<?> c) {return set.containsAll(c);}
    	@Override
    	public boolean addAll(Collection<? extends E> c) {return set.addAll(c);}
    	@Override
    	public boolean retainAll(Collection<?> c) {return set.retainAll(c);}
    	@Override
    	public boolean removeAll(Collection<?> c) {return set.removeAll(c);}
    	@Override
    	public void clear() {set.clear();}
    	@Override
    	public boolean equals(Object o) {return set.equals(o);}
    	@Override
    	public int hashCode() {return set.hashCode();}
    }
    • InstrumentedSet 클래스는 ForwardingSet 클래스를 확장하여 add와 addAll 메서드를 ForwardingSet의 메서드를 사용한다. ForwardingSet은 Set 인터페이스를 구현했다.
    • ForwardingSet은 HashSet을 private final 필드로 참조한다. 이를 Composition이라고 할 수 있다.
    • ForwardingSet 클래스는 Set 인터페이스의 모든 메소드를 재정의하고 있는데, 재정의 내용은 기존의 Set 인터페이스의 메소드를 그냥 반환하고 있다. (전달 메소드이다.)
    • 기능의 확장을 일궈내면서, 상위 클래스에 기능 변경에 영향을 받지 않으며, 기존 클래스와 기능을 똑같이 사용

     

    • InstrumentedSet 클래스는  Set에 기능을 덧씌운다는 뜻에서 데코레이터 패턴이라고 부른다.
      • 다른 Set 인스턴스를 감싸고 있다는 뜻에서 InstrumentedSet 같은 클래스를 래퍼 클래스라 부른다.
      • Composition과 Forwarding 조합은 넓은 의미로 위임이라고 한다. 

    3. 데코레이터 패턴 

     

    • 원본의 행위 인터페이스 정의 
    • 이를 구현한 원본객체와 추상 데코레이터 클래스 정의 
    • 추상 데코레이터에 원본 객체를 연관관계로 가지고, 원본 행위 + 추가 행위 메서드 정의 (추가 행위는 추상 메서드)
    • 이를 구현한 데코레이터 콘크리트 생성

     

    interface Component{
    	void operation(); // 공통 책임
    }
    
    class ConcreteComponent implements Component{
    	public void operation(){
        	//원본의 행위 
        }
    }
    abstract class Decorator implements Component{ 
       //구체 장식 책임 중 origin의 operation 호출까지만
       // 장식 클래스는 변화 가능, 구체 클래스에 느슨한 의존관계를 위해..	
        Component origin;
        
        Decorator(Component origin){
        	this.origin = origin;
        }
    	public void operation(){
        	origin.operation; 
        }
        
        public void extraop();
    
    }
    • 생성자는 콘크리트에서 호출하려고 만들어 두었다. 
    class ComponentDecorator1 extends Decorator{
    	ComponentDecorator1(Component co){
        	super(co); //부모생성자 호출로 origin초기화
        }
        
        public void operation() {
        	super.operation();
            extraop();
        }
        
        public void extraop(){
        	//장식할 행위
        }
    }
    • 콘크리트는 Component를 받아서 필드에 넣고, 추가 행위만 재정의

    핵심은 원본과 같은 인터페이스 구현 후 원본을 필드로 받아서, 원본 행위 + 추가 행위 구현한다는 것 

     


    정리

    • 상속은 반드시 하위 클래스가 상위 클래스의 진짜 하위 타입인 상황에서만 쓰여야 한다.
    • 클래스 A를 상속하는 클래스 B가 정말 A인가를 생각해보자 
      • 확장하려는 클래스의 API에 아무런 결함이 없는가? -> 결함이 하위 클래스까지 전파돼도 괜찮은가?
      • 상속은 상위 클래스의 API를 그 결함까지도 승계한다. 
    • 될 수 있으면 컴포지션을 사용하자, 특히 래퍼 클래스로 구현할 적당한 인터피이스 (Set)이 있다면 더 그렇다 래퍼 클래스는 하위 클래스보다 견고하고, 강력하다.! 
Designed by Tistory.