[이펙티브 자바] 클래스와 인터페이스 Item15 - 클래스와 멤버의 접근 권한을 최소화하라

객체 지향 방법론(Object Oriented Programming : OOP)의 기본 개념은
공통적으로 사용되는 부분을 미리 추상화 해놓는 것이다.

그것을 필요에 따라 구체화시켜 사용하는 것은 코드의 재사용성을 높이며,
그것은 곧 OOP의 지향점이 된다.


자바에서는 추상화의 기본 단위로 클래스(Class)와 인터페이스(Interface) 를 정의하고 있고, 이는 곧 자바의 심장과도 같다.


"4장 - 클래스와 인터페이스" 에서는,
쓰기 편하고, 견고하며, 유연한 클래스와 인터페이스를 만드는 방법에 대해 내용을 서술한다.



  • Item15. 클래스와 멤버의 접근 권한을 최소화하라.
  • Item16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라.
  • Item17. 변경 가능성을 최소화하라.
  • Item18. 상속보다는 컴포지션을 사용하라.
  • Item19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라.
  • Item20. 추상 클래스보다는 인터페이스를 우선하라.
  • Item21. 인터페이스는 구현하는 쪽을 생각해 설계하라.
  • Item22. 인터페이스는 타입을 정의하는 용도로만 사용하라.
  • Item23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라.
  • Item24. 멤버 클래스는 되도록 static으로 만들라.
  • Item25. 톱레벨 클래스는 한 파일에 하나만 담으라.




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




#   정보 은닉이란?


우리는 이번 장을 시작하기에 앞서 정보 은닉 이라는 개념을 먼저 알아야 한다.


<정보 은닉 : Information Hiding>

  • 클래스나 인터페이스 내부의 정보를 외부에서 접근하지 못하게 하는것.
  • 꼭 필요한 경우 외에는 모든 정보를 숨겨야함.
  • 정보에 대한 통제는 접근제어자를 통해 이루어짐.
  • 프로그램의 보안을 유지하고, 의도치 않은 변형을 막음.
  • 캡슐화 (Encapsulation)이라고도 부른다.


보통 잘 설계된 컴포넌트는 API 부분과 실제 구현 부분을 완벽히 분리하여,
클래스 내부의 데이터를 완벽히 숨기게 되어 있다.

그렇기에 각 컴포넌트들은 서로의 내부적인 동작 방식에 대하여 관여할 수도 없고 궁금해도 알 수가 없다.


그저 API를 통해서 소통하면 그만인 것이다.


<Comment.>

  • 예시로, Spring 프레임워크의 MVC 구조를 생각하면 쉽게 이해가 가능하다.

  • Controller 단에서 API를 명세하고, 그 실제 구현은 Service에서 서술한다.
  • 클라이언트는 Controller에서 명세된 API만 볼 수 있을 뿐, 그 실제 구현은 아예 볼 수가 없다.




#   정보 은닉의 장점



그렇다면 정보 은닉을 어째서 사용해야 할까? 다음과 같은 장점이 존재한다.


  • 시스템의 개발 속도를 높인다.

    컴포넌트들이 서로 독립적이라는 것이 주된 포인트이다.
    각자 병렬로 개발이 가능하기 때문에, 전체적인 속도가 올라간다.


  • 시스템 관리 비용을 낮춘다.

    각 컴포넌트들을 더 빨리 파악하여 디버깅이 가능하다.
    다른 컴포넌트로 교체하는 부담도 적다.


  • 정보 은닉 자체가 성능을 높여주지는 않지만, 성능 최적화에 도움이 된다.

    다른 컴포넌트들에 영향 없이, 필요한 컴포넌트만 최적화 하는 것이 가능하다.


  • 소프트웨어 재사용을 높인다.

    외부에 의존이 없는 컴포넌트인 만큼, 새로운 환경에서도 유용하게 쓰일 가능성이 높다.


  • 큰 시스템을 제작하는 난이도를 낮춰준다.

    시스템 전체가 아직 완성되지 않아도 개별적으로 컴포넌트의 동작을 검증할 수가 있다.





#   정보 은닉의 기본 원칙


정보 은닉이 필수적이라는 것은 알았으니 이제 어떻게 하면 되는지를 알아보자.


자바는 정보 은닉을 달성하기 위해 다양한 장치를 제공한다.
그 중 하나가 접근 제어 메커니즘이다.

이는 클래스, 인터페이스, 멤버의 접근 허용 범위(접근성)를 제어하는 것이다.

이때 접근성은 접근 제한자로 명시될 수 있다.


<접근 제한자>

  • 클래스나 인터페이스, 멤버등의 접근 권한을 명시한다.

  • private - 멤버를 선언한 톱레벨 클래스에서만 접근 가능.
  • package-private - 해당 패키지 안의 모든 클래스에서 접근 가능.
    (default 접근 수준, 인터페이스는 public이 default)
  • protected - private-package의 범위를 포함하고, 해당 클래스의 하위 클래스에서도 접근 가능.
  • public - 모든 곳에서 접근 가능.


정보 은닉의 기본 원칙은 아주 간단하다.

그냥 모든 클래스와 멤버의 접근성을 가능한 한 최대로 좁힌다.
쉽게 말해, 항상 제일 낮은 수준의 접근성을 부여해야 한다는 것이다.
소프트웨어가 동작하기만 하면 된다.


이러한 대원칙 아래서, 다음과 같은 상황들을 정리할 수가 있다.



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

<Comment.>
톱레벨 클래스는 그 자체로 가장 바깥쪽에 있는 클래스를 말한다.


톱레벨 클래스와 인터페이스에는 package-private 또는 public을 선언할 수 있다.


  1. public으로 선언할 경우

    -> 클래스가 공개 API가 됨.
    -> 클라이언트와의 호환을 위해 릴리즈마다 계속해서 관리를 해줘야 함.

  1. package-private 으로 선언할 경우

    -> 클래스가 내부 구현이 됨.
    -> 릴리즈마다 클라이언트에 상관없이 언제든 수정, 교체, 제거가 가능.


정리하자. API로 쓸거면 public, 그게 아니면 package-private로 선언하면 된다.



  • 한 클래스에서만 사용하는 package-private 클래스나 인터페이스의 경우

다시말해, 패키지 내에서 클래스나 인터페이스의 사용처가 단 한 곳 밖에 없을 때를 말한다.

이럴 때는 굳이 따로 클래스나 인터페이스로 빼지 말고, 사용처 내에서 private static으로 선언하는 것이 좋다.




  • 멤버 (필드, 메서드, 중첩 클래스, 중첩 인터페이스)

멤버에 부여할 수 있는 접근 수준은,
private, package-private, protected, public로 총 네가지이다.


멤버는 접근 수준 종류가 많아 구별하기 어려워 보일수 있지만 단순하다.


  1. 일단 모든 멤버를 private으로 만든다.
  2. 오직 같은 패키지의 다른 클래스가 접근해야 하는 멤버에 한해 package-private으로 풀어준다.

만약 위와 같은 과정에서 권한을 풀어주는 일이 많다면, 컴포넌트를 더 분해해야 하는 것이 아닌가 고민해봐야 한다.



  • protected는 적을수록 좋다.

protected는 package-private에 비해 접근 가능 범위가 엄청나게 넓다.

public 클래스의 protected 멤버는 공개 API이기 때문에 영원히 지원되어야 한다.

그래서 protected는 적을수록 좋다.




  • 상위 클래스의 메서드를 재정의 하는 경우

앞에서 멤버의 접근성을 최대한 작게 하라고 말한 바가 있다.

그런데 이게 불가능한 경우가 하나가 있는데,
바로 인터페이스를 구현하는 클래스의 경우가 그렇다.


상위 클래스의 인스턴스는 하위 클래스의 인스턴스로 대체하여 사용할 수 있어야 한다. (리스코프 치환 규칙)

실제로 이를 어기면 하위 클래스를 컴파일 할 때 오류가 난다.


그렇기에 하위 클래스는 상위 클래스에서보다 접근 수준을 좁게 설정할 수 없다.

클래스가 인터페이스를 구현할 경우, 인터페이스가 정의한 모든 메서드들을 public으로 선언해야만 한다.




  • 코드를 테스트 해야할 경우

코드를 테스트 하기 위해 클래스, 인터페이스, 멤버의 접근 범위를 넓히려 할 때가 있다.

하지만 애초에 테스트 코드를 대상과 같은 패키지에 두면 굳이 그럴 이유가 없어진다. (package-private에 접근이 가능하므로)




  • public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다.

만약 가변 객체를 참조하거나, final이 아닌 인스턴스를 public으로 선언하면,
그 필드에 담을 수 있는 값을 제한할 힘을 잃게 된다.

이는 정리하면 그 필드와 관련된 모든 것은 불변식을 보장할 수 없다는 의미이다.


또한, 필드가 수정될 경우 다른 작업을 할 수 없게 되므로,
public 가변 필드를 갖는 클래스는 일반적으로 스레드가 안전하지 않다.


문제는 필드를 final로 바꾸고 불변 객체를 참조하더라도,
public이면 여전히 문제가 있다는 것이다.

내부 구현을 바꾸고 싶어도,
그 public 필드를 없애는 방식으로는 리팩터링할 수 없게 된다.




  • 정적 필드의 경우

정적 필드는 앞서 인스턴스 필드에서 말했던 것과 같은 문제점을 가지게 된다.

다만 예외가 하나 있다.


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

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

만약 가변 객체를 참조한다면 앞서 말한 불이익이 모두 다시 적용된다.




  • 길이가 0이 아닌 배열

길이가 0이 아닌 배열은 모두 변경 가능하니 주의해야 한다.

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


만약 이를 어기면, 클라이언트에서 그 배열의 내용을 수정할 수 있게 된다.

예컨대 다음과 같은 코드가 허점이 존재하는 것이다.

public static final Thing[] VALUES = { ... };

위 경우를 해결할 수 있는 방법은 두 가지이다.


첫번째 방법은 다음과 같다.

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

위처럼 public 이였던 배열을 private로 변경한다.
그리고 public 불변 리스트를 추가한다.


두번째 방법은 다음과 같다.

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

마찬가지로 public이였던 배열을 private으로 변경한다.
그리고 그 복사본을 반환하는 public 메서드를 하나 추가해준다. (방어적 복사)


클라이언트가 무엇을 원하냐에 따라 둘 중 하나를 선택해주면 된다.





#   암묵적 접근 수준


자바 9에서부터는 모듈 시스템이라는 개념이 도입되었다.
이때 두 가지의 암묵적 접근 수준이 추가되었다.


패키지가 클래스들의 묶음이라면, 모듈은 패키지들의 묶음이다.

모듈은 그 안에 속하는 패키지들 중, 공개할 것들을 명시한다. (module-info.java)
이때 공개하지 않은 패키지들은 public이던 protected던 외부에서 접근할 수가 없다.

이를 이용하여 클래스를 외부에 공개하지 않으면서 같은 모듈안에 패키지들 끼리는 자유롭게 공유가 가능하다.


암묵적 접근 수준이란, public이나 protected이지만 그것이 모듈 내로 한정되는 것을 말한다.


하지만 보통 이런 형태가 필요한 경우는 흔하지 않고,
그런 경우가 있더라도 패키지들 사이에서 클래스를 재배치하면 해결된다.
(별로 필요하지 않다는 이야기를 하고싶은 것이다.)


새롭게 등장한 모듈의 접근 수준을 적극 활용한 예시가 바로 JDK 그 자체이다.

자바 라이브러리에서 공개하지 않은 패키지들은 해당 모듈 밖에서는 절대로 접근할 수가 없다.


접근 보호 방식이 추가된 것 외에도 모듈 자체를 제대로 사용하려면 할 일이 많다.
(패키지들을 모듈 단위로 묶기, 모듈 선언에 모든 의존성을 명시하기 등..)


결론은, 아직 모듈 자체를 사용하기에는 조금 이른 감이 있으므로 당분간은 사용하지 않는게 좋다는 것이다.




여기까지 클래스와 인터페이스의 접근 권한에 대한 내용이였다.

필자의 코멘트로 글을 마무리 한다.

Item15 정리

  • 프로그램 요소의 접근성은 가능한 한 최소한으로 하라.
  • 꼭 필요한 것만 골라 public API로 설계하자.
  • public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가지면 안된다.
  • public static final 필드가 참조하는 객체가 불변인지 확인하자.


좋은 웹페이지 즐겨찾기