캡슐화
잘 설계된 컴포넌트의 기본은 내부 정보를 외부로부터 잘 은닉하기! 즉 캡슐화이다.
내부 구현을 완벽히 숨겨서 "구현"과 "API"를 깔끔히 분리하는 것이다.
소프트웨어 설계의 근간이다! 항상 고민하도록 하자.
캡슐화 장점은 간단히..
- 시스템 개발 속도를 높인다.
- 시스템 관리 비용을 낮춘다.
- 캡슐화 자체가 성능을 높여주진 않으나 최적화에 도움을 준다.
(다른 컴포넌트에 영향 주지 않고 해당 컴포넌트만 최적화 가능)
- 재사용성을 높인다.
- 큰 시스템 제작 난이도를 낮춘다.
접근 제한자를 잘 다루는 것이 중요
private, protected, public ... 을 잘 활용하는 것이 캡슐화의 핵심이다!
기본 원칙은 간단하다. "모든 클래스와 멤버의 접근성을 가능한 좁혀야 한다"는 것이다.
가능한 가장 낮은 접근 수준을 부여해야 한다.
접근제한 종류
접근제한은 어디에 붙일 수 있을까?
"클래스/인터페이스" 또는 "멤버"에 붙일 수 있다.
▶ 클래스와 인터페이스 접근제한
이 클래스의 목적이 공개 API인지를 생각해보면 편할 것이다. 클래스나 인터페이스에는 public 또는 package-private를 지정 가능하다. 만약 public으로 선언하면 공개 API가 되며, package-private로 선언하면 해당 패키지 내에서만 사용가능하다.
- public 선언 즉 API인 경우 하위 호환을 위해 영원히 관리해주어야 한다.
- package-private일 경우 API가 아닌 내부 구현이 되어 언제든 수정할 수 있다.
▶ 멤버 접근제한
*멤버: 필드, 메서드, 중첩 클래스, 중첩 인터페이스
─ private: 자신만 접근 가능
─ package-private: 자신의 패키지 내 모든 클래스에서 접근 가능
─ protected: 자신의 패키지 내 모든 클래스 및 하위클래스에서 접근 가능
─ public: 모든 곳에서 접근 가능!
멤버 접근제한 가이드: 클래스의 공개 API를 세심히 설계한 후, 그 외의 모든 멤버는 private으로 한다. 그리고나서, 오직 '같은 패키지의 다른 클래스'가 접근해야 하는 멤버에 한하여 package-private으로 풀어준다. 그러다가 여러분이 권한을 좀 빈번히 풀어주고 있는 것 같다는 생각이 들면, 컴포넌트 분해를 다시 고민해보자.
멤버 접근성을 좁히지 못하는 예
- 오버라이딩의 경우 상위 메서드보다 좁게 할 수 X
*cf. 리스코프 치환 원칙(Item10), 상위 클래스의 인스턴스는 하위 클래스의 인스턴스로 대체해 사용할 수 있어야 하므로
*cf. 클래스가 인터페이스를 구현하는 클래스는 오버라이딩 메서드를 public으로 선언해야 함
각종 유의점 및 추천사항
아래 두가지 추천사항은 "접근성 좁히기"를 하는 방식을 추천하고 있다.
■ 추천:
어떤 package-private 클래스 또는 인터페이스를 사용하고 있는 클래스가 단 하나밖에 없다면, 해당 package-private 클래스를 사용하는 클래스 안에 private static으로 중첩시켜보자(Item24).
─ 이유:
전자는 같은 패키지의 모든 클래스가 접근할 수 있는 상태지만, 후자처럼 private static으로 중첩시키면 바깥 클래스 하나에서만 접근할 수밖에 없기 때문이다.
■ 추천(중요):
public일 필요가 없는 클래스의 접근 수준을 package-private로 좁히자.
─ 이유:
public 클래스는 그 패키지의 API인 반면, package-private는 내부 구현에 속하기 때문이다.
.
.
아래 주의점들은 크게는 "나도 모르게 공개 API로 노출시키지 않기", "public static final 필드가 '가변 객체'가 아닌 '불변 객체'를 참조하는지 꼭 확인하기"를 강조하고 있다.
■ 주의:
private과 package-private 멤버는 모두 해당 클래스의 구현에 해당하므로 보통은 공개 API에 영향을 주지 않는다. 단, Serializable을 구현한 클래스에서는 그 필드들도 의도치 않게 공개 API가 될 수도 있음을 주의하자(Item86)(Item87)
■ 주의:
단지 테스트를 위해서 클래스, 인터페이스, 멤버의 접근 수준을 넓이려 하는 경우가 있다. 주의하라. 테스트만을 위해서 공개 API를 만들어서는 안 된다. 테스트 코드를 테스트 대상과 같은 패키지에 두어 package-private까지는 허용하는 것은 가능하다.
■ 주의:
public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다(Item16). 왜냐하면 필드의 불변식을 보장하지 못하기 때문이다 즉 담길 값을 제한하지 못하게 된다. 만약 필드가 가변 객체를 참조하거나, final이 아닌 상황에서 접근제한이 public이라면 언제든 값이 변경될 수도 있을 것이다.
+
필드가 수정된다면, 락 획득 같은 다른 작업을 할 수 없게 되므로 public 필드를 갖는 클래스는 일반적으로 thread-safe하지 않다.
+
위까지는 이해했는데,
책에서 이 설명이 잘 이해가 가지 않는다. 그대로 적어두겠다.
"여기에 더해, 필드가 수정될 때 (락 획득 같은) 다른 작업을 할 수 없게 되므로 public 가변 필드를 갖는 클래스는 일반적으로 스레드 안전하지 않다. 심지어 필드가 final이면서 불변 객체를 참조하더라도 문제는 여전히 남는다. 내부 구현을 바꾸고 싶어도 그 pubilc 필드를 없애는 방식으로는 리팩터링할 수 없게 된다"
(의문1)
음.. 필드가 final이면서 불변 객체를 참조하며 public인 경우에 왜 thread-safe하지 않을까? 멀티스레드 환경에서 주의해야 할 상황은 어차피 값을 read할 때는 상관 없고 write할 때만 주의해야 하는 것 아닌가?
(의문2)
마지막 문장이 무슨 이야기인지 모르겠다.
■ 주의:
길이가 0이 아닌 배열은 모두 변경가능하니 주의하자. 따라서 클래스에서 public static final 배열 필드를 두거나, 이 필드를 반환하는 접근자 메서드를 제공해서는 안 된다! 클라이언트에서 배열을 수정할 수 있게 하는 격이다.
+
예를 들어 다음 코드가 그렇다.
// 보안 허점 있음!
// public이면 안되고, private 이어야 한다.
public static final Thing[] VALUES = { ... };
// 방어 방법 1 : public 불변 리스트 사용
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
// 방어 방법 2 : 방어적 복사 사용
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
return PRIVATE_VALUES.clone()
}
+
단, 어떤 추상화에서 꼭 필요한 상수 개념이라면 public static final 필드로 공개해도 좋다. 이런 필드는 반드시 기본 타입 값이나 불변 객체를 참조해야 한다(Item17) 만약 가변 객체를 참조한다면 끔찍하게도 final인 것이 소용없이 내부 값이 변하므로 그렇다.
자바의 접근 보호 방식: 모듈 시스템
자바9에서는 "모듈 시스템" 이라는 개념이 도입되었다. 이 모듈은 패키지의 묶음이다. 모듈 자신에 속하는 패키지 중 공개할 것들을 별도로 선언할 수 있다. 어떻게? 공개(export)할 것들을 module-info.java(관례)에 선언하면 된다.
*패키지: 클래스들의 묶음
모듈 시스템을 사용하면, 아무리 public, protected 멤버라도 해당 패키지를 공개하지 않았다면 모듈 외부에서는 접근할 수 없다! 모듈 외부로부터의 접근 제한을 module-info.java를 통해 설정할 수 있는 것이다.
(당연히, 모듈 내부에서는 exprots로 선언했든 하지 않았든 아무 영향을 받지 않음)
모듈 시스템을 활용하면 편한 점이, 클래스를 외부에 공개하지 않으면서도 같은 모듈을 이루는 패키지 사이에서는 자유롭게 공유할 수 있다! 내가 멤버를 public, protected로 정하더라도, 그 접근제한이 "모듈 내부로" 한정되는 것이다.
─ 모듈의 주의점 (상당히 주의...)
- 일단, 이런 형태로 공유해야 하는 상황은 흔하지 않다. 그래야 하는 상황이 벌어지더라도, 패키지들 사이에서 클래스들을 재배치하면 대부분 해결된다.
- 여러분 모듈의 JAR파일을 자신의 모듈 경로가 아닌 애플리케이션의 classpath에 두면 그 모듈 안의 모든 패키지는 마치 모듈이 없는 것처럼 행동한다. 즉 public 클래스의 모든 public, protected 멤버들이 "모듈 밖에서도 접근 가능해지는 것이다"
- 이펙티브 자바가 쓰여진 시점에서는 이렇게 말한다. "JDK 외에도 모듈 개념이 널리 받아들여질지 예측하기에는 아직 이른 감이 있다. 그러니 꼭 필요한 경우가 아니라면 당분간은 사용하지 않는 게 좋을 것 같다."
─ 활용 예시
모듈 시스템의 접근 수준을 적극 활용한 예시는 JDK 그 자체다.
자바 라이브러리에서 공개하지 않은 패키지들은 해당 모듈 밖에서는 절대 접근할 수 없다.
핵심
- 접근성은 가능한 좁게하자.
- 의도치 않게 API로 공개되면 안된다.
- public 클래스는 public static final 상수 외에는 어떠한 public 필드도 가져선 안된다.
- public static final 필드가 '가변 객체'가 아닌 '불변 객체'를 참조하는지 꼭 확인하라.
'JAVA > Effective Java' 카테고리의 다른 글
[이펙티브 자바] 변경 가능성을 최소화하라 ─ 4장:클래스와 인터페이스:Item17 (0) | 2023.10.25 |
---|---|
[이펙티브 자바] public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 ─ 4장:클래스와 인터페이스:Item16 (0) | 2023.10.25 |
[이펙티브 자바] 최적화는 신중히 사용하라 ─ 9장:일반적인 프로그래밍 원칙:Item67 (0) | 2023.10.22 |
[이펙티브 자바] 네이티브 메서드는 신중히 사용하라 ─ 9장:일반적인 프로그래밍 원칙:Item66 (0) | 2023.10.22 |
[이펙티브 자바] 리플렉션보다는 인터페이스를 사용하라 ─ 9장:일반적인 프로그래밍 원칙:Item65 (0) | 2023.10.21 |