금요일, 3월 29
Shadow

#006 클래스와 인터페이스 두번째

자바에서는 클래스와 인터페이스를 설계하는데 사용할 수 있는 강력한 요소들을 제공한다. 이 장에서는 우리가 만드는 클래스와 인터페이스가 쓸모있고 강력하며 유연성이 있도록 하기 위해 도움을 주는 지침을 설명한다.


가급적 상속(inheritance)보다는 컴포지션(composition)을 사용하자

상속의 문제점

동일한 프로그래머가 서브클래스와 수퍼 클래스의 구현을 관장하는 같은 패키지 내에서 상속을 사용하는 것은 안전하다.
또한 상속을 위해 특별히 설계되고 문서화된 클래스를 확정하기 위해 상속을 사용하는 것도 안전하다.
그러나 다른 패키지에 걸쳐 일반적인 실체 클래스로부터 상속을 받는 것은 위험하다.

메소드 호출과는 달리 상속은 캡슐화를 위배한다.

// 상속을 잘못 사용한 예!
import java.util.*;

public class InstrumentedHashSet extends HashSet {
    // 요소를 추가한 횟수
    private int addCount = 0;

    public InstrumentedHashSet() {
    }

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

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

    @Override public boolean addAll(Collection c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }

    public static void main(String[] args) {
        InstrumentedHashSet s =
            new InstrumentedHashSet();
        s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));    
        System.out.println(s.getAddCount());
    }
}

---------- execute ----------
6

getAddCount 메소드를 호출하면 3이 반환될 것으로 예상하겠지만 실제로는 6이 반환된다.
addAll 메소드는 HashSet의 상위 슈퍼 클래스에 구현되어 있으며, 내부적으로 add 메소드를 호출하게 되어 있기 때문이다.

// AbstractCollection.java addAll 메소드 발췌
public abstract class AbstractCollection implements Collection {

    public boolean addAll(Collection c) {
	boolean modified = false;
	Iterator e = c.iterator();
	while (e.hasNext()) {
	    if (add(e.next()))
		modified = true;
	}
	return modified;
    }
}

InstrumentedHashSet의 addAll 메소드를 호출하면서 addCount에 3이 더해진 후, super.addAll의 호출에 의해 HashSet의 addAll 메소드가 호출 된다.
이때 다시 add 메소드가 호출되는데, 이 메소드는 InstrumentedHashSet에서 오버라이딩 하였으므로 InstrumentedHashSet.add 메소드가 호출된다.
결국 addCount에 1씩 세 번이 추가로 더해져 최종 값은 6이 된다.


상속의 대안

기존 클래스의 인스턴스를 참조하는 private 필드를 새로운 클래스에 둔다.
이런 식의 설계를 컴포지션(Composition)이라고 하는데, 그 이유는 기존 클래스가 새 클래스의 컴포넌트로 포함되기 때문이다.
새 클래스의 각 인스턴스 메소드에서는 포함된 기존 클래스 인스턴스의 대응되는 메소드를 호출하여 결과를 반환할 수 있다.
이것을 포워딩(forwarding)이라고 하고 새 클래스의 메소드를 포워딩 메소드라 한다.

// Wrapper class - 상속 대신 컴포지션을 사용한다.
import java.util.*;

public class InstrumentedSet extends ForwardingSet {
    private int addCount = 0;

    public InstrumentedSet(Set s) {
        super(s);
    }

    @Override public boolean add(E e) {
        addCount++;
        return super.add(e);
    }
    @Override public boolean addAll(Collection c) {
        addCount += c.size();
        return super.addAll(c);
    }
    public int getAddCount() {
        return addCount;
    }

    public static void main(String[] args) {
        InstrumentedSet s =
            new InstrumentedSet(new HashSet());
        s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));    
        System.out.println(s.getAddCount());
    }
}
// 재사용 가능한 포워딩 클래스

import java.util.*;

public class ForwardingSet implements Set {
    private final Set s;
    public ForwardingSet(Set s) { this.s = s; }

    public void clear()               { s.clear();            }
    public boolean contains(Object o) { return s.contains(o); }
    public boolean isEmpty()          { return s.isEmpty();   }
    public int size()                 { return s.size();      }
    public Iterator iterator()     { return s.iterator();  }
    public boolean add(E e)           { return s.add(e);      }
    public boolean remove(Object o)   { return s.remove(o);   }
    public boolean containsAll(Collection c)
                                   { return s.containsAll(c); }
    public boolean addAll(Collection c)
                                   { return s.addAll(c);      }
    public boolean removeAll(Collection c)
                                   { return s.removeAll(c);   }
    public boolean retainAll(Collection c)
                                   { return s.retainAll(c);   }
    public Object[] toArray()          { return s.toArray();  }
    public  T[] toArray(T[] a)      { return s.toArray(a); }
    @Override public boolean equals(Object o)
                                       { return s.equals(o);  }
    @Override public int hashCode()    { return s.hashCode(); }
    @Override public String toString() { return s.toString(); }
}

상속을 위한 설계와 문서화를 하자. 그렇지 않다면 상속의 사용을 금지시킨다

– 문서화
오버라이드 가능한 메소드들의 자체 사용(self-use) 즉, 같은 클래스의 메소드들를 호출하는지에 대해 반드시 문서화해야 한다.
이와는 반대로, 각각의 public이나 protected 메소드 및 생성자가 어떤 오버라이드 가능한 메소드를 호출하는지, 어떤 순서로 하는지, 호출한 결과가 다음 처리에 어떤 영향을 주는지에 대해서도 반드시 문서화해야 한다.

-상속 사용 금지
서브 클래스를 안전하게 만들 수 있도록 설계나 문서화되지 않은 클래스의 상속을 금지하는 것이다.
첫번째 방법은 클래스를 final로 선언하는 것이고, 두번째 방법은 모든 생성자를 private이나 패키지 전용으로 하고 생성자 대신 public static 팩토리 메소드를 추가하는 것이다.


추상 클래스보다는 인터페이스를 사용하자

추상 클래스와 인터페이스의 차이점

추상 클래스로 정의된 타입을 구현하는 클래스는 반드시 추상 클래스의 서브 클래스가 되어야 한다는 것이다.
이와 달리 인터페이스를 구현하는 클래스의 경우는 인터페이스에 정의된 모든 메소드를 구현하고 인터페이스 구현 계약을 지키면 된다.

인터페이스의 장점

1. 믹스인(mixin)을 정의하는데 이상적이다.
믹스인은 클래스가 자신의 “본래 타입”에 추가하여 구현할 수 있는 타입으로써, 선택 가능한 기능을 제공하고, 그 기능을 제공받고자 하는 클래스에서 선언한다.
예. Comparable은 믹스인 인터페이스로써, 상호 비교 가능한 다른 객체와의 비교를 통해 클래스의 인스턴스가 정렬된다는 것을 그 클래스에서 선안할 수 있다.
2. 비계층적인 타입 프레임워크를 구축할 수 있게 해준다.
3. 안전하고 강력한 기능 향상을 가능하게 해준다.
4. 외부에 공개한 각각의 중요한 인터페이스와 연관시킨 골격 구현(skeleton implementation) 추상 클래스를 제공하여 인터페이스와 추상 클래스의 장점을 결합할 수 있다.

번외 추상 골격 클래스 예제

추상 골격 클래스는 이름은 거창하지만 사실 프로그래밍 하다보면 자신도 모르게 구현해봤을 것이다.
책에서는 skeletal implementation 으로 표현하고 있다. 하지만 실전 오픈소스 구현에서는 Skeleton~ 으로 네이밍하지 않고 Abstract 의 네이밍이 더 많이 보인다.
어쨌든 추상골격클래스의 생성 타이밍은 언제가 좋을까. 경험상 아래와 같은 경우이다.

다수의 객체를 생성할 때 그 객체가 한 기능적 그룹으로 묶일 수 있다
구현체가 공통된 기능(intrinsc)과 공통적이지 않은 기능(extrinsic)으로 나뉠 수 있다
공통된 기능을 매번 객체마다 구현하는 것이 비효율적이다

이럴 경우 추상 골격 클래스를 만들어 두면 객체 생성시 일종의 뼈대를 제공하여 객체 생성에 부담을 덜고 코드량도 줄일 수 있게 해준다.
초등학교 미술 시간때 찰흙을 가지고 사람을 만들어 본 사람은 뼈대가 얼마나 도움이 되는지 알 것이다.
가령 매우 복잡한 기능을 제공하는 인터페이스가 있다고 가정해보자.

ackage design.pattern;

public interface 매우복잡한인터페이스 {

	public void 복잡한기능();
	public void 객체의특성을타는기능();

	public void 간단한기능();	
	public void 어려운기능();
	public void 퇴근기능();
	public void 해도안해도그만인기능();

}

매우 복잡한 기능들로 채워진 인터페이스이며 이 인터페이스 구현체를 다수 코딩해야 한다고 가정한다.
그럴 경우 매번 저 7개의 인터페이스를 계속 구현해야 한다. 만일 구현체가 기능중 몇가지가 같은 기능(intrinsic) 이라면 구현체 수만큼 코드가 반복된다.
이럴 경우 도움이 되는 것이 추상 골격 클래스이다.
위에서 만일 객체별 구현(extrinsic) 이 복잡한기능 과 객체의특성을타는기능 메서드이고 나머지가 공통된 기능(intrinsic) 이라고 가정하면 추상 골격 클래스 디자인은 다음과 같이 나올 수 있다.

package design.pattern;

public abstract class 매우복잡한인터페이스골격 implements 매우복잡한인터페이스 {

	// public abstract void 객체의특성을타는기능();
	// public abstract void 퇴근기능();

	private void 복잡한일하기() { /* ... */ }
	private void 간단한일하기() { /* ... */ }
	private void 어려운일하기() { /* ... */ }

	@Override
	public void 복잡한기능() {
		간단한일하기();
		복잡한일하기();
	}

	@Override
	public void 간단한기능() {
		간단한일하기();
	}

	@Override
	public void 어려운기능() {
		간단한일하기();
		복잡한일하기();
		어려운일하기();
	}

	// 해도 안해도 그만인 기능이므로 구현하고 싶다면 구현하고 안해도 상관이 없다.
	@Override
	public void 해도안해도그만인기능() {
		throw new UnsupportedOperationException();
	}
}

위에 말한 구현체별 기능은 무시하고 나머지 기능을 구현했으며 추가로 해도안해도그만인기능 은 그냥 예외를 던지도록 해서 구현체가 필요에 따라 선택적으로 구현할 수 있게 두었다.
그렇다면 실제 구현체는 인터페이스의 모든 기능을 구현할 필요 없이 두가지만 구현하면 바로 사용할 수 있는 클래스가 된다.

package design.pattern;

public class 간단해진클래스 extends 매우복잡한인터페이스골격 {

	@Override
	public void 객체의특성을타는기능() {
		// 자신만의 기능 구현
	}

	@Override
	public void 퇴근기능() {
		// 퇴근
	}
}

하지만 이 골격 클래스 구현은 매우 신중해야 하는데, 태생 자체가 상속을 기본에 둔 클래스이기 때문에, 편하다고 기능을 마구 골격클래스에서 구현하게 되면 앞서 설명된 상속의 늪에 빠져버릴 수 있다.
실제 구현체는 반드시 추상 골격 클래스를 상속하지 않아도 구현체로서 동작할 수 있게 되어야지 모든 구현체가 골격 클래스를 반강제 상속하게 되는 구조라면 골격 클래스를 만들지 말고 그냥 온전한 상속 클래스의 디자인을 고려하는 것이 낫다.
골격 클래스는 단지 구현에 용이성을 주기 위한 수단이어야지 실제 구현체에서 구현 기능을 줄이기 위한 수단이 되어서는 안된다!


타입을 정의할 때만 인터페이스를 사용하자

어떤 클래스에서 인터페이스를 구현할 때, 그 인터페이스는 그 클래스의 인스턴스를 참조하는데 사용될 수 있는 타입(type)의 역할한다.
이외의 다른 목적으로 인터페이스를 정의하는 것은 적합하지 않다. 상수를 외부에 제공하기 위해 사용하면 안 된다.
자바 플랫폼 라이브러리에는 다수의 상수 인터페이스가 있다. 예를 들면, java.io.ObjectStreamConstants이다.
이런 인터페이스는 이례적인 것으로 여겨야 하지 따라하면 안된다.

// 상수 인터페이스 안티패턴(antipattern) 이렇게 사용하지 말자!
public interface PhysicalConstants {
  // 아보가드로(Avogadro) 상수 (1/mol)
  static final double AVOGADROS_NUMBER   = 6.02214199e23;

  // 볼쯔만(Boltzmann ) 상수 (J/K)
  static final double BOLTZMANN_CONSTANT = 1.3806503e-23;

  // 전자 질량(Mass of the electron) (kg)
  static final double ELECTRON_MASS      = 9.10938188e-31;
}

상수를 외부에 제공하고 싶을 때 적절한 방법

1. 만일 어떤 상수가 기존 클래스나 인터페이스와 밀접하게 연관된다면, 그 상수를 해당 클래스나 인터페이스에 추가한다.
예. Integer나 Double와 같은 박스화 기본형 클래스에 있는 MIN_VALUE, MAX_VALUE 상수

package java.lang;

import java.util.Properties;

public final class Integer extends Number implements Comparable {
    public static final int   MIN_VALUE = 0x80000000;
    public static final int   MAX_VALUE = 0x7fffffff;

   ... // 나머지 코드 생략

2. enum 타입 (항목 30)
3. 인스턴스를 생성할 수 없는 유틸리티 클래스 (항목 4)

 
// 상수 유틸리티 클래스
package com.effectivejava.science;

public class PhysicalConstants {
    private PhysicalConstants() { }  // Prevents instantiation

    public static final double AVOGADROS_NUMBER   = 6.02214199e23;
    public static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
    public static final double ELECTRON_MASS      = 9.10938188e-31;
}

유틸리티 상수를 사용할 때 PhysicalConstants.AVOGADROS_NUMBER와 같이 사용한다.
만일 유틸리티 클래스의 상수를 무척 많이 사용한다면, static import 문(자바 1.5 배포판에 추가됨)을 사용해서 상수명 앞에 클래스를 붙이지 않게 할 수 있다.

import static com.effectivejava.science.PhysicalConstants.*;

public class Test {
     double atoms(double mols) {
        return AVOGADROS_NUMBER * mols;
    }
    ... // 나머지 코드 생략
}

creative by javacafe

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.