핵심 내용
- 모든 클래스와 멤버는 소프트웨어가 동작하는 한 가장 낮은 접근 권한을 부여하라.
- 꼭 필요한 것만 골라 최소한의 public API를 설계하자.
- public 클래스는 상수용 필드 외에는 어떠한 public필드를 가져선 안된다.
- public static final 필드가 참조하는 객체가 불변인지 확인하라.
클래스와 멤버의 접근 권한을 최소화 하라
잘 설계된 컴포넌트는 클래스 내부 구현 정보를 외부로부터 얼마나 잘 숨겼는지에 판단된다.
자동차를 예로 들어보자.
운전자에게 제공되어야 할 기능(정보)는 자동차를 조작하기 위해 사용될 핸들, 브레이크 등 일 것이다. 반대로 운전자가 알 필요 없는 기능은 엔진의 동작방식, 브레이크의 동작 방식 등 내부적인 동작 방식들 일 것이다.
이를 자바 세계에서 살펴보자.
우리는 운전자에게 제공되야할 기능을 인터페이스로 선언할 수 있을 것이다. 이를 구현하는 자동차 클래스에서는 운전자에게 제공되어야할 기능을 public으로 선언하고, 내부 동작은 private 으로 감출 수 있다.
// interface의 기본 접근제한은 public이다.
public interface Car {
void 시동걸기();
void 핸들작동();
void 브레이크페달();
}
public class Sonata implements Car {
// public 메서드는 API가 된다.
@Override
public void 시동걸기() {
엔진기능점검();
엔진작동();
}
private void 엔진기능점검() { }
private void 엔진작동() { }
...
}
이처럼 잘 설계된 컴포넌트는 내부 구현을 깔끔하게 잘 숨기고 API를 통해서만 사용자(다른 컴포넌트)와 소통한다. 서로의 동작 방식은 전혀 몰라도 아무 문제가 없다.
운전자는 Sonata
클래스를 알 필요가 없다. Car
인터페이스만 안다면 Sonata
를 운전하는 데 무리가 없을 것이기 때문이다.
정보 은닉
이러한 개념을 정보은닉, 캡슐화라 부르며, 아주 많은 장점이 존재한다.
- 시스템 개발 속도를 높인다.예를 들어 웹 개발을 진행하며 내부적으로 두 API가 소통해야 할 때, 두 API는 요청과 응답 대한 인터페이스를 미리 정의해둔다. 이 인터페이스만 잘 지키면 두API가 각자 병렬로 개발되어 동작할 때 아무런 문제가 없을 것이다. 컴포넌트간 소통하는 인터페이스만 잘 설계 해놓으면, 내부 구현은 각자 병렬로 개발할 수 있게 된다.
- 시스템 관리 비용을 낮춘다.다른 컴포넌트로 교체하는 부담이 적다.
인터페이스가 잘 설계되어 있다면, 인터페이스를 따라 새로운 컴포넌트를 구현하고, 이를 문제가 있는 컴포넌트와 교체할 수 있기 때문이다.
전체 코드에서 일부분만 수정하는 방법은 수정에 의해 연쇄적으로 또 다른 문제가 발생할 수 있기 때문에 컴포넌트 교체 보다 더 많은 부담을 준다. - 각 컴포넌트는 독립적으로 개발되기 때문에 디버깅 시간을 단축할 수 있다.
프로그램에서 문제가 발생했을 때, 컴포넌트 단위로 로그를 분석하면 되기 때문에 근본적인 문제에 더 빨리 접근할 수 있고, 문제 해결도 빠를 것이다.
성능 최적화에 도움을 준다. 완성된 프로그램을 분석하여 최적화가 필요한 컴포넌트만 수정할 수 있다. 이는 다른 컴포넌트에 영향을 끼치지 않는다. - 소프트웨어 재사용을 높인다.
외부에 의존하지 않고 독자적으로 동작하는 컴포넌트는 다른 프로그램에서도 유용하게 동작할 것이다. - 큰 시스템을 제작하는 난이도를 낮춰준다.
큰 시스템을 여러 작은 컴포넌트로 나누어 개발할 수 있고, 각자 개발 되는 컴포넌트들은 전체 시스템이 완성되지 않았더라도 개별로 동작을 테스트할 수 있다.
우리가 개발을 진행하며 잘 만들어진 외부혹은 내부 라이브러리를 사용하는 모습을 떠올리면 쉽게 이해가 될 것이다.
예를 들어 웹에서 사용자의 입력한 문자열을 마크업 형식으로 저장해주는 라이브러리를 사용한다고 가정하자.
사용자인 우리는 해당 라이브러리를 사용하기 위해 라이브러리가 내부에서 어떻게 문자열을 마크업 형식으로 저장하는지 알아야 할까?
없을 것이다. 해당 라이브러리의 API만 제대로 숙지했다면 사용하는 데 문제는 없을 것이다.
그럼 컴포넌트를 잘 설계하기 위해서는 어떻게 해야 할까?
우리는 ArrayList
와 LinkedList
를 사용하는 데 있어 List
인터페이스 혹은 public 메서드만 알면 된다. private한 내부 동작은 몰라도 사용하는데에는 아무런 문제가 없다.
이 처럼 접근 제한자를 제대로 활용하는 것이 정보 은닉의 핵심이라고 할 수 있다.
모든 클래스와 멤버의 접근성을 최대한 좁히자.
소프트웨어가 올바르게 동작하는 선에서 최대한 낮은 접근 수준을 부여해야 한다.
톱레벨 클래스나 인터페이스에 부여할 수 있는 접근 수준은 package-private
, public
이다.
톱레벨 클래스는 다른 클래스 내에 선언되지 않고 독립적으로 선언된 클래스를 말한다.
public
은 API(application programming Interface)가 된다.
해당 컴포넌트를 사용하기 위한 인터페이스가 된다.
이는 새로운 버전이 나와도 이전 버전과 호환성을 유지하기 위해 영원히 관리 되어야 한다.
package-private
는 해당 패키지 안에서만 이용할 수 있다.
이는 내부 구현이 되어 클라이언트에게 아무런 피해 없이 다음 릴리즈에서 수정, 교체, 삭제 될 수 있다.
먼저 public일 필요가 없는 클래스의 접근 수준을 package-private한 톱 레벨 클래스로 좁히자.
다음으로 두 톱 레벨 클래스 A, B가 있다고 가정하자. 만약 B가 package-private하고 A에서만 사용된다면 A 클래스 내부에 private static(inner class)으로 B를 선언하자. Builder를 생각하면 이해가 쉬울 것이다.
클래스에서 외부에 공개될 API를 설계하고 외의 모든 멤버는 private
으로 변환하자.
이후 같은 패키지의 클래스가 접근해야 하는 경우 package-private
로 풀어주자.
private
와 package-private
는 모두 해당 클래스의 구현에 해당된다.
이는 보통 공개 API에 영향을 주지 않는다.
만약 public 클래스에서 멤버의 접근 수준을 package-private에서 protected로 변경하는 순간 해당 멤버에게 접근할 수 있는 대상 범위가 엄청나게 넓어지게 된다. (상속에 상속에 상속에 상속,,)
따라서 protected 멤버의 수는 적을수록 좋다.
테스트 코드를 위해 접근 범위를 넓히려할 때가 있다. 이 때 package-private까지 풀어주는 것을 허용한다. 테스트 코드를 같은 패키지 내에 두고 접근하도록 하자.
public클래스의 인스턴스 필드는 되도록 public이 아니어야 한다.
필드가 가변 객체를 참조하거나, final이 아닌 인스턴스 필드를 public으로 선언하면 그 필드에 담을 수 있는 값을 제한할 힘을 잃게 된다.
이게 무슨 말일까?,,, 나중에 아이템 16에서 확인하도록 하자
위의 필드와 관련된 모든 것은 불변임을 보장할 수 없다. 불변이 아닌 필드를 참조한다면 아무리 필드가 변경안된다고 하더라도 불변임을 보장할 수 있을까?
가변 객체의 예시로는 대표적으로 배열을 들 수 있겠다. 어떤 Object배열을 final로 선언한다고 해서 배열이 불변임을 보장할 수 있는가? 배열 안의 값을 언제나 변경될 수 있다.
따라서 클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공해서는 안된다.
public static final Object[] VALUES = {...};
VALUES가 참조하는 배열을 바꿀 순 없겠지만 VALUES가 참조하는 배열의 요소는 바꿀 수 있다.
즉, 불변이 아니다.
그렇다면 이는 어떻게 해결해야 할까
private static int Object[] PRIVATE_VALUSES = {...};
public static final List<Object> VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES))
위의 코드 처럼 진정할 불변 리스트를 만들어버리자.
혹은 배열을 복사해서 반환하는 메서드를 만들자!
private static final Object[] PRIVATE_VALUES = {...};
public static final Object[] values() {
return PRIVATE_VALUE.clone();
}
모듈 개념은 아직 사용하지 않는것이 좋다고 했으니 패스하겠다.
정리
외부에 노출 시킬 API는 최소화 하자.
그 외의 필드는 프로그램이 동작하는 한 가장 낮은 접근 권한을 부여하자.
생각해보기
한 패키지 안에 API는 얼마나 있는 게 적절할까