클래스와 멤버의 접근 권한을 최소화하라

@023· January 16, 2025 · 11 min read

잘 설계된 컴포넌트

책에서는 잘 설계된 컴포넌트에 대해 다음과 같이 설명한다.

  • 클래스 내부 데이터와 내부 구현 정보를 외부로 부터 잘 숨겨야 한다.
  • API를 통해서만 다른 컴포넌트와 소통해야 하며, 내부 동작 방식에 개의치 않아야 한다.

이는 캡슐화(정보 은닉)의 개념으로, 이를 잘 지키는 것이 잘 설계된 컴포넌트라고 말하고 있다.

캡슐화의 장점

캡슐화는 각각의 컴포넌트들을 서로 독립시켜서 개별적으로 동작할 수 있게 함으로써 다음과 같은 장점이 있다.

  • 각각의 컴포넌트를 병렬로 개발할 수 있끼에 개발 속도를 높인다.
  • 각 컴포넌트를 더 빨리 파악하여 디버깅이 할 수 있고, 교체 부담이 적어서 시스템 관리 비용을 낮춘다.
  • 다른 컴포넌트에 영향을 안주고 해당 컴포넌트만 최적화가 가능하여 성능 최적화에 도움을 준다(아이템 67).
  • 기존 환경에 대한 의존성이 낮아 독자적으로 동작이 가능해 다른 환경에서의 이식성 및 재사용성을 높인다.
  • 개별 컴포넌트의 동작을 검증할 수 있어, 큰 시스템 상황에서 제작 및 관리하는 난이도를 낮춘다.

접근 제어자

자바에서는 이러한 캡슐화를 제공하기 위해 다양한 장치를 제공한다. 그 중 하나가 접근 제어자이다. 접근 제어자를 통해 클래스, 인터페이스, 멤버의 접근 허용 범위를 명시할 수 있다.

톱레벨 클래스와 인터페이스

가장 외부의 톱레벨 클래스와 인터페이스에 부여할 수 있는 접근 수준은 package-privatepublic이다.
톱레벨 클래스와 인터페이스에 대해서는 패키지 외부에서 사용할 이유가 없다면, package-private로 선언하라고 권장한다. 그 이유는 클라이언트에 영향을 주지 않고 내부 구현을 변경할 수 있기 때문이다. 반면, public으로 선언하면 해당 클래스나 인터페이스를 사용하는 클라이언트에게 영구적인 API로 제공하게 되므로, 하위 호환성 유지를 위해 내부 구현 변경이 어려워진다.

톱레벨에 위치한다는 것은 같은 패키지의 모든 클래스가 해당 클래스에 접근할 수 있다는 것을 의미한다. 하지만 한 클래스에서만 사용하는 package-private 톱레벨 클래스는 외부 접근을 방지하기 위해 private static으로 중첩시키는 것을 권장한다.

public class Outer {
    private static class Inner { ... }
}

멤버의 접근 수준

그럼 public만 신경 쓰면 될까? 책에서는 public일 필요가 없는 클래스의 접근 수준을 package-private 톱레벨 클래스로 더 좁게 최소한으로 설정하라고 권장하고 있다. 멤버(필드, 메서드, 중첩 클래스, 중첩 인터페이스)에 부여할 수 있는 접근 수준은 다음과 같다.

  • private: 멤버를 선언한 톱레벨 클래스에서만 접근할 수 있다.
  • package-private: 멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다. 접근 제한자를 명시하지 않았을 때 적용되는 패키지 접근 수준이다(단, 인터페이스의 멤버는 기본적으로 public이 적용된다).
  • protected: package-private의 접근 범위를 포함하며, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근할 수 있다(제약이 조금 따른다).
  • public: 모든 곳에서 접근할 수 있다.

이러한 접근 제어자를 통해 공개 API를 엄격히 설계해야 한다. 다른 클래스가 반드시 접근해야 하는 멤버에 한하여 private를 제거해, package-private으로 풀어줘도 된다. 하지만, 이러한 상황이 빈번하게 발생한다면 시스템 컴포넌트를 더 분해해야 하는지 고려해봐야한다.

package-private에서 protected로 접근 수준을 넓히는 순간 그 멤버에 대한 대상 범위가 넓어지게 되는데, 이때 public 뿐만 아니라 protected도 공개 API로 취급되기 때문에, 내부 동작 방식이 담긴 API 문서화의 대상이 된다. 그래서 protected의 멤버를 최대한 줄이고, privatepackage-private을 사용하는 것이 좋다.

privatepackage-private 멤버는 공개 API에게 영향을 주지 않는 것이 보통이지만, Serializable을 구현하는 클래스에서는 의도치 않게 공개 API로 노출될 수 있다.

그런데 멤버 접근성을 좁히지 못하게 방해하는 제약이 있다. 상위 클래스의 메서드를 재정의할 때는 그 접근 수준을 상위 클래스의 접근 수준보다 좁게 설정할 수 없다. 이 제약은 상위 클래스의 인스턴스는 하위 클래스의 인스턴스로 대체할 수 있어야 한다는 규칙 리스코프 치환 법칙을 지키기 위해 필요하다. 만일 이 제약을 어기면 컴파일 단계에서 오류가 날 것이다.

테스트와 접근 제어자

테스트 코드를 작성하기 위해 클래스, 인터페이스, 멤버의 접근 범위를 넓혀야 할 때가 있다. private에서 package-priavte 까지의 완화 허용을 하지만, 그 이상으로 완화해서 공개 API로 노출 시키는 것을 피해야 한다.

실제로 테스트 코드를 같은 패키지에 두면 package-private 멤버에 접근할 수 있기 때문에, 테스트 코드를 같은 패키지에 두는 것이 일반적이다.

public 필드의 위험성과 해결책

final이 아닌 인스턴스 필드를 public으로 선언하는 것은 좋지 않다. 이는 필드에 대한 제어권을 잃어 불변식을 보장할 수 없게 되기 때문이다. 또한, 필드가 수정될 때 락(Lock) 같은 작업을 할 수 없게 되어 멀티 스레드 환경에서 스레드 안정성을 보장하지 못한다.

그러면 public final로 선언하면 되지 않을까?라고 생각할 수 있지만, 이는 불변식을 보장하지만, 캡슐화를 깨는 행위이다.

public final int[] VALUES = {1, 2, 3};

public void test() {
    VALUES[0] = 4; // 이러한 변경이 가능해진다.
    assertThat(VALUES[0]).isNotEqualTo(1);
}

상수라는 예외가 하나 있다. 이럴 때 public static final로 선언하는 것하여 공개하여 사용해도 된다. 이때, 관례상 상수의 이름은 대문자로 작성하며, 단어 사이는 밑줄로 구분한다. 또한 이런 필드는 반드시 기본 타입이나 불변 객체로 초기화해야 한다.

이러한 문제를 해결하기 위해, 책에서는 두 가지 방법을 제시한다.

public static final Thing[] VALUES = { ... };// 접근이 가능해서 배열의 내용을 변경할 수 있다.

첫 번째 방법으로는 public 배열을 private으로 만들고, public 불변 리스트를 추가하는 것이다.

private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

두 번째 방법으로는 private 배열을 만들고, 그 복사본을 반환하는 public 메서드를 추가하는 방법이다(방어적 복사).

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

자바 9 모듈 시스템

자바 9에서 도입된 모듈 시스템은 패키지의 몪음 단위로 접근성을 제어한다. 모듈 선언 파일(module-info.java)에서 exports 키워드를 사용해 패키지를 공개할 수 있다. 이를 통해 모듈의 공규 여부와 상관없이 모듈 내부의 패키지를 외부로 노출하지 않고, 자유롭게 공유할 수 있다. 이러한 기술 활용한 대표 사례가 JDK이다. 하지만, 사용이 간단하지 않고, 모듈 시스템을 사용하려면 모든 코드를 모듈 시스템에 맞게 수정해야 한다. 이로 인해, 반드시 필요한 경우가 아니라면 모듈 시스템을 사용하지 않는 것이 좋다.

정리

프로그램 요소의 접근 성은 가능한 한 최소한으로 해서 멤버가 의도치 않게 API로 공개되는 일이 없도록 해야 한다. 그리고 public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안 된다.

@023
focus and hustle