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,67 @@
## item 12

### toString을 항상 재정의하라

---

### 🙋‍♀️ 왜 재정의하는 게 좋은가요?

기본적인 `Object`의 `toString` 메서드를 사용한다면, 우리가 원하는 대로 문자열을 보통 반환하지 않는 다는 것을 알 수 있습니다.
`PhoneNumber@abdbd` 처럼 `클래스_이름@16진수로_표시한_해시코드`를 반환할 뿐입니다.

그래서 우리는 `toString` 일반 규약에 따라 `간결하면서 사람이 읽기 쉬운 형태의 유익한 정보`를 반환해줘야 합니다.
또한 이는 `모든 하위 클래스에서 이 메서드를 재정의하라` 라고 표현될만큼 매우 강조되고 있습니다.

결국 재정의를 하게 되면, 해당 클래스를 사용할 때`즐겁고, 디버깅하기 쉽`습니다.

#### 예시 1) 오류 메세지 로깅
우리가 작성한 객체를 참조하는 컴포넌트가 오류 메세지를 로깅할 때 `toString`은 자동으로 호출될 수 있습니다.
이때 보다 쉽게 파악하기 위해 `toString`을 정의해둔다면 `보다 의미있는 메세지를 얻어갈 것`이기에 재정의하는 게 좋습니다.

대부분 프로그래머들은 진단 메시지를
```java
System.out.println(phoneNumber + "에 연결할 수 없습니다.");
```
해당 방식으로 작성하게 됩니다. 재정의를 하지 않았다면 의미없는 메세지를 보게 되는 것입니다.

#### 예시 2) 해당 객체를 포함하는 상황에서 출력을 진행
이 객체를 포함하는 상황(ex `map 객체`)에 출력을 할 때 보다 의미있게 쓰이게 됩니다.
`{Jenny=PhoneNumber@abdbd}`가 아닌 `{Jenny=707-867-5309}` 라는 의미있는 정보를 받게 될테니 말입니다.

---

### 🙋‍♀️ 그렇다면 어떻게 toString을 재정의해야 하나요?

#### 1) 객체가 가진 주요 정보 모두를 반환하는 게 좋습니다.
#### 2) `객체가 거대`하거나 `객체의 상태를 문자열로 표현하는 게 적합하지 않`다면 요약 정보를 담아서 합니다.
#### 3) 반환값을 문서화할지 정합니다.

---

### 🙋‍♀️ 문서화할지 어떻게 정하나요?

#### 1) 문서화의 장점
표준적이고, 명확하고, 사람이 읽을 수 있게 됩니다.
그렇기에 `그대로 입출력에 사용`하거나 `데이터 객체로 저장`하며 사용할 수 있습니다.

따라서 문서화를 할것이라면, 명시한 포맷에 맞는 문자열과 객체를 상호 전환할 수 있는
`정적 팩터리`나 `생성자`를 함께 제공해주면 좋습니다.

#### 2) 문서화의 단점
따라서 당연히 단점은 평생 그 포맷에 얽히게 된다는 점입니다.

#### 결론
그렇기에 명시하던 아니던, 의도는 명확히 밝혀야 합니다.
포맷을 명시하려면, 아주 정확히, 포맷을 명시하지 않기로 했다면 `형식은 정해지지 않았으며 향후 변경될 수 있다`라는 문구를 추가해 명확히 달라질 수 있음을 이야기해야한다.

### 🙌 주의점

#### 1) 문서화 여부와 상관없이 toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하는 게 좋습니다.
포맷이 되었다고 해서 해당 값에 접근할 수 있는 `getter` 가 없어서 `toString`에서 값을 파싱하는 건 성능이 나빠지고 필요하지도 않은 작업입니다.
향후 포맷이 변경되면 이는 시스템이 망가지는 결과를 초래하기에 `getter`를 지원해주는 게 중요합니다.

#### 2) 정적 유틸리티 클래스는 toString 제공할 필요 X
#### 3) 대부분의 열거 타입도 자바가 이미 완벽한 toString 제공하니 따로 제공할 필요 X
#### 4) 상위 클래스에서 이미 잘 재정의된 toString 이 있다면 제공할 필요 X
#### 5) 다만!! 하위 클래스들이 공유해야할 문자열 표현이 있는 추상 클래스에선 정의 필요!!
#### 6) AutoValue 프레임워크는 toString을 생성해주지만, 클래스의 의미가 중요한 경우 사용하지 않는 것이 좋음(ex PhoneNumber는 형식에 맞춰 반환 필요 / Potion은 자동 생성에 적합)
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ Entry deepCopy(){
> 좋은 방식은 아니다. 생성자에서는 재정의될 수 있는 메서드를 호출하지 않아야 하는데 clone 메서드도 마찬가지이기 때문이다.

### 주의점
- 상속용 클래스의 경우 loneable을 구현해서는 안됨
- 스레드 안전 클래스 작성 시 Clneable을 구현하는 모든 클래스는 clone을 재정의해야 함(접근 제한자는 pubic, 반환 타입은 클래스 자신)
- 상속용 클래스의 경우 Cloneable을 구현해서는 안됨
- 스레드 안전 클래스 작성 시 Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 함(접근 제한자는 public, 반환 타입은 클래스 자신)

### 가능한 다른 선택지! 변환 생성자/ 변환 팩터리

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
## item 17

### 변경 가능성을 최소화하라

---

### 🙋‍♀️ 왜 변경 가능성을 최소화하는 게 좋은가요?
변경 가능성이 최소화된 클래스를 `불변 클래스`라고 합니다. 이는 인스턴스 내부 값을 수정할 수 없는 클래스로, 객체가 파괴되는 순간까지 절대 달라지지 않습니다.

이런 클래스를 지향해야 하는 이유는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며,
오류가 생길 여지도 적고, 안전하기 때문입니다.

#### 1) 상대적으로 설계,구현,사용이 쉬움
- 불변:
- 프로그래머의 노력 없이도 영원히 불변으로 남음
- 값이 바뀌지 않는 상태이기에 구조가 아무리 복잡해도 불변식을 유지하기 쉬움 (실패 원자성)
- 불변 객체인 경우 불변 객체끼리는 내부 데이터 공유도 가능함
- 가변: 메서드에 의해 임의의 복잡한 상태에 놓일 수 있음
- 따라서 가변 객체 사용시에는 클래스의 보안을 지키기 위해 진짜 해당 객체인지 확인해야 함

#### 2) 스레드 안전
- 근본적으로 스레드 안전하여 따로 동기화할 필요 X
- 따라서 안심하고 공유가 가능하고, 그렇기에 최대한 재활용하는 게 좋음
- 상수로 선언하거나, 정적 팩터리를 통해 생성하여 중복 생성을 피하고, 메모리 사용량/가비지 컬렉션 비용을 줄일 수 있음
- 결국 추후에 나올 `방어적 복사`도 필요 없게 만듦(1번 장점에 해당)

---

### 🙋‍♀️ 불변 클래스는 어떻게 만들 수 있나요?

아래의 다섯 가지 규칙을 잘 따르면 됩니다.
#### 1) 객체의 상태를 변경하는 메서드를 제공하지 않는다.
#### 2) 클래스를 확장할 수 없도록 한다. 하위 클래스에서 객체의 상태를 변하게 하는 것을 막아준다. (final 클래스로 정의 / 생성자를 private, package-private 정의)
#### 3) 모든 필드를 `final`로 선언한다. 불변으로 명시적으로 선언하여 설계자의 의도를 명확히 들어낸다.
#### 4) 모든 필드를 `private`로 선언한다. 필드가 참조하는 가변 객체를 클라이언트에서 직접 접근하여 수정하는 일을 막아준다. `public`으로 정의한 경우, 내부 표현을 쉽게 바꾸지 못하기에 `private`을 추천한다.
#### 5) 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. 클래스에 가변 객체를 참조하고 있는 필드가 있다면, 클라이언트에서 그 객체를 변경할 수 있다.
<details><summary>✅ 5번의 해결책 방어적 복사</summary>

1) `List<String>` 가변 객체를 참조하는 필드가 있는 클래스에서, 해당 객체를 그대로 반환하고 있습니다.
```java
public final class ImmutableClass {
private final List<String> items;

public ImmutableClass(List<String> items) {
this.items = items; // 가변 리스트를 직접 참조
}

// 가변 객체를 직접 반환
public List<String> getItems() {
return items; // 외부에서 이 리스트를 수정할 수 있음
}
}
```

2) 이 과정에서 문제가 발생합니다. 외부에서도 해당 객체의 내부 필드에 직접 접근하여 수정이 가능해지기 때문에 불변성을 보장받지 못하기 때문입니다.
```java
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("item1");
ImmutableClass immutable = new ImmutableClass(list);

// items 리스트를 직접 수정할 수 있음
List<String> retrievedList = immutable.getItems();
retrievedList.add("item2"); // 외부에서 리스트 수정 가능
System.out.println(retrievedList); // [item1, item2]

// 원래 리스트도 수정됨
System.out.println(immutable.getItems()); // [item1, item2]
}
```

3) 따라서 `방어적 복사`라는 것을 수행하라고 합니다. 가변 객체의 복사본을 저장하여 반환하는 방식입니다.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

이 경우 Collections.unmodifiableList()로 반환하는 것도 방법이라고 봐도 무방한가요?

Copy link
Copy Markdown
Contributor Author

@yerim123456 yerim123456 Jan 18, 2025

Choose a reason for hiding this comment

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

조건적으로 괜찮은 방법이지만, 방어적 복사와는 조금 결이 다르다고 합니다..!

말씀해주신 Collections.unmodifiableList()는 원본 리스트와 연결되어 있는 상황이기 때문에 원본이 변경된다면 읽기 전용 뷰도 변경되게 됩니다. 즉, 외부에서의 변경은 막아주지만 내부의 변경에 대한 영향을 막아주지 못한다는 단점이 생깁니다.

정리하자면 외부에서는 수정을 못하는가?에서는 비슷한 역할을 하지만, 내부의 변경(진정한 불변성 X)이 확실히 복사본에 영향이 없는가?에서 다르게 된다고 합니다.

방어적 복사의 핵심을 원본과 완전히 독립된 객체를 만든다 라는 부분을 만족하기 위해 새로운 메모리 할당이라고 생각해주시는 것도 좋을 것 같습니다!

질문해 주신 덕분에 그 경우에는 어떻게 동작하는지 생각해볼 수 있었습니다! 좋은 질문 주셔서 감사합니다:) 혹시 제가 잘못 생각하고 있는 부분이 있다면 편하게 알려주시면 감사하겠습니다!

```java
public final class ImmutableClass {
private final List<String> items;

public ImmutableClass(List<String> items) {
this.items = new ArrayList<>(items); // 가변 리스트의 복사본을 저장
}

// 가변 객체의 복사본을 반환
public List<String> getItems() {
return new ArrayList<>(items); // 외부에서 수정할 수 없도록 복사본 반환
}
}

```

4) 이렇게 되면 아까와 달리 외부에서 직접 수정이 가능하지만, 이가 내부 필드에는 영향을 미치지 않게 되어 불변성이 보장됩니다.
```java
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("item1");
ImmutableClass immutable = new ImmutableClass(list);

// items 리스트를 직접 수정할 수 있음
List<String> retrievedList = immutable.getItems();
retrievedList.add("item2"); // 외부에서 리스트 수정 가능
System.out.println(retrievedList); // [item1, item2]

// 원래 리스트는 수정되지 않음
System.out.println(immutable.getItems()); // [item1]
}
```

</details>

---

### 🙌 함수형 프로그래밍을 통한 불변 클래스 제작
제목과 같이 함수형 프로그래밍을 통한 불변 클래스를 제작할 수 있습니다.

<details><summary>✅함수형 프로그래밍과 절차적/명령형 프로그래밍의 차이</summary>

#### 함수형 프로그래밍
- 인스턴스 자신은 수정하지 않고 새로운 인스턴스를 만들어 반환하는 방식을 사용한 것으로,
실제 필드에는 영향이 없어 불변을 유지하는 프로그래밍 패턴
- 불변이라는 점을 강조하기 위해 메서드 이름에도 동사 대신 전치사를 사용

#### 절차적/명령형 프로그래밍
- 인스턴스 자신을 수정하여 자신의 상태를 변화시키는 방식을 사용한 것으로,
- 실제 필드에 영향이 있기에 불변을 유지하기는 어려운 프로그래밍 패턴

</details>

### 🙋‍♀️ 그럼 불변 클래스의 단점은 없나요?
다만 가변에 비해 성능(시간, 공간 문제)이 떨어지는 문제가 생길 수 있습니다.
원하는 객체를 완성하기까지의 단계가 많고, 그 중간 단계에서 만들어진 객체들이 모두 버려진다면 성능 문제가 더 불거지게 됩니다.
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.

저는 사실 “불변 객체를 많이 만들면, 중간 객체들이 전부 버려지고 GC가 자주 일어나 성능이 떨어지지 않을까?”라는 질문이 과연 옳을까 하는 생각이 들었습니다!
자바 GC 구조상 새로 생성된 객체들은 Young영역에 위치하게 됩니다. 불변 객체를 생성했는데 그 객체가 금방 필요 없어지면, Young 영역 안에서 Minor GC를 통해 빠르게 수거될 수 있겠죠? 또한 HTTP는 stateless하게 동작하기 때문에 , 요청마다 생성되는 많은 객체(불변 객체 포함)도 금방 수거될 수 있으므로 실제로는 성능 문제가 없을 때가 많다고 알고있습니다!
이러한 이유로 불변 클래스 == 성능 문제 라고 동일시하는 건 아니지 않나.. 라는 생각을 했는데 예림님의 의견이 궁금합니다😀

(물론 책에서는 성능문제가 생길 수 있다고 언급하기는 합니다.😇)

Copy link
Copy Markdown
Contributor Author

@yerim123456 yerim123456 Jan 18, 2025

Choose a reason for hiding this comment

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

우와 그렇군요!! 말씀 듣고 찾아보니 되레 불변 객체는 GC의 성능 개선에 도움이 되는 장점이다라는 글이 있었습니다. 말씀하신 이유처럼 빠르게 수거되거나, 계속 참조되는 상황에선 Old 영역으로 이동하도록 되며 GC가 불변 객체들은 스킵하며 성능 개선에 도움이 된다는 것이었습니다. (참고 링크)

되레 장점이 되기도 하니 동일시 하는 것은 아니지 않나 라고 생각하신 부분이 맞다고 생각되었습니다. 사실 책에서도 큰 단점으로 언급했기 보단 이런 단점이 있을 수 있다라고 간단히 짚고 넘어갔던 것 같습니다..!

다만 GC 이외의 문제 때문에 성능에 대한 부분이 언급되었을 가능성도 있을 것이라는 생각이 들어서 찾아보니 다음과 같다고 나왔습니다.

1) 메모리&CPU 사용량 고려
불변 객체가 크고 복잡한 경우에도 매우 빈번히 중간 객체가 생성된다면, 메모리 사용량과 CPU 사용량이 증가하기에 부정적 영향이 있을 수 있을 수 있다고 합니다.

2) 객체 생성 비용 고려
객체가 매번 새로 생성되면 객체 생성 비용이라는 게 발생한다고 합니다. 이는 메모리 할당과 초기화가 발생하는 것으로, 객체가 생성되는 순간에만 발생한다고 합니다. 1번과 마찬가지로 매우 빈번히 중간 객체가 생성된다면, 객체 생성 자체에서 비용이 계속 발생해서 성능에 영향을 미칠 수 있다고 합니다.

저는 책을 받아들이기 바빴는데, 늘 질문 던지시고 생각할 거리를 만드신다는 점이 너무 멋지십니다..!! 매번 많이 배워요!! 생각해볼 수 있는 기회 주셔서 감사합니다!☺

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.

너무 정성스러운 답변 감사합니다!!!!!!!!!!!!! 최고!!!


#### 1) 다단계 연산들을 예측하여 기본 기능으로 제공 (private 가변 동반 클래스를 활용)
#### 2) 예측이 어렵다면, public 가변 동반 클래스로 제공

하여 해당 단점을 보완해볼 수 있습니다.

### 🙌 정리해보자면,
1) 클래스는 꼭 필요한 경우가 아니라면 불변이어야 합니다.
2) 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄여야 합니다.
3) 다른 합당한 이유가 없다면 모두 `priavte final`을 사용하는 게 좋습니다.
4) 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 합니다.
Loading