ITEM 15 "클래스와 멤버의 접근 권한을 최소화하라"
이번 장(Item 15 ~ Item 25)부터는 자바라는 언어에 국한되어 서술되어 있는 것이 아닌 객체지향적으로 잘 설계하는 방법에 대해 서술하고 있다.
지금은 코틀린 라이브러리 공식 문서에서 사라져서 찾아볼 수 없지만, 코틀린 언어의 철학이 이 Item 들을 지키려고 노력했을 정도로 객체지향 세계에서 표준에 가깝다.
잘 설계된 컴포넌트는 클래스 내부 데이터와 내부 구현 로직들을 외부 컴포넌트로부터 철저히 숨겨두었다. 오직 API 를 통해서만 다른 컴포넌트와 소통하며 서로의 내부 동작 방식에는 전혀 개의치 않는다.
정보은닉, 혹은 캡슐화라고 하며 이 개념은 소프트웨어 설계의 근간이 되는 원리다. 정보 은닉과 캡슐화를 철저히 해두었다면 아래와 같은 장점들이 있다.
1 . 시스템 개발 속도를 높인다.
컴포넌트를 독립적으로 기능 별로 분리할 수 있다면, 각 기능들을 개발할 수 있도록 개발자를 할당할 수 있다. 즉 컴포넌트를 병렬로 개발할 수 있다.
public interface MemberService {
void joinMember(Member member);
List<Member> getMembers();
void leaveMember(Member member);
}
예를 들어 위와 같이 회원 가입에 필요한 서비스들("회원 가입", "회원 탈퇴", "회원 검색")을 나누어서 인터페이스로 제공하고 있다면, 각 팀원들이 한 가지 기능들을 맡아 개발할 수 있다.
2 . 시스템 관리 비용을 낮춘다.
소스코드 한 줄로 기능들이 뒤 섞여 있는 것보다 각 컴포넌트를 빨리 파악하여 디버깅 할 수 있고, 다른 컴포넌트로 교체하는 부담도 적다.
위 예시에서 "회원 검색" 에 대한 요구사항이 변경되었을 경우, getMembers 인터페이스의 구현체의 로직 일부분을 변경하기만 하면 된다.
3 . 성능 최적화에 도움을 준다.
정보 은닉 자체가 성능 향상이 되지는 않지만, 프로파일링해서 최적화할 컴포넌트를 정해서 다른 컴포넌트에 영향을 주지 않고 해당 컴포넌트만 최적화 할 수 있다.
속도가 더 빠른 알고리즘으로 바꾼다던지, 입력/수정/읽기 세션의 크기를 보고 아키텍처를 변경할 수도 있다. 독립적이기에 가능하다.
4 . 소프트웨어 재사용성을 높인다.
다른 컴포넌트에 의존하지 않고 독자적으로 동작할 수 있는 컴포넌트라면 다른 곳에서도 충분히 재사용할 수 있다.
5 . 큰 시스템을 제작하는 난이도를 낮춰준다.
시스템 전체가 만들어지지 않아도 개별 컴포넌트의 동작을 검증할 수 있다. Mockito 를 이용해 가짜 객체를 생성하고 그 객체를 통해 테스트 케이스 작성이 가능해진다. DIP 원칙을 통해 인터페이스로 접지한 객체를 DI(dependency injection) 해서 독자적인 테스트 케이스를 만들 수도 있다.
public class MemberTest {
private static class Member {
private String name;
private int age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
@Mock
Member member;
@Test
public void 멤버별_테스트() {
Member member = mock(Member.class);
assertTrue(member != null);
when(member.getName()).thenReturn("gold-egg");
when(member.getAge()).thenReturn(29);
assertTrue(member.getName() == "silver-egg");
assertTrue(member.getAge() == 30);
}
}
위와 같이 Member 에 대한 클래스만 따로 테스트 하는 것을 볼 수 있다.
정보 은닉은 모든 클래스와 멤버의 접근성을 가능한 좁히는 쪽으로 설계하면 자연스레 안전한 캡슐화가 된다.
자바에서는 아래와 같이 접근 제한자를 제공하고 있으며 클래스 (및 인터페이스), 변수, 메서드등 범용적으로 사용할 수 있다.
JAVA 의 접근제한자 private: 멤버를 선언한 top-level 클래스에서만 접근할 수 있다. package-private: 멤버가 소속된 패키지 안의 모든 클래스에서 접근 할 수 있다. protected: package-private의 접근 범위를 포함하며, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근할 수 있다. public: 모든 곳에서 접근 할 수 있다. |
클래스와 인터페이스 내에서 접근 제한 사항
top-level 클래스와 인터페이스는 package-private 과 public 두 가지이고 public 으로 선언하면 공개 API 가 되며, package-private 으로 선언하면 해당 패키지 안에서만 이용이 가능하다.
패키지 외부에서 사용할 것이 아니라면 package-private 로 선언하자. 반면 public 으로 선언한다면 API 가 되므로 하위 호환을 위해 영원히 관리해주어야 한다.
한 클래스에만 사용하는 private-package top-level 클래스나 인터페이스는 이를 사용하는 클래스 안에 private static 으로 중첩시키자.
public 일 필요가 없는 클래스의 접근 수준을 package-private top-level 클래스로 좁히는 일이다. public 클래스는 그 패키지의 API 인 반면, package-private 톱레벨 클래스는 내부 구현에 속하기 때문이다.
만약, 공개 API 를 제외한 나머지 멤버들은 private 로 만들자. 오직 같은 패키지의 다른 클래스가 접근해야 하는 멤버에 한하여 package-private 로 풀어두자. 권한을 자주 풀어준다면, 컴포넌트를 더 분해해야 되는 것은 아닌지 고민해야 한다.
public 클래스에서 멤버 접근 수준을 package-private 에서 protected 로 바꾸는 순간 그 멤버에 접근할 수 있는 대상 범위가 넓어지므로 public 클래스의 protected 멤버는 공개 API 이므로 영원히 지원돼야 한다.
내부 동작 방식을 API 문서에 적어 사용자에게 공개해야 할 수도 있다. Protected 멤버의 수는 적을수록 좋다.
참고로, 멤버 접근성을 못 좁히는 방해 제약도 있다. 상위 클래스의 메서드를 재정의할 때는 그 접근 수준을 상위 클래스보다 좁게 설정할 수 없다. (리스코프 치환 원칙)
public 클래스의 인스턴스 필드는 되도록 public 이 아니어야 한다.
클래스의 필드가 가변 객체를 참조하거나, final 이 아닌 인스턴스 필드를 public 으로 선언하면 그 필드에 담긴 값을 제한할 힘을 잃게 된다. 그 필드와 관련된 모든 것은 불변식을 보장할 수 없게 된다.
심지어 가변 필드가 수정될 때 다른 작업을 할 수 없게 되므로 thread-safe 하지도 않다. public 지시어는 될 수 있으면 삼가자.
예외는 있다. 상수의 경우에는 관례대로 public static final 지시어를 사용해 필드를 공개해도 좋다. 이런 필드는 반드시 기본 타입 값이나 불변 객체를 참조해야 한다.
그렇다고 절대로 아래와 같이 배열을 public static final 로 선언하면 안 된다. 배열의 참조를 변경할 수 없겠지만, 배열 내 내용을 변경할 수 있는 보안 허점이 있다.
문제점
public static final Thing[] VALUES = {...};
해결책 1
private static final Thing[] PRIVATE_VALUES = {...};
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
배열을 private 로 만들고 public 불변 리스트로 변환하여 추가한다.
해결책 2
private static final Thing[] PRIVATE_VALUES = {...};
public static final Thing[] values() {
return PRIVATE_VALUES.clone(); //방어적 복사본
}
배열을 private 로 만들고 public 메서드를 추가한다.
"프로그램 요소들의 접근성은 가능한 최소한으로 하자!"
'독후감 > Effective JAVA' 카테고리의 다른 글
[Effective JAVA] 17 "변경 가능성을 최소화하라" (0) | 2022.05.10 |
---|---|
[Effective JAVA] 16 "public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라" (0) | 2022.05.10 |
[Effective JAVA] 14 "Comparable 을 구현할지 고려하라" (0) | 2022.05.06 |
[Effective JAVA] 13 "clone 재정의는 주의해서 진행하라" (0) | 2022.05.05 |
[Effective JAVA] 12 "toString 을 항상 재정의하라" (0) | 2022.05.03 |