-
Notifications
You must be signed in to change notification settings - Fork 7
[3주차] 공희상_item15, item21 #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| # 클래스와 멤버의 접근 권한을 최소화하라 | ||
|
|
||
| ## 잘 설계된 컴포넌트 | ||
|
|
||
| 책에서는 잘 설계된 컴포넌트에 대해 다음과 같이 설명한다. | ||
|
|
||
| - 클래스 내부 데이터와 내부 구현 정보를 외부로 부터 잘 숨겨야 한다. | ||
| - API를 통해서만 다른 컴포넌트와 소통해야 하며, 내부 동작 방식에 개의치 않아야 한다. | ||
|
|
||
| 이는 캡슐화(정보 은닉)의 개념으로, 이를 잘 지키는 것이 잘 설계된 컴포넌트라고 말하고 있다. | ||
|
|
||
| ## 캡슐화의 장점 | ||
|
|
||
| 캡슐화는 각각의 컴포넌트들을 서로 독립시켜서 개별적으로 동작할 수 있게 함으로써 다음과 같은 장점이 있다. | ||
|
|
||
| - 각각의 컴포넌트를 병렬로 개발할 수 있끼에 개발 속도를 높인다. | ||
| - 각 컴포넌트를 더 빨리 파악하여 디버깅이 할 수 있고, 교체 부담이 적어서 시스템 관리 비용을 낮춘다. | ||
| - 다른 컴포넌트에 영향을 안주고 해당 컴포넌트만 최적화가 가능하여 성능 최적화에 도움을 준다(아이템 67). | ||
| - 기존 환경에 대한 의존성이 낮아 독자적으로 동작이 가능해 다른 환경에서의 이식성 및 재사용성을 높인다. | ||
| - 개별 컴포넌트의 동작을 검증할 수 있어, 큰 시스템 상황에서 제작 및 관리하는 난이도를 낮춘다. | ||
|
|
||
| ## 접근 제어자 | ||
|
|
||
| 자바에서는 이러한 캡슐화를 제공하기 위해 다양한 장치를 제공한다. | ||
| 그 중 하나가 접근 제어자이다. | ||
| 접근 제어자를 통해 클래스, 인터페이스, 멤버의 접근 허용 범위를 명시할 수 있다. | ||
|
|
||
| ## 톱레벨 클래스와 인터페이스 | ||
|
|
||
| 가장 외부의 톱레벨 클래스와 인터페이스에 부여할 수 있는 접근 수준은 `package-private`와 `public`이다. | ||
| 톱레벨 클래스와 인터페이스에 대해서는 패키지 외부에서 사용할 이유가 없다면, `package-private`로 선언하라고 권장한다. | ||
| 그 이유는 클라이언트에 영향을 주지 않고 내부 구현을 변경할 수 있기 때문이다. | ||
| 반면, `public`으로 선언하면 해당 클래스나 인터페이스를 사용하는 클라이언트에게 영구적인 API로 제공하게 되므로, 하위 호환성 유지를 위해 내부 구현 변경이 어려워진다. | ||
|
|
||
| > 톱레벨에 위치한다는 것은 같은 패키지의 모든 클래스가 해당 클래스에 접근할 수 있다는 것을 의미한다. | ||
| > 하지만 한 클래스에서만 사용하는 `package-private` 톱레벨 클래스는 외부 접근을 방지하기 위해 `private static`으로 중첩시키는 것을 권장한다. | ||
|
|
||
| ```java | ||
| public class Outer { | ||
| private static class Inner { ... } | ||
| } | ||
| ``` | ||
|
|
||
| ## 멤버의 접근 수준 | ||
|
|
||
| 그럼 `public`만 신경 쓰면 될까? | ||
| 책에서는 `public`일 필요가 없는 클래스의 접근 수준을 `package-private` 톱레벨 클래스로 더 좁게 최소한으로 설정하라고 권장하고 있다. | ||
| 멤버(필드, 메서드, 중첩 클래스, 중첩 인터페이스)에 부여할 수 있는 접근 수준은 다음과 같다. | ||
|
|
||
| - `private`: 멤버를 선언한 톱레벨 클래스에서만 접근할 수 있다. | ||
| - `package-private`: 멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다. 접근 제한자를 명시하지 않았을 때 적용되는 패키지 접근 수준이다(단, 인터페이스의 멤버는 기본적으로 `public`이 적용된다). | ||
| - `protected`: `package-private`의 접근 범위를 포함하며, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근할 수 있다(제약이 조금 따른다). | ||
| - `public`: 모든 곳에서 접근할 수 있다. | ||
|
|
||
| 이러한 접근 제어자를 통해 공개 API를 엄격히 설계해야 한다. | ||
| 다른 클래스가 반드시 접근해야 하는 멤버에 한하여 `private`를 제거해, `package-private`으로 풀어줘도 된다. | ||
| 하지만, 이러한 상황이 빈번하게 발생한다면 시스템 컴포넌트를 더 분해해야 하는지 고려해봐야한다. | ||
|
|
||
| `package-private`에서 `protected`로 접근 수준을 넓히는 순간 그 멤버에 대한 대상 범위가 넓어지게 되는데, | ||
| 이때 `public` 뿐만 아니라 `protected`도 공개 API로 취급되기 때문에, 내부 동작 방식이 담긴 API 문서화의 대상이 된다. | ||
| 그래서 `protected`의 멤버를 최대한 줄이고, `private`과 `package-private`을 사용하는 것이 좋다. | ||
|
|
||
| > `private`과 `package-private` 멤버는 공개 API에게 영향을 주지 않는 것이 보통이지만, `Serializable`을 구현하는 클래스에서는 의도치 않게 공개 API로 노출될 수 있다. | ||
|
|
||
| 그런데 멤버 접근성을 좁히지 못하게 방해하는 제약이 있다. | ||
| 상위 클래스의 메서드를 재정의할 때는 그 접근 수준을 상위 클래스의 접근 수준보다 좁게 설정할 수 없다. | ||
| 이 제약은 상위 클래스의 인스턴스는 하위 클래스의 인스턴스로 대체할 수 있어야 한다는 규칙 리스코프 치환 법칙을 지키기 위해 필요하다. | ||
| 만일 이 제약을 어기면 컴파일 단계에서 오류가 날 것이다. | ||
|
|
||
| ## 테스트와 접근 제어자 | ||
|
|
||
| 테스트 코드를 작성하기 위해 클래스, 인터페이스, 멤버의 접근 범위를 넓혀야 할 때가 있다. | ||
| `private`에서 `package-priavte` 까지의 완화 허용을 하지만, | ||
| 그 이상으로 완화해서 공개 API로 노출 시키는 것을 피해야 한다. | ||
|
|
||
| > 실제로 테스트 코드를 같은 패키지에 두면 `package-private` 멤버에 접근할 수 있기 때문에, 테스트 코드를 같은 패키지에 두는 것이 일반적이다. | ||
|
|
||
| ## public 필드의 위험성과 해결책 | ||
|
|
||
| `final`이 아닌 인스턴스 필드를 `publi`c으로 선언하는 것은 좋지 않다. | ||
| 이는 필드에 대한 제어권을 잃어 불변식을 보장할 수 없게 되기 때문이다. | ||
| 또한, 필드가 수정될 때 락(Lock) 같은 작업을 할 수 없게 되어 멀티 스레드 환경에서 스레드 안정성을 보장하지 못한다. | ||
|
|
||
| 그러면 `public final`로 선언하면 되지 않을까?라고 생각할 수 있지만, 이는 불변식을 보장하지만, 캡슐화를 깨는 행위이다. | ||
|
|
||
| ```java | ||
| public final int[] VALUES = {1, 2, 3}; | ||
|
|
||
| public void test() { | ||
| VALUES[0] = 4; // 이러한 변경이 가능해진다. | ||
| assertThat(VALUES[0]).isNotEqualTo(1); | ||
| } | ||
| ``` | ||
|
|
||
| > 상수라는 예외가 하나 있다. 이럴 때 `public static final`로 선언하는 것하여 공개하여 사용해도 된다. | ||
| > 이때, 관례상 상수의 이름은 대문자로 작성하며, 단어 사이는 밑줄로 구분한다. 또한 이런 필드는 반드시 기본 타입이나 불변 객체로 초기화해야 한다. | ||
|
|
||
| 이러한 문제를 해결하기 위해, 책에서는 두 가지 방법을 제시한다. | ||
|
|
||
| ```java | ||
| public static final Thing[] VALUES = { ... };// 접근이 가능해서 배열의 내용을 변경할 수 있다. | ||
| ``` | ||
|
|
||
| 첫 번째 방법으로는 `public` 배열을 `private`으로 만들고, `public` 불변 리스트를 추가하는 것이다. | ||
|
|
||
| ```java | ||
| private static final Thing[] PRIVATE_VALUES = { ... }; | ||
| public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES)); | ||
| ``` | ||
|
|
||
| 두 번째 방법으로는 `private` 배열을 만들고, 그 복사본을 반환하는 `public` 메서드를 추가하는 방법이다(방어적 복사). | ||
|
|
||
| ```java | ||
| private static final Thing[] PRIVATE_VALUES = { ... }; | ||
| public static final Thing[] values() { | ||
| return PRIVATE_VALUES.clone(); | ||
| } | ||
| ``` | ||
|
|
||
| ## 자바 9 모듈 시스템 | ||
|
|
||
| 자바 9에서 도입된 모듈 시스템은 패키지의 몪음 단위로 접근성을 제어한다. | ||
| 모듈 선언 파일(module-info.java)에서 `exports` 키워드를 사용해 패키지를 공개할 수 있다. | ||
| 이를 통해 모듈의 공규 여부와 상관없이 모듈 내부의 패키지를 외부로 노출하지 않고, 자유롭게 공유할 수 있다. | ||
| 이러한 기술 활용한 대표 사례가 JDK이다. | ||
| 하지만, 사용이 간단하지 않고, 모듈 시스템을 사용하려면 모든 코드를 모듈 시스템에 맞게 수정해야 한다. | ||
| 이로 인해, 반드시 필요한 경우가 아니라면 모듈 시스템을 사용하지 않는 것이 좋다. | ||
|
|
||
| ## 정리 | ||
|
|
||
| 프로그램 요소의 접근 성은 가능한 한 최소한으로 해서 멤버가 의도치 않게 API로 공개되는 일이 없도록 해야 한다. | ||
| 그리고 public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안 된다. | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # 인터페이스는 구현하는 쪽을 생각해 설계하라 | ||
|
|
||
| 자바 8 이전에는 기존 인터페이스에 메서드르 추하려면 기존 구현체를 수정해야 했고, | ||
| 이를 안할 시 컴파일 오률를 일으켰다. | ||
| 자바 8에서는 디폴트 메서드라는 기능을 도입하면서 인터페이스에 새로운 메서드를 추가할 수 있게 되었지만, | ||
| 모든 상황에 대비하여 안전하게 동작하리라는 보장은 없다. | ||
|
|
||
| ## 디폴트 메서드와 위험성 | ||
|
|
||
| 디폴트 메서드는 인터페이스 구현체에서 재정의하지 않으면 기본 구현을 사용한다. | ||
| 이런 면에서 기존 클래스들은 새로운 메서드의 동작과 충돌할 수 있다. | ||
| 예를 들어, `removeIf` 디폴트 메서드는 대부분의 상황에서 잘 동작하지만, | ||
| 기존 구현체가 가진 고유 불변식이나 동기화 정책을 깨뜨릴 위험이 있다. | ||
|
|
||
| 실제로 아파치 토미캣 8.5 버전에서는 `removeIf` 메서드를 사용하면 `ConcurrentModificationException`이 발생하는 문제가 있었다. | ||
| 이는 아파치 커먼즈의 `Collectoins.synchronizedCollection` 클래스는 동기화를 위해 메서드 호출하다 락을 걸지만, | ||
| `removeIf` 디폴트 구현은 동기화와 관련된 지식이 없기 때문이다. | ||
|
|
||
| ## 디폴트 메서드와 기존 구현체의 충돌 | ||
|
|
||
| 이렇듯 디폴트 메서드는 기존 구현체와 충돌할 수 있다. | ||
| 그럼 자바 표준 라이브러리에서는 어떻게 이 문제를 해결했을까? | ||
| 자바에서는 기존 인터페이스를 구현하는 클래스에서 디폴트 메서드를 재정의하거나, | ||
| 디폴트 메서드를 호출하기 전 동기화같은 작업을 수행하도록 했다. | ||
|
|
||
| ```java | ||
| @Override | ||
| public synchronized boolean removeIf(Predicate<? super E> filter) { | ||
| return c.removeIf(filter); // c는 내부 컬렉션 객체 | ||
| } | ||
|
|
||
| ``` | ||
|
|
||
| 하지만 자바 플랫폼에 속하지 않은 제 3자의 라이브러리의 구현체들은 이런 수정이 어려우며, 이로인해 런타임 오류가 발생할 수도 있다 | ||
|
|
||
| ## 디폴트 메서드의 설계 원칙 | ||
|
|
||
| 위와 같이 디폴트 메서드는 컴파일에 성공하더라도 기존 구현체에 대한 런타임 오류를 일으킬 수 있다. | ||
| 책은 이를 방지하기 위해서 다음과 같은 원칙을 제시한다. | ||
|
|
||
| 1. 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니면 피해야 한다. | ||
| 2. 추가하려는 디폴트 메서드가 기존 구현체들과 충돌하지는 않을지 테스트해야 한다. | ||
| 3. 인터페이스로부터 메서드를 제거하거나 기존 메서드의 시그니처를 수정하는 것은 기존 클라이언트를 망가뜨리게 되므로 피해야 한다. | ||
|
|
||
| ## 정리 | ||
| 디폴트 메서드는 유용한 도구지만, 기존 인터페이스를 수정할 때는 매우 신중해야 한다. | ||
023-dev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 사용을 한다면, 다양한 테스트를 통해 완성도를 높이고, 릴리스 전에 결함을 수정해야 한다. | ||
|
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.