Programming/Java

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

VSFe 2021. 2. 18. 14:45
클래스와 멤버의 접근 권한을 최소화하라!

객체지향의 요소를 떠올리면, 캡슐화 (Encapsulation) 이라는 말이 있다. 해당 요소의 의미는 프로그램 내 같은 기능을 목적으로 작성된 코드를 모아서 보이지 않게 숨기는 것을 의미한다.

그런데 왜 캡슐화를 해야할까? 객체지향적인 이야기이긴 하지만, 몇 가지 장점을 생각해보자.

  • 여러 컴포넌트를 병렬로 개발할 수 있기 때문에 개발 속도를 향상시킨다.
  • 컴포넌트를 변경할 수 있고, 특정 컴포넌트를 분리해서 디버깅 할 수 있다.
  • 다른 컴포넌트와 분리되어 있는 만큼, 성능 최적화를 위해선 특정 컴포넌트만 떼어서 최적화 하기에도 편하다.
  • 독자적으로 동작하는 컴포넌트를 개발했으면, 해당 컴포넌트를 다른 프로젝트에서도 사용할 수 있기 때문에 재사용성이 올라간다.
  • 시스템을 분리해서 컴포넌트 별로 검증할 수 있기 때문에 대형 프로젝트 난이도를 낮춰준다.

자바는 객체지향을 위해 설계된 언어인 만큼, 캡슐화가 잘 이뤄지도록 다양한 장치를 제공하는데, 우리는 이 아이템에서 핵심 요소인 접근 제한자에 대해 알아보려고 한다.

기본적인 원리는 모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다는 것이다.

가장 바깥쪽 클래스와 인터페이스의 경우, public이나 package-private를 사용하는데, 전자의 경우 공개 API가 되며, 후자의 경우 패키지 내부에서만 사용할 수 있다. "그냥 속 시원하게 죄다 public으로 하면 안되나요?" 라고 생각할 수도 있지만, 이 경우 API 처럼 사용되기 때문에 앞으로도 레거지 지원을 위해 계속 관리해줘야 하는 대상이 된다.

iterator 같이, 한 클래스에서만 사용하는 것들은 어떨까? 이 경우엔 클래스 내부에 private로 만들자. 이렇게 하면 바깥 클래스에서만 접근이 가능한 클래스가 된다.

멤버 필드, 중첩 클래스/인터페이스, 메소드의 경우 네 가지의 수준을 제공한다.

  • private: 멤버를 선언한 톱레벨 클래스에서만 접근할 수 있다.
  • package-private: 멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다.
  • protected: package-private + 하위 클래스까지 접근이 가능함.
  • public: 모든 곳에서 접근할 수 있다.

공개할 수 있는 범위를 충분히 고려해서 설계하고, 그 이외의 모든 멤버는 private로 지정하는 것이 좋다. 이후에 같은 패키지 내에서 접근할 필요가 있는 것에 한해 package-private로 살짝 풀어주는게 좋다. protected도 사실상 상속만 받아버리면 공개 API와 유사한 수준이 되기 때문에 고려해서 올리도록 하자.

다만 문제가 있다. 상위 클래스의 메소드를 재정의 할 때는 접근 수준을 상위 클래스보다 낮게 할 수 없다! (리스코프 치환 원칙을 떠올려보자. 상위 클래스의 인스턴스로 대체할 수 있기 때문이다.) 그래서 클래스가 인터페이스를 구현할 때는 모든 메소드를 public으로 해야 한다.

public으로 클래스를 만들게 된다면, 인스턴스 필드는 가능한 한 public이 아니어야 한다! 만약 필드가 가변 객체를 참조하거나 final이 아니라면, 클래스 자체가 안전하지 않다. (스레드 세이프하지도 않은더러, 무결성도 보장할 수 없다.)

만약 클래스가 표현하는 추상 개념을 완성하는데 필요한 부분이 있다면 public static final로 공개해도 좋다. 다만 이런 경우엔 기본 타입 값이나 불변 객체를 참조해야 한다. (값 변경 가능성을 없애기 위해) 특히, 길이가 0이 아닌 배열은 모두 변경이 가능하니 public static final로 사용하는 것을 피해야 한다!

그렇다면 어떻게 해결해야 할까? 하나는 배열을 private로 만들고 public 불변 리스트를 추가하는 것이다.

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

다른 하나는 배열을 private로 만들고 복사본을 반환하는 public 메소드를 만드는 것이다. 우리는 이것을 방어적 복사 (Defensive Copy) 라고 하는데, 이렇게 하면 내부와 외부에서 주소값을 공유하는 인스턴스의 관계를 끊어버릴 수 있다. (즉, 외부에서 배열에 add를 하여도, 내부에는 전혀 영향이 없다.)

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

자바 9 부터는 모듈 시스템이라는 개념이 도입되었다. 모듈은 패키지의 묶음이다. 모듈은 자신에 속하는 패키지 중 공개 할 것을 선언할 수 있다. 즉, protected나 public 으로 선언되어 있어도 모듈에서 공개하지 않으면 접근할 수 없다. 이것을 활용하면 모듈 내부에선 자유롭게 공유할 수 있으면서도 바깥에서의 접근을 차단할 수 있다.

다만 모듈에 적용되는 접근 수준은 조심해서 사용해야 한다. 모듈의 JAR 파일을 모듈 경로가 아닌 classpath에 넣어버리면 패키지가 모듈이 없는 것 처럼 행동하기 때문에 밖에서 접근할 수 있게 된다.

또 모듈을 사용하기 위해 손이 많이 간다는 것도 문제다. 패키지를 모듈 단위로 묶고, 패키지의 의존성을 명시하고, 소스 트리를 재배치 하고 일반 패키지로의 접근을 위해 조치를 취해야 한다. 비록 JDK에서는 활발하게 모듈을 사용하고 있지만, 당분간은 사용을 하지 않는 것이 좋을 것이다.