DEV ℧ Developer Diary

[EffectiveJava] item15 - 클래스와 멤버의 접근 권한을 최소화 하라.

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

어설프게 설계된 컴포넌트와 잘 성계된 컴포넌트의 가장 큰 차이는 바로 클래스 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 얼마나 잘 숨겼냐 이다. 이는 객체지향 4대 특징중 하나라고 불리는 정보 은닉, 혹은 캡슐화 라고 불리며, 소프트웨어 설계의 근간이 되는 원리이다.

정보은닉의 장점

  • 시스템 개발속도를 높인다. 여러 컴포넌트를 병렬로 개발할 수 있기 때문이다.
  • 시스템 관리 비용을 낮춘다. 각 컴포넌트를 더 빨리 파악하여 디버깅할 수 있고, 다른 컴포넌트로 교체하는 부담도 적기 때문이다.
  • 정보 은닉 자체가 성능을 높여주지는 않지만, 성능 최적화에 도움을 준다. 완성된 시스템을 프로파일링해 최적화할 컴포넌트를 정한 다음, 다른 컴포넌트에 영향을 주지 않고 해당 컴포넌트만 최적화할 수 있기 때문이다.
  • 소프트웨어 재사용성을 높인다. 외부에 거의 의존하지 않고 독자적으로 동작할 수 있는 컴포넌트라면 그 컴포넌트와 함께 개발되지 않은 낯선 환경에서도 유용하게 쓰일 가능성이 크기 때문이다.
  • 큰 시스템을 제작하는 난이도를 낮춰준다. 시스템 전체가 아직 완성되지 않은 상태에서도 개별 컴포넌트의 동작을 검증할 수 있기 때문이다.

자바의 정보은닉

기본원칙은 간단하다. 모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다. 소프트웨어가 올바르게 동작하는 한 접근 권한을 가장 낮게 부여해야 한다.

가장 바깥의 클래스와, 인터페이스에 부여할 수 있는 접근 수준은 default, public 두가지가 있다. 만약 public을 선언한다면 공개 API가 된다. 하지만, default로 선언 할 경우에는 패키지 내부에서만 쓸 수 있는데 패키지 외부에서 쓸 필요가 없다면 default로 선언 해주자. 그렇다면 해당 로직은 API가 아닌 내부 구현이 되어 클라이언트에 아무런 피해 없이 언제든 수정, 교체, 제거가 가능하다.

자바에서 정보은닉에 대한 핵심은 접근제어자 이며, 멤버에 부여할 수 있는 접근 수준은 네가지다. 접근 범위가 좋은 것부터 순서대로 살펴보자.

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

접근제어자에 대해 주의할 점이 있다면, 상위클래스의 메서드를 재정의 할 때에는 그 접근 수준을 상위 클래스보다 좁게 설정 할 수 없다. 이제약은 리스코프 치환 법칙을 지키기 위해 필요하다. 이 규칙을 어기면 하위 클래스를 컴파일 할때 컴파일 에러가 날 수 있다.

public 클래스의 필드

public클래스의 인스턴스 필드는 되도록 public이 아니어야 한다. 필드가 가변객체를 참조하거나, final이 아닌 인스턴스 필드를 public으로 선언하면, 그 필드에 담을 수있는 값을 제한할 힘을 잃게된다. 이는 불변식을 보장할 수 없게 된다는 뜻이고 해당 방법은 Thread-safe하지 않다.

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

배열의 인스턴스 필드

길이가 0이 아닌 배열은 모두 변경 가능하니 조심하자. 따라서 배열에 관련된 필드는 클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공해서는 안된다. 주의해야할 코드는 아래와 같다.

/* final로 선언했지만, public 으로 제어되어 보안 허점이 있다. */
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();
}

해당 방법중 어느 반환타입이 더 쓰기 편할지, 성능은 어느 쪽이 나을지 고민해서 정해주면 된다.