Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# "아이템 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라."

핵심 정리
- 상속용 클래스는 내부 구현을 문서로 남겨야 합니다.
- @implSpec을 사용할 수 있습니다.
- 내부 동작 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드로 공개해야 합니다.
- 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 합니다.
- 상속용 클래스의 생성자는 재정의 가능한 메서드를 호출해서는 안됩니다.
- Cloneable(아이템 13)과 Serializable(아이템 86)을 구현할 때 조심해야 합니다.
- 상속용으로 설계한 클래스가 아니라면 상속을 금지해야합니다.
- final 클래스 또는 private 생성자

-----------------------------------------------
## 1. 상속 가능 메서드(override 가능 메서드)에 대한 문서화

상속용 클래스를 설계할 때는 하위 클래스에서 재정의할 수 있는 메서드들이 어떤 역할을 하는지 정확히 문서로 남겨야 합니다.
즉, 재정의할 수 있는 메서드들이 내부적으로 어떻게 동작하고, 어떤 순서로 호출되며, 그 호출 결과가 전체 흐름에 어떤 영향을 미치는지를 모두 명시해야 합니다.

재정의 가능한 메서드의 범위
• public, protected 메서드 중 final이 아닌 메서드

문서에 포함해야 할 내용
1. 메서드의 API 설명
• 어떤 일을 하는 메서드인지, 파라미터와 반환값이 각각 어떤 의미를 갖는지 등을 구체적으로 작성합니다.
2. 호출 순서
• 해당 메서드가 실행되는 구체적인 순서를 명시합니다.
• 예: “add 메서드가 호출되면, 내부적으로 먼저 validate 메서드를 호출한 후, 요소를 추가하고, 마지막에 notifyChange 메서드를 호출한다.”
3. 호출 결과의 영향
• 재정의한 메서드가 호출된 뒤, 이어지는 처리 과정(예: 상태 변경, 다른 메서드 호출 등)에 어떠한 영향을 주는지 설명합니다.
4. 메서드가 호출될 수 있는 모든 상황
• 언제, 어떤 조건에서 해당 메서드가 호출되는지 “구체적으로” 작성합니다.
5. 메서드의 내부 동작(Implementation Requirements, @implSpec)
• 내부에서 어떤 로직을 수행하는지, 어떤 데이터를 갱신하는지 등을 “설계 문서나 Javadoc(@implSpec)” 등에 기술합니다.

예시
• AbstractCollection Java 11 문서:
[AbstractCollection Java 11 문서](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/AbstractCollection.html#remove(java.lang.Object))
> public boolean remove(Object o) 주어진 원소가 이 컬렉션 안에 있다면 그 인스턴스를 하나 제거한다. 더 정확하게 말하면 이 컬렉션 안에 'Object.equals(e, e)가 참인 원소' e가 하나 이상 있다면 그 중 하나를 제거한다. 주어진 원소가 컬렉션 안에 있다면 true를 반환한다.
@implSpec (Implementation Requirements) 이 메서드는 컬렉션을 순회하며 주어진 원소를 찾도록 구현되었다. 주어진 원소를 찾으면 반복자의 remove 메서드를 사용해 컬렉션에서 제거한다. 이 컬렉션이 주어진 객체를 갖고 있으나, 이 컬렉션의 iterator 메서드가 반환한 반복자가 remove 메서드를 구현하지 않았으면 UnsupportedOperationException을 던지니 주의하자.

AbstractCollection의 remove 메서드는 재정의하기 쉽도록 자세한 문서를 제공합니다. AbstractCollection의 remove 문서에서는 “remove 호출 시 iterator.remove()를 통해 원소가 제거되며, 해당 iterator가 이를 지원하지 않으면 UnsupportedOperationException이 발생한다” 등을 명시합니다.
이를 통해 iterator()나 remove()를 재정의할 때 서로 어떤 영향을 주고받는지 쉽게 파악할 수 있습니다.

```java
/**
* {@inheritDoc}
*
* @implSpec
* This implementation iterates over the collection looking for the
* specified element. If it finds the element, it removes the element
* from the collection using the iterator's remove method.
*
* <p>Note that this implementation throws an
* {@code UnsupportedOperationException} if the iterator returned by this
* collection's iterator method does not implement the {@code remove}
* method and this collection contains the specified object.
*
* @throws UnsupportedOperationException {@inheritDoc}
* @throws ClassCastException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public boolean remove(Object o) {
Iterator<E> it = iterator();
if (o==null) {
while (it.hasNext()) {
if (it.next()==null) {
it.remove();
return true;
}
}
} else {
while (it.hasNext()) {
if (o.equals(it.next())) {
it.remove();
return true;
}
}
}
return false;
}

```

## 2. 클래스 동작 중간에 개입이 필요하다면 ‘훅(hook) 메서드’를 protected로 제공

상위 클래스의 동작 흐름을 변경하지 않으면서도 하위 클래스가 특정 로직을 추가해야 할 수 있습니다. 이때는 ‘훅(hook) 메서드’를 protected로 공개하여, 하위 클래스가 원하는 기능을 적절히 삽입할 수 있게 합니다.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상위 클래스의 동작 흐름을 변경하지 않는다는 것이 정확히 어떤 의미인지 궁금합니다!

Copy link
Copy Markdown
Contributor Author

@yunjeooong yunjeooong Jan 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상위 클래스가 내부적으로 처리하는 흐름(flow), 즉 어떤 순서로 메서드를 호출하고 어떤 로직으로 진행하는지는 그대로 유지한다는 뜻이라고 저는 이해했습니다!
예를 들어, 상위 클래스가 a→ b → c와 같은 순서를 갖고 있다면, 하위 클래스가 메서드를 재정의해도 이 순서 자체는 바뀌지 않는다는 것으로 이해 했습니다. 위 정리글을 보면 하위 클래스는 지정된 “훅(hook) 메서드”를 재정의해서 새로운 기능을 끼워 넣을 수 있지만 상위 클래스의 전체 프로세스는 그대로 흘러간다는 뜻입니다,
결과적으로, 상위 클래스는 자신이 의도한 로직을 안전하게 지키면서도 하위 클래스가 필요한 부분만 확장할 수 있게 돕는 것이라고 생각하시면좋을 것 같습니다:)

예시
AbstractList의 removeRange(int fromIndex, int toIndex)는 부분 리스트 제거 로직을 개선할 수 있도록 제공됩니다.
하위 클래스가 이 메서드를 재정의해 내부 구조를 활용하면, clear 같은 연산을 더 빠르게 구현할 수 있습니다.

> java.util.AbsractList의 removeRange 메서드
protected void removeRange(int fromIndex, int toIndex) fromIndex(포함)부터 toIndex(미포함)까지의 모든 원소를 이 리스트에서 제거한다. toIndex 이후의 원소들은 앞으로 (index만큼씩) 당겨진다. 이 호출로 리스트는 'toIndex - fromIndex'만큼 짧아진다. (toIndex == fromIndex라면 아무 효과도 없다.) 이 리스트 혹은 이 리스트의 부분리스트에 정의된 clear 연산이 이 메서드를 호출한다. 리스트 구현의 내부 구조를 활용하도록 이 메서드를 재정의하면 이 리스트와 부분리스트의 clear 연산 성능을 크게 개선할 수 있다. Implementation Requirements: 이 메서드는 fromIndex에서 시작하는 리스트 반복자를 얻어 모든 원소를 제거할 때까지 ListIterator.next와 ListIterator.remove를 반복 호출하도록 구현되었다. 주의: ListIterator.remove가 선형 시간이 걸리면 이 구현의 성능은 제곱에 비례한다.
Parameters: fromIndex 제거할 첫 원소의 인덱스 toIndex 제거할 마지막 원소의 다음 인덱스

## 3. 상속용으로 설계한 클래스는 배포 전에 실제로 ‘하위 클래스’를 작성해 검증하라

1. 필요한 protected 멤버를 놓쳤는지 발견할 수 있습니다.
2. 사용되지 않는 protected 멤버는 사실 private여도 되었음을 깨달을 수 있습니다.

• 하위 클래스를 최소 3개 이상 작성해보되, 그 중 하나 이상은 제3자(외부)에게 작성하도록 하는 것이 좋습니다.
• 광범위하게 쓰일 클래스일수록, 이 “내부 사용 패턴”과 “protected 메서드/필드”가 성능과 기능 면에서 제약으로 작용할 수 있음을 꼭 문서에 명시합니다
## 4. 상위 클래스 생성자에서 ‘재정의 가능 메서드’를 직접·간접적으로 호출하지 말 것

```java
public class Super {
// 잘못된 예시: 생성자에서 재정의 가능 메서드를 호출
public Super() {
overrideMe(); // 하위 클래스에서 재정의될 수 있음
}

public void overrideMe() { }
}

```

```java
public final class Sub extends Super {
private final Instant instant; // 생성자에서 초기화

Sub() {
instant = Instant.now();
}

@Override
public void overrideMe() {
System.out.println(instant); // 상위 생성자에서 이미 호출될 수 있음
}

public static void main(String[] args) {
Sub sub = new Sub(); // 첫 호출 시 null 출력 가능
sub.overrideMe();
}
}

```

-> 이 프로그램은 상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화하기도 전에 overrideMe를 호출하기 때문에
instance를 두 번 출력하지 않고, 첫 번째는 null을 출력하게 됩니다.

* 안전하게 호출할 수 있는 메서드: private, final, static 메서드는 재정의되지 않으므로 생성자에서 호출해도 괜찮습니다.

## 5. clone과 readObject에서도 ‘재정의 가능 메서드’를 호출하지 말 것
• clone():
• 상위 클래스의 clone()에서 재정의된 메서드를 호출하면, 복제본이 완전한 상태로 초기화되기 전에 호출될 수 있습니다.
• 잘못된 깊은 복사로 원본 객체 상태까지 손상시킬 위험이 있습니다.
• readObject():
• 역직렬화 과정에서 하위 클래스의 필드가 아직 다 복원되지 않은 시점에 재정의된 메서드가 불려 예기치 못한 동작을 일으킬 수 있습니다.

결과적으로, 생성자에서 재정의 가능 메서드를 부르면 안 되듯이, clone과 readObject 역시 동일한 주의를 기울여야 합니다.

## 6. Serializable을 구현한 상속용 클래스의 readResolve/writeReplace 메서드는 protected로 선언
• private로 선언하면 하위 클래스에서 이 메서드들이 무시됩니다.
• 상속을 위해서는 내부 구현을 일정 부분 공개해야 하므로, protected로 선언해 하위 클래스가 필요한 경우 오버라이드할 수 있도록 합니다.

## <상속을 금지하는 방법>

상속용으로 설계하지 않은 클래스라면, 원치 않는 확장을 막기 위해 상속을 금지해야 합니다.
1. 클래스를 final로 선언
2. 모든 생성자를 private 또는 package-private으로 선언, 대신 public static 팩터리 메서드를 제공

“구체 클래스를 상속해 계측, 알림, 동기화 기능 등을 추가하기보다는, 아이템 18에서 언급하는 ‘래퍼(Wrapper) 클래스 패턴’을 사용하는 편이 더 안전하고 유연합니다.”
## <상속을 허용해야 한다면?>

재정의 가능 메서드를 호출하는 ‘자기 사용 코드(self-use code)’를 완벽히 제거해야 합니다.

1. 재정의 가능 메서드의 실제 구현 로직을 private 도우미 메서드(helper method)로 옮깁니다.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private 도우미 메서드는 어떻게 사용하나요??

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“private 도우미 메서드”는 상위 클래스 내부에서만 쓰이는 작은 보조 메서드입니다! . 중요한 로직은 이 메서드 안에 몰아넣어서, 하위 클래스에서 재정의 가능한 메서드가 호출은 할 수 있지만 실제 로직에는 직접 손대지 못하게 보호하게 사용하시면 됩니다!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇다면 상속을 허용할 때는 항상 private 도우미 메서드를 제공해야 하나요 ❔

2. 그 메서드(재정의 가능 메서드)는 도우미 메서드만 호출하도록 만듭니다.
3. 클래스 내에서 재정의 가능 메서드를 직접 호출하는 부분이 있다면, 모두 도우미 메서드를 호출하도록 수정합니다.

이렇게 하면 하위 클래스가 메서드를 재정의해도, 상위 클래스 내부 동작과의 의도치 않은 간섭을 최소화할 수 있습니다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# 아이템 25. 톱 레벨 클래스는 한 파일에 하나만 담으라
핵심 정리

• 한 소스 파일에 톱 레벨 클래스를 여러 개 선언하면 컴파일 순서에 따라 결과가 달라질 수 있습니다.
• 다른 클래스에 딸린 부차적인 클래스는 정적 멤버 클래스로 만드는 것이 낫습니다.
-> 읽기 좋으며 private으로 선언해서 접근 범위도 최소한으로 관리할 수 있습니다.

----------------------

소스 파일 하나에 톱레벨 클래스를 여러 개 선언하더라도 자바 컴파일러는 문제 없이 컴파일을 합니다.
하지만 이는 심각한 위험을 감수해야 하는 행위입니다.
이렇게 하게 된다면 한 클래스를 여러 가지로 정의할 수 있으며, 그중 어느 것을 사용할지는 어느 소스 파일을 먼저 컴파일하느냐에 따라 달라지기 때문입니다.

구체적인 예를 보겠습니다.

```java

public class Main {
public static void main(String[] args) {
System.out.println(Utensil.NAME + Dessert.NAME);
}
}

```
다음은 두 클래스들을 한 파일에 담아보았습니다.
```java

class Utensil {
static final String NAME = "pan";
}

class Dessert {
static final String NAME = "cake";
}

```
```java

class Utensil {
static final String NAME = "pot";
}

class Dessert {
static final String NAME = "pie";
}

```



javac Main.java Dessert.java 명령으로 컴파일 한다면, 운좋게 오류가 나며 두 클래스를 중복 정의했다고 알려줄 것입니다.

⭐️ 컴파일 과정
1. Main.java 컴파일
2. Utensil 참조를 보고 Utensil.java 파일에서 Utensil + Dessert를 찾아냄
3. 두 번째 명령어 인수줄의 Dessert.java 를 처리하려 할때 에러 발생

하지만 javac Main.java 혹은 javac Main.java Utensil.java 로 컴파일을 하면은 pancake을 출력하고, javac Dessert.java Main.java 로 컴파일을 하면은 potpie를 출력하게됩니다.

위에서 말한것 처럼
"이처럼 컴파일러에 어느 소스 파일을 먼저 건네느냐에 따라 동작이 달라지는 문제가 발생하는 것입니다!"

⭐️문제의 해결책: 파일 분리
단순히 톱 레벨 클래스들(Utensil, Dessert)를 서로 다른 소스 파일로 분리하기만 하면 문제가 해결됩니다.

굳이 여러 톱레벨 클래스를 한 파일에 담고 싶다면, 정적 멤버 클래스를 고려해봅시다.
-> 읽기도 좋고, private 으로 선언 하면 접근 범위도 최소로 관리할 수 있습니다.

▶ 톱레벨 클래스들을 정적 멤버 클래스로 변경

```java

public class Test {
public static void main(String[] args) {
System.out.println(Utensil.NAME + Dessert.NAME);
}

private static class Utensil {
static final String NAME = "pan";
}

private static class Dessert {
static final String NAME = "cake";
}
}

```