잘 설계된 컴포넌트를 설계하기 위해서는 클래스 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 잘 숨겨야 한다. 즉, 모든 내부 구현을 완전히 숨김으로서, 구현과 API를 깔끔히 분리해야 한다.
정보 은닉(캡슐화)
정보 은닉(캡슐화)라고 하는 이 개념은 소프트웨어 설계의 근간이 되는 원리다. 오직 API를 통해서만 서로 소통하며 내부적으로 어떻게 동작하는지는 신경쓰지 않는다.
정보 은닉의 장점
- 여러 컴포넌트를 병렬로 개발할 수 있기 때문에 개발 속도를 높일 수 있다.
- 각 컴포넌트를 더 빨리 파악하여 디버깅하기 쉽고, 다른 컴포넌트로 교체하는 부담도 적기 때문에 시스템 관리 비용이 낮다.
- 정보 은닉 자체가 성능을 높여주지는 않지만, 성능 최적화에 도움을 준다. -> 해당 컴포넌트만 최적화할 수 있다.
- 외부에 거의 의존하지 않고 독자적으로 동작할 수 있는 컴포넌트이기 때문에 소프트웨어 재사용성이 높아진다.
- 시스템 전체가 완성되지 않은 상태에서 개별 컴포넌트의 동작을 검증할 수 있기 때문에 큰 시스템 제작의 난이도를 낮춰준다.
정보 은닉을 위한 접근 제한자
접근 제한자를 선택할 때는 모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다.
- protected, public으로 선언하면 API가 되므로 하위 호환성 유지를 위해 영원히 관리해야 한다.
- private, package-private으로 선언하면 내부 구현이 되어 언제든 수정할 수 있기 때문에 패키지 외부에서 쓰지 않을 클래스나 인터페이스라면 package-private으로 선언한다.
MemberService 인터페이스와 DefaultMemberService가 있다 가정해보자. MemberService 인터페이스는 다른 패키지에서도 사용할 수 있을 것 같다. (e.g 게임 아이템을 구매 로직, 책을 빌리는 로직 등). 그런데 DefaultMemberService는 다른 패키지에서 사용할 이유가 없어 보인다. 사용하는 쪽에서는 내부의 구현을 자세히 알 필요가 없기 때문이다. 따라서 다음과 같이 MemberServcie는 public으로 DefaultMemberService는 package-private으로 선언할 수 있다.
public interface MemberService { }
class DefaultMemberService { }
-> 만약 한 클래스에서만 사용하는 package-private 클래스나 인터페이스가 있다면 해당 클래스에 private static으로 중첩시키자.
class DefaultMemberService {
private String name;
public static MemberInnerService() {}
public MemberInnerService() {}
}
- public static으로 선언하면 DefaultMemberService와의 참조 관계를 전혀 갖기 않기 때문에 필드 접근이 어려워진다. 하지만 원래는 별개의 클래스임을 생각해본다면 public static이 적절해보인다.
- public으로만 선언하면 DefaultMemberService의 참조를 가지기 때문에 name필드에 접근을 할 수 있다.
주의 사항
- 단지 코드를 테스트하려는 목적으로 클래스, 인터페이스, 멤버의 접근 범위를 넓히면 안된다.
=> public 클래스의 private 멤버를 package-private까지 푸는 것은 괜찮지만 그 이상은 안된다.
=> 즉, 테스트만을 위해 클래스, 인터페이스, 멤버를 공개 API로 만들어서는 안 된다. -> package-private으로 만들면 충분히 접근할 수 있다. - public 클래스의 인스턴스 필드는 되도록 public이 아니여야 한다.
=> 필드가 가변 객체를 참조하거나, final이 아닌 인스턴스 필드를 public으로 선언하면 불변식을 보장할 수 없게 된다.
=> 락 획득과 같은 작업이 불가능하므로 public 가변 필드를 갖는 클래스는 일반적으로 스레드 안전하지 않다. - public 클래스 안의 public static final 상수를 만들 수는 있다.
=> public static final Thing[] VALUES ={ ... } 와 같이 선언하게 되면 다른 클래스에서 요소에 직접 접근해서 값을 바꿀 수 있는 문제가 있다.