4장 — 클래스와 인터페이스

규칙 13 클래스와 멤버의 접근 권한은 최소화하라

  • 정보 은닉
    • 시스템을 구성하는 모듈 사이의 의존성을 낮춰서(decouple), 각자 개별적으로 개발하고, 시험하고, 최적화하고, 이해하고, 변경할 수 있도록 한다는 사실에 기초
    • 좋은 성능을 자동적으로 보장하는 것은 아니지만, 효과적인 성능 튜닝(tuning)을 가능하게 하는 것은 사실
    • 소프트웨어의 재시용 가능성을 높인다. 모듈간 의존성이 낮으므로 각 모듈은 다른 소프트웨어 개발에도 유용하게 쓰일 수 있음
    • 대규모 시스템 개발 과정의 위험성(risk)도 낮춤
  • 캡슐화 (encapsulation)
  • 각 클래스와 멤버는 기능한 한 접근 불가능하도록 만들라는 것.
  • private - 이렇게 선언된 멤버는 선언된 최상위 레벨 클래스 내부에서만 접근 가능하다.
  • package-private - 이렇게 선언된 멤버는 같은 패키지 내의 아무 클래스나 사용할 수 있다. 기본 접근 권한(default access)으로 알려져 있는데, 멤버를 선언할 때 아무런 접근 권한 수정자(access modifier)도 붙이지 않으면, 이 권한이 주어지기 때문.
  • protected - 이렇게 선언된 멤버는 선언된 클래스 및 그 하위 클래스만 사용할 수 있다(몇 가지 제약 사항에 대해서는 [JLS, 6.6.2] 참고). 선언된 클래스와 같은 패키지에 있는 클래스에서도 시용이 기능하다.
  • public - 이렇게 선언된 멤버는 어디서도 사용이 가능하다.
  • 메서드의 접근 권한을 줄일 수 없는 경우가 하나 있다. 상위 클래스 메서드를 재정의할 때는 원래 메서드의 접근 권한보다 낮은 권한을 설정할 수 없다.

  • 객체 필드(instance field)는 절대로 public으로 선언하면 안 된다.
  • 변경 가능 public 필드를 가진 클래스는 다중 스레드에 안전하지않다.
  • 요약
    • public static final 배열 필드를 두거나, 배열 필드를 반환하는 접근지(accessor)를 정의하면 안 된다.
    • 접근 권한은 가능한 낮추라. 최소한의 pubic API를 설계한 다음, 다른 모든 클래스, 인터페이스, 멤버는 API에서 제외하라.
    • public static final 필드를 제외한 어떤 필드도 public 필드로 선언하지 마라.
    • public static final 필드가 참조하는 객체는 변경 불가능 객체로 만들라.
  • Q : 정보 은닉 VS 캡슐화
  • Q : 최상위 레벨 클래스 란?
  • Q : 변경 가능 public 필드를 가진 클래스는 다중 스레드에 안전은 하지 못한 이유 혹은 사례
import java.util.ArrayList;

public class ThreadNonSafe implements Runnable {

	private int index;
	private Member member;

	public static final Number[] numbers = {1, 2, 3, 4, 5};
    public static final List<String> numberList;
    public static final List<String> unmodifiableListNumberList;

    static {
        numberList = Arrays.asList("one", "two", "three");
        //numberList = new ArrayList<>(Arrays.asList("one", "two", "three"));
        unmodifiableListNumberList = Collections.unmodifiableList(numberList);
    }

	public ThreadNonSafe(int index, Member member) {

    	//numberList.add("four");
    	//numberList.remove("four");
    	//unmodifiableListNumberList.add("four");

		this.index = index;
		this.member = member;
	}

	@Override
	public void run() {
		try {
			member.age++;
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println(this.index + " " + member.age);
	}


	public static void main(String[] args) throws InterruptedException {
		ArrayList<Thread> threads = new ArrayList<Thread>();
		Member member = Member.builder().id(1L).name("Genius").age(37).build();
		for (int i = 0; i < 5000; i++) {
			Thread t = new Thread(new ThreadNonSafe(i, member));
			t.start();
			threads.add(t);
		}

		for (int i = 0; i < threads.size(); i++) {
			Thread t = threads.get(i);
			try {
				t.join();
			} catch (Exception e) {
			}
		}

		System.out.println(member.age);
	}
}
import lombok.Builder;

@Builder
public class Member {
	public long id;
	public String name;
	public int age;
}

규칙 14 public 클래스 안에는 public 필드를 두지 말고 접근자 메서드를 사용하라

  • 선언된 패키지 밖에서도 사용 가능한 클래스에는 접근자 메서드를 제공하라.
// 이런 저급한 클래스는 절대로 public으로 선언하지 말 것
class Point {
  public double x;
  public double y;
}
// 접근자 메서드와 수정자룰 이용한 데이터 캡슐화
class Point {
  public double x;
  public double y;

  public Point(double x, double y) {
    this.x = x;
    this.y = y;
  }

  public double getX() { return x; }
  public double getY() { return y; }
  public void setX(double x) { this.x = x; }
  public void setY(double y) { this.y = y; }
}
  • package-private 클래스나 private 중첩 클래스(nested class)는 데이터필드를 공개하더라도 잘못이라 말할 수 없다.
  • 중첩 클래스 사용 이유 Nested Classes
    • 한 곳에서만 사용되는 클래스를 논리적으로 그룹화하는 방법입니다. 한 클래스가 다른 한 클래스에 유용하면 그 클래스에이 클래스를 포함시키고 두 클래스를 함께 유지하는 것이 논리적입니다. 이러한 “헬퍼 클래스”를 중첩하면 패키지가보다 간결 해집니다.
    • 캡슐화를 증가시킵니다. A와 B라는 두 개의 최상위 클래스를 생각해 봅니다. 여기서 B는 그렇지 않으면 private으로 선언 될 A의 멤버에게 액세스해야합니다. A 클래스 내의 B 클래스를 숨김으로써 A 멤버는 비공개로 선언 될 수 있고 B 멤버는 비공개로 선언 될 수 있습니다. 또한 B 자체는 외부 세계로부터 숨길 수 있습니다.
    • 보다 읽기 쉽고 유지 보수가 쉬운 코드로 이어질 수 있습니다. 최상위 클래스의 작은 클래스를 중첩하면 코드가 사용되는 위치에 가깝게 배치됩니다.
  • Q : 이런 클래스는 데이터 필드를 직접 조작할 수 있어서 캡슐화의 이점을 누릴 수가 없다(규칙 13). API를 변경하지 않고서는 내부 표현을 변경할 수 없고, 불변식(invariant)도 강제할 수 없고, 필드를 사용하는 순간에 어떤 동작이 실행되도록 만들 수도 없다.
  • Q : private 중첩 클래스 란 ?

규칙 15 변경 가능성을 최소화하라

  • 변경 불가능 클래스 규칙
    • 객체 상태를 변경하는 메서드(수정자 메서드 등)를 제공하지 않는다.
    • 계승할 수 없도록 한다.
    • 모든 필드를 final로 선언한다.
    • 모든 필드를 private로 선언한다.
    • 변경 기능 컴포넌트에 대한 독점적 접근권을 보장한다.
  • 변경 불가능 객체는 단순하다.
  • 변경 불가능 객체는 스레드에 안전, 어떤 동기화도 필요 없음
  • 변경 불가능한 객체는 자유롭게 공유할 수 있다.
  • 변경 불기능한 객체는 그 내부도 공유할 수 있다.
  • 변경 불가능 객체는 다른 객체의 구성요소로도 홀륭하다.
  • 변경 불기능 객체의 유일한 단점은 값마다 별도의 객체를 만들어야 한다는 점이다.
  • Q : BitSet 클래스는 백만 개 비트 가운데 한 비트의 상태를 상수 시간(constant time)에 변경할 수 있도록 한다.
  • Q : 예를 들어 BigInteger 클래스는 package-private로 선언된 변경 가능 “동료 클래스”(companion Class)를 사용해 모듈라 멱승(modular exponentiation) 같은 연산의 속도를 높인다.

규칙 16 계승하는 대신 구성하라

  • 메서드 호출과 달리, 계승은 캡슐화(encapsulation) 원칙을 위반한다.
  • 계승은 하위 클래스가 상위 클래스의 하위 자료형(subtype)이 확실한 경우에만 바람직하다.
  • 구성(composition) : 기존 클래스를 계승하는 대신, 새로운 클래스에 기존 클래스 객체를 참조하는 private 필드를 하나 두는 것, 기존 클래스가 새 클래스의 일부가 됨
  • 전달(forwarding) : 새로운 클래스에 포함된 각각의 메서드는 기존 클래스에 있는 메서드 가운데 필요한 것을 호출해서 그 결과를 반환
  • 전달 메서드 (forwarding method) : 전달 기법을 사용해 구현된 메서드
  • 계승은 하위 클래스가 상위 클래스의 하위 자료형(subtype)이 확실한 경우에만 바람직하다. 다시 말해서, 클래스 B는 클래스 A와 “IS-A” 관계가 성립할 때만 A를 계승해야 한다.
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;

public class InstrumentedHashSet<E> extends HashSet<E> {

	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<? extends E> c) {
		addCount += c.size();
		return super.addAll(c);
	}

	public int getAddCount() {
		return addCount;
	}

	public static void main(String[] args) {
		InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
		s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
		System.out.println(s.getAddCount());
	}
}
  • Q : 기술적으로 보자면, 포장 객체가 자기 자신을 포장된(wrapped) 객체에 전달하지 않으면 위임이라고 부를 수 없다.
  • A : To achieve the same effect with delegation, the receiver passes itself to the delegate to let the delegated operation refer to the receiver. 참고

  • Q : 역호출(callback) 프레임워크와 함께 사용하기에는 적합하지 않다.
  • A : 참고

규칙 17 계승을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 계승을 금지하라

  • 재정의 가능 메서드를 내부적으로 어떻게 사용하는지(self-use) 반드시 문서에 남기라는 것이다.
  • 클래스 내부 동작에 개입할 수 있는 훅(hooks)을 신중하게 고른 protected 메서드 형태로 제공해야 한다.
  • 계승을 위해 설계한 클래스를 테스트할 유일한 방법은 하위 클래스를 직접 만들어 보는 것이다.

규칙 18 추상 클래스 대신 인터페이스를 사용하라

  • 이미 있는 클래스를 개조해서 새로운 인터페이스를 구현하도록 하는 것은 간단하다.
  • 인터페이스는 믹스인(mixin)을 정의하는 데 이상적이다.
  • 인터페이스는 비 계층적인(nonhierarchical) 자료형 프레임워크(type framework)를 만들 수 있도록 한다.
  • 인터페이스를 사용하면 포장 클래스 숙어(wrapper class idiom)을 통해 안전하면서도 강력한 기능 개선이 기능하다.
  • 추상 골격 구현(abstract skeletal implementation) 클래스를 중요 인터페이스마다 두면, 인터페이스의 점과 추상 클래스의 장점을 결합할 수 있다.
  • 인터페이스보다는 추상 클래스가 발전시키기 쉽다
  • 인터페이스가 공개되고 널리 구현된 다음에는, 인터페이스 수정이 거의 불기능

규칙 19 인터페이스는 자료형을 정의할 때만 사용하라

  • 상수 인터페이스 패턴은 인터페이스를 잘못 사용한 것이다.
  • 자바 플랫폼 라이브러리에도 상수 인터페이스가 몇 개 있음 (java.io.ObjectStreamConstants 이런 인터페이스는 실수로 포함된 것이라 생각해야 하며, 절대로 따라해서는 안 됨)

규칙 20 태그 달린 클래스 대신 클래스 계층을 활용하라

  • 태그 기반 클래스(tagged class)는 너저분한데다 오류 발생 가능성이 높고, 효율적이지도 않다.
  • 태그 기반 클래스는 클래스 계충을 얼기설기 흉내 낸 것일 뿐이다.

규칙 21 전략을 표현하고 싶을 때는 함수 객체를 사용하라

  • 함수 객체의 주된 용도는 전략 패턴(Strategy pattern)을 구현
  • 패턴을 구현하기 위해서는 전략을 표현하는 인터페이스를 선언하고, 실행 기능 전략 클래스가 전부 해당 인터페이스를 구현하도록야 함
  • 실행 가능 전략이 한 번만 시용되는 경우에는 보통 그 전략을 익명 클래스 객체로 구현
  • 반복적으로 시용된다면 private static 멤버 클래스로 전략을 표현한 다음, 전략 인터페이스가 자료형인 public static final 필드를 통해 외부에 공개하는 것이 바람
// 실행 가능 전략들을 외부에 공개하는 클래스
class Host {

	private static class StrLenCmp implements Comparator<String>, Serializable {

        public int compare(String s1, String s2) {
            return s1.length() - s2.length();
        }
    }

  // 이 비교자는 직렬화가 가능
  public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp();

  // 나머지 생략
}

규칙 22 멤버 클래스는 가능하면 static으로 선언하라

  • 중첩 클래스(nested class)
    • 다른 클래스 안에 정의된 클래스
    • 해당 클래스가 속한 클래스 안에서만 사용
  • 중첩 클래스 정적 멤버 클래스 (static member class), 비-정적 멤버 클래스(nonstatic member class), 익명 클래스(anonymous class), 그리고 지역 클래스(local class)
  • 내부 클래스(inner class) 비-정적 멤버 클래스(nonstatic member class), 익명 클래스(anonymous class), 그리고 지역 클래스(local class)
  • 정적 멤버 클래스
    • 가장 간단한 중첩 클래스
    • 바깥 클래스의 모든 멤버에 (private로 선언된 것까지도) 접근할 수 있음
    • 바깥 클래스의 정적 멤버이며, 다른 정적 멤버와 동일한 접근 권한 규칙(accessibility rule)을 따름
    • private로 선언했다면 해당 중첩 클래스에 접근할 수 있는 것은 바깥 클래스
    • 흔한 용례 가운데 하나는, 바깥 클래스와 함께 사용할 때 만 유용한 public 도움 클래스(helper class)를 정의
    • 바깥 클래스 객체와 독립적으로 존재할 수 있음
    • private 정적 멤버 클래스는 바깥 클래스 객체의 컴포넌트(component)를 표현한느데 흔히 쓰임
  • 비-정적 멤버 클래스
    • 문법적으로 보자면 정적 멤버 클래스와 비-정적 멤버 클래스의 차이는 멤버클래스 앞에 static이라는 키워드가 붙는다는 것
    • 바깥 클래스 객체와 자동적으로 연결
    • 바깥 클래스의 메서드를 호출할 수도 있고, this 한정(qualified this) 구문을 통해 바깥 객체에 대한 참조를 획득할 수도 있음
    • 바갇 클래스 객체 없이는 존재할 수 없음
    • 비-정적 멤버 클래스 객체와 바깥 객체와의 연결은 비-정적 멤버 클래스의 객체가 만들어지는 순간에 확립되고, 그 뒤에는 변경할 수 없음
    • 어댑티(Adapter)를 정의할 때 많이 쓰임
  • 비-정적 멤버 클래스 안에서는 비깥 클래스의 메서드를 호출할 수도 있고, this 한정(qualified this) 구문을 통해 바깥 객체에 대한 참조를 획득할 수도 있다.
  • 바깥 클래스 객체에 접근할 필요가 없는 멤버 클래스를 정의할 때는 항상 선언문 앞에 static을 붙여서 비-정적 멤버 클래스 대신 정적 멤버 클래스로 만들자.
  • 익명 클래스
    • 이름이 없음
    • 바깥 클래스의 멤버가 아님
    • 사용하는 순간에 선언하고 객체를 만듬
  • 지역 클래스
    • 지역 변수가 선언될 수 있는 곳이라면 어디서든 선언 할 수 있음
    • 지역 변수와 동일한 유효범위(scoping rule) 규칙을 따름