본문 바로가기

Dev Book Review/Effective Java 3판

[Effective Java] Item15. 클래스와 멤버의 접근 권한을 최소화하라

잘 설계된 컴포넌트란?

1) 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 얼마나 잘 숨겼느냐 (캡슐화)

2) 노출되는 API와 실제 구현이 얼마나 잘 분리되었는지

3) 메시지를 주고받는 두 컴포넌트가 서로의 내부 동작을 신경쓰지 않는지

1. 정보 은닉의 장점

  • 컴포넌트끼리의 구현을 서로 몰라도 되기 때문에 병렬로 개발이 가능하여 개발 속도가 빨라진다.
  • 컴포넌트가 잘 분리되어 있기 때문에 빨리 파악하여 디버깅할 수 있고, 다른 컴포넌트로의 교체 비용도 하락한다.
  • 다른 컴포넌트에 영향을 주지 않고 해당 컴포넌트만 최적화할 수 있도록 도와준다.
  • 외부 컴포넌트에 거의 의존하지 않기 때문에 소프트웨어 재사용성을 높인다.
  • 개별 컴포넌트의 동작을 검증할 수 있기 때문에 큰 시스템을 개발하는 난이도를 낮춰준다.

2. 정보 은닉의 기본 원칙

모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다.
달리 말해서 소프트웨어가 동작하는 한 항상 가장 낮은 접근 수준을 부여해야 한다.

1) 톱레벨 클래스, 인터페이스

public: 공개 API / package-private: 해당 패키지 안에서만 이용

 

패키지 외부에서 쓸 이유가 없다면 package-private으로 선언!

덕분에 클라이언트에 아무런 피해 없이 다음 릴리스에서 수정, 교체, 제거

2) private static 중첩 클래스

한 클래스에서만 사용하는 package-private 톱레벨 클래스/인터페이스를 private static 중첩으로 작성시켜본다.

그렇게 하면 바깥 클래스 하나에서만 접근할 수 있게 된다.

3) 멤버 접근성

  • private: 멤버를 선언한 톱레벨 클래스에서만 접근 가능
  • package-private(default): 멤버가 소속된 패키지 안의 모든 클래스에서 접근 가능
  • protected: package-private + 멤버를 선언한 클래스의 하위 클래스에서 접근 가능
  • public: 모든 곳에서 접근 가능

먼저 클래스의 공개 API를 세심히 설계한 후에, 그 외의 모든 멤버를 private으로 만든다.

그런 다음 같은 패키지의 다른 클래스가 접근해야 하는 멤버는 package-private으로 풀어준다.

(이러한 경우가 잦다면 애초에 컴포넌트를 더 분해해야 하는 경우일 수도 있다)

 

private, package-private으로 선언된 멤버는 공개 API에 영향을 주지 않는다.

다만, Serializable을 구현한 클래스에서는 해당 필드들도 의도치 않게 공개 API가 될 수 있다는 점을 주의해야 한다.

참고: https://javabom.tistory.com/16

public 클래스에서 멤버의 접근 수준을 protected로 바꾸게 되면 해당 멤버에 접근할 수 있는 대상 범위가 엄청 넓어진다.

protected 멤버는 공개 API이므로 영원히 지원되어야 한다. 따라서 protected 멤버는 적을수록 좋다.

상위 클래스의 메서드를 재정의할 때 그 접근 수준을 상위 클래스보다 좁게 설정할 수는 없다. (리스코프 치환 원칙)

3. public 클래스의 인스턴스 변수는 되도록 public이 아니어야 한다.

1) 변경에 매우 취약하다

필드가 가변 객체를 참조하거나 final이 아닌 경우, 해당 public 필드의 불변식을 보장할 수 없다.

2) 스레드 안전하지 않다.

인스턴스 변수는 힙 영역에 할당되며 공유 자원이다. 따라서 모든 스레드가 접근할 수 있다.

public 인스턴스 변수는 Foo.resource와 같이 접근하기 때문에 다른 작업을 할 수 없다. 예를 들어 Lock 획득 같은 thread-safe한 작업을 할 수 없기 때문에 사용해서는 안된다.

Getter 함수를 통해 필드를 얻게 되면 메서드 안에서 별도의 작업을 해줄 수 있다는 의미로 받아들였다.

3) public final 필드의 문제점

심지어 필드가 final이면서 불변 객체를 참조하더라도 문제는 여전히 남는다.

private 필드라면 내부 구현을 바꾸고 싶을 때 private 필드를 삭제하고, Getter/Setter 메서드만 수정하면 된다. 하지만 public final 필드는 해당 필드가 사용되고 있는 모든 소스코드를 일일이 수정해야 한다.

 

static 필드에서도 이러한 문제점은 마찬가지이지만, public static final 필드는 예외이다.

해당 클래스가 표현하는 추상 개념을 완성하는 데 꼭 필요한 구성요소로써의 상수라면 public static final로 공개해도 좋다.

해당 필드는 반드시 기본 타입 값이나 불변 객체를 참조해야 한다.

가변 객체를 참조한다면, final이 아닌 필드에 적용되는 모든 불이익이 그대로 적용된다. (참조된 객체 자체가 수정)

4) 클래스에서 public static final 배열 필드를 두거나, 이 필드를 반환하는 접근자 메서드를 제공하면 안 된다.

이러한 필드나 접근자를 제공한다면 클라이언트에서 해당 배열의 내용을 수정할 수 있게 된다.

(길이가 0이 아닌 배열은 모두 변경할 수가 있다)

 

해결책은 다음과 같다.

// 방법 1
// public 불변 리스트를 추가한다.

private static final Thing[] PRIVATE_VALUES = {...}
public static final List<Thing> VALUES =
	Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
// 방법 2
// private 배열을 만든 뒤 그 복사본을 반환하는 public 메서드 추가 (방어적 복사)

private static final Thing[] PRIVATE_VALUES = {...}
public static final Thing[] values() {
	return PRIVATE_VALUES.clone();
}

편리함 혹은 성능을 잘 고민해서 상황에 맞게 사용할 수 있다.

4. 모듈 시스템 (자바9부터 도입)

  • 모듈은 자신이 공개할 패키지를 선언해야 한다. public/protected 또한 공개대상이 아니면 외부에서 접근 불가.
  • jar 패키징 시 module-info를 포함하여 패키징하기 때문에 프로젝트 루트의 classpath에 추가하여도 공개하지 않은 모듈은 접근할 수가 없다.