Skip to content

Commit

Permalink
GITBOOK-200: item 50 : 적시에 방어적 복사본을 만들라
Browse files Browse the repository at this point in the history
  • Loading branch information
GoldenPearls authored and gitbook-bot committed Nov 20, 2024
1 parent 4a3e1e9 commit e2614dc
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 0 deletions.
Binary file added developLog/.gitbook/assets/image (74).png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added developLog/.gitbook/assets/image (75).png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions developLog/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
* [item 48 : 스트림 병렬화는 주의해서 적용하라](programming-lanuage/java/effective-java/7/item-48.md)
* [8장 : 메서드](programming-lanuage/java/effective-java/8/README.md)
* [item 49 : 매개변수가 유효한지 검사하라](programming-lanuage/java/effective-java/8/item-49.md)
* [item 50 : 적시에 방어적 복사본을 만들라](programming-lanuage/java/effective-java/8/item-50.md)
* [스터디에서 알아가는 것](programming-lanuage/java/effective-java/undefined.md)
* [모던 자바 인 액션](programming-lanuage/java/modern-java-in-action/README.md)
* [chaper 1. 자바 8, 9, 10, 11 : 무슨 일이 일어나고 있는가?](programming-lanuage/java/modern-java-in-action/chaper-1.-8-9-10-11.md)
Expand Down
169 changes: 169 additions & 0 deletions developLog/programming-lanuage/java/effective-java/8/item-50.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# item 50 : 적시에 방어적 복사본을 만들라

자바는 안전한 언어다. 네이티브 메서드를 사용하지 않으니 메모리 충돌 오류에서 안전하다.

그래도 **클라이언트가 불변식을 깨뜨리려 혈안이 되어있다고 가정하고 방어적으로 프로그래밍해야 한다.**

어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다. 하지만 주의를 기울이지 않으면 허락하는 경우가 생긴다.

## 1. 잘못 사용해서 클래스 내부 수정을 허락하는 경우의 문제점과 해결방법

### **1) 클래스 내부 수정을 본의 아니게 허락하는 경우**

<pre class="language-java"><code class="lang-java">/ 코드 50-1 기간을 표현하는 클래스 - 불변식을 지키지 못했다. (302-305쪽)
public final class Period {
private final Date start;
<strong> private final Date end;
</strong>
/**
* @param start 시작 시각
* @param end 종료 시각. 시작 시각보다 뒤여야 한다.
* @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다.
* @throws NullPointerException start나 end가 null이면 발생한다.
*/
public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(
start + "가 " + end + "보다 늦다.");
this.start = start;
this.end = end;
}

public Date start() {
return start;
}
public Date end() {
return end;
}

public String toString() {
return start + " - " + end;
}
</code></pre>

final로 선언을했고, setter도 없으니 불변처럼 보인다. 그래서 시작 시각이 종료 시각보다 늦으면 안된다는 불변식이 지켜질 것 같다. `private final Date`로 선언된 필드이지만 문제는 `Date` 클래스의 가변성 때문이다. `Date` 객체 자체가 가변적이기 때문에 내부 값이 변경될 수 있는 문제가 있다.&#x20;

> &#x20;즉, 필드 자체가 불변인 것은 맞지만 그 내부 필드들은 불변이 아니다.
```java
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p 내부 수정
```

자바 8이후로는 `Date`대신 불변인 `Instant`를 사용하면 된다. (혹은 `LocalDateTime` 또는 `ZonedDateTime`). 문제는 이미 오래된 API에 `Date`가 많이 사용되어 있다.

외부 공격으로 부터 내부를 보호하려면 **생성자에서 받은 기본 매개변수 각각을 방어적으로 복사해야 한다.**

### **2) 문제점: 가변 객체로 인해 의도와 다르게 값이 변경될 수 있음**

<figure><img src="../../../../.gitbook/assets/image (74).png" alt=""><figcaption></figcaption></figure>

`Date` 클래스는 가변이기 때문에, `start()` 또는 `end()` 메서드에서 반환된 객체를 통해 내부 값을 변경할 수 있다. 예를 들어 `Period` 객체의 `end` 필드를 통해 반환된 `Date` 객체를 수정하면, `Period` 클래스 내부의 `end` 값도 변경된다. 이로 인해 의도하지 않은 내부 상태 변경이 발생할 수 있다.

자바 8 이후로는 이러한 문제를 간단히 해결하기 위해 `LocalDateTime` 또는 `ZonedDateTime`과 같은 **불변(immutable) 날짜 객체**를 사용할 수 있다. 이러한 클래스들은 변경 불가능한 특성을 가지므로, 내부 상태가 외부에서 수정될 가능성이 없다.

### **3) 해결방법 1 : 방어적 복사 사용**

외부 공격으로 부터 내부를 보호하려면 **생성자에서 받은 기본 매개변수 각각을 방어적으로 복사해야 한다.** 그런 다음 Period 인스턴스 안에서는 원본이 아닌 복사본을 사용한다.

```java
public Period(Date start, Date end, int version) {
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + "" + end + "보다 늦다.");
}

if (version == 1) {
this.start = start;
this.end = end;
} else {
// 방어적 복사본 만들기
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
}
}
```

버전이 1일 경우 원래 객체를 사용하지만, 그렇지 않으면 **방어적 복사본**을 만들어 필드에 저장한다. 이로 인해 원래 객체를 외부에서 변경하더라도 `Period` 클래스의 내부 필드에는 영향을 주지 않게 된다.

새로 작성한 생성자를 사용하면 방어할 수 있다.

**매개변수의 유효성을 검사(아이템49)하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자.** 반드시 이렇게 작성해야 한다.&#x20;

> 멀티스레딩 환경이라면 원본 객체의 `유효성`을 검사한 후 **검사본을 만드는 찰나의 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문**이다.
**매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안된다.** `Date``final`이 아니므로 `clone``Date`가 정의한게 아닐 수도 있다. 즉, `clone`이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있다. 예를 들어 이 하위 클래스는 `start``end` 필드의 참조를 `private` 정적 리스트에 담아뒀다가 공격자에게 이 리스트에게 접근하는 길을 열어줄 수도 있다.

```java
// Period 인스턴스를 향한 두 번째 공격
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78);
```

### **4) 해결방법 2 :** 픧드의 방어적 복사본을 반환한다.

접근자 메서드에서도 마찬가지로 방어적 복사를 사용하면 불변성을 유지할 수 있다. 두 번째 공격을 막으려면 접근자가 **가변 필드의 방어적 복사본을 반환하면 된다.**

```java
// 수정한 접근자 - 필드의 방어적 복사본 반환
public Date start() {
return new Date(start.getTime());
}

public Date end() {
return new Date(end.getTime());
}
```

접근자 메서드가 **방어적 복사본**을 반환하면, 반환된 객체를 수정하더라도 `Period` 클래스의 내부 상태에는 영향을 미치지 않는다. 필드 자체를 직접 반환하면, `final`로 선언했더라도 내부 메서드를 통해 해당 객체가 변경될 가능성이 있다는 점을 항상 인지해야 한다.

이제 Period 자신 말고는 가변 필드에 접근할 방법이 없으니 모든 필드가 캡슐화되었다.

> 생성자와 달리 접근자 메서드에서는 방어적 복사에 `clone`을 사용해도 된다. `Period`가 가지고 있는 `Date` 객체는 `java.util.Date`임이 확실하기 때문이다.
메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지를 생각해야 한다. 확신할 수 없다면 복사본을 만들어 저장해야 한다.

> 길이가 1이상인 배열은 무조건 가변임을 잊지말자.
방어적 복사에는 성능 저하가 따르고 항상 쓸수 있는 것도 아니다. 방어적 복사를 생략할 때는 해당 매개변수가 반환값을 수정하지 말아야함을 명확히 문서화해야 한다.

<figure><img src="../../../../.gitbook/assets/image (75).png" alt=""><figcaption></figcaption></figure>

### **5) 방어적 복사를 위한 `clone()` 사용 고려**

방어적 복사를 위해 생성자에서 `clone()` 메서드를 사용하는 것은 위험할 수 있다. 악의적인 사용자가 `clone()` 메서드를 재정의한 하위 타입을 생성하고, 이를 이용해 방어적 복사를 우회할 가능성이 있기 때문이다. 따라서 **생성자에서 `clone()` 사용은 권장되지 않는다**.

반면, 접근자 메서드에서 `clone()`을 사용하는 것은 괜찮다. `new Date()`를 사용해 방어적 복사본을 반환함으로써, 해당 객체가 안전한 `Date` 타입임을 보장할 수 있기 때문이다. 하지만 여전히 `clone()`은 예측하기 어려운 상황을 야기할 수 있으므로, 생성자나 정적 팩토리 메서드를 통한 방어적 복사가 더 권장된다.

### 6) 방어적 복사를 생략해도 되는 상황

1. 클래스와 클라이언트를 상호 신뢰할 수 있을 때
2. 불변식이 깨지더라도 그 영향이 오직 클라이언트로 국한될 때
3. 래퍼 클래스 패턴은 클라이언트는 래퍼에 넘긴 객체에 여전히 직접 접근할 수 있지만 그 영향을 오직 클라이언트 자신만 받게 된다.

## **2. 추가적으로 고려해야 할 사항**

1. **불변 객체 사용**: 가변 객체 대신 불변 객체(`LocalDate`, `LocalDateTime`) 사용을 우선 고려하자. 불변 객체는 이러한 문제를 원천적으로 방지할 수 있다.
2. **객체의 깊은 복사**: 방어적 복사 시, 필드가 객체 참조를 포함하는 경우 **깊은 복사**가 필요할 수 있다. 얕은 복사는 필드 내부의 참조를 그대로 복사하므로, 여전히 원본 객체가 변경될 위험이 있다.
3. **인터페이스 설계 시 가변성 고려**: API 설계 시 외부에서 객체의 내부 상태를 변경할 수 없도록 설계하는 것이 중요하다. 특히 외부에 노출되는 데이터는 불변성을 보장하거나 복사본을 제공하는 방식을 사용하는 것이 좋다.
4. **통제권 이전 명시**: 때로는 메서드나 생성자의 매개변수로 넘기는 행위가 그 객체의 통제권을 명백히 이전함을 뜻하기도 한다. 이 경우, 클라이언트가 해당 객체를 더 이상 직접 수정하지 않는다고 약속해야 하며, 그 사실을 문서화하는 것이 중요하다.
5. **방어적 복사 생략 조건**: 호출자가 컴포넌트 내부를 수정하지 않으리라 확신하거나, 불변식이 깨지더라도 그 영향이 호출한 클라이언트로 국한될 경우 방어적 복사를 생략할 수 있다. 그러나 이러한 경우에도 클라이언트가 해당 매개변수나 반환값을 수정하지 않도록 명확히 문서화해야 한다.

이러한 원칙을 지킴으로써 객체의 불변성을 보장하고, 예기치 않은 변경으로부터 시스템을 안전하게 보호할 수 있다.

## 📚 핵심 정리

* 객체를 제공받을 때는 받아들인 객체가 변경되어도 정상적으로 작동할지 생각해보고 **정상적으로 작동하지 않을 것 같다면, 잠재적으로 변경되지 않도록 방어적 복사를 하자.**
* 이러한 객체가 Set 혹은 Map에 들어간 뒤에 필드 값이 변경된다면, 불변식이 깨질 수도 있다.
* 되도록 기본 지원 불변 객체를 활용하자. (LocalDateTime)
* 방어적 복사를 하기에는 복사 비용이 너무 크다면, 문서에서 해당 요소를 수정했을 때의 책임은 클라이언트에게 있음을 명시해놓자.



> 참고 및 출처
>
> * [https://jake-seo-dev.tistory.com/517](https://jake-seo-dev.tistory.com/517)
> * [https://velog.io/@injoon2019/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EC%95%84%EC%9D%B4%ED%85%9C-50.-%EC%A0%81%EC%8B%9C%EC%97%90-%EB%B0%A9%EC%96%B4%EC%A0%81-%EB%B3%B5%EC%82%AC%EB%B3%B8%EC%9D%84-%EB%A7%8C%EB%93%A4%EB%9D%BC](https://velog.io/@injoon2019/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EC%95%84%EC%9D%B4%ED%85%9C-50.-%EC%A0%81%EC%8B%9C%EC%97%90-%EB%B0%A9%EC%96%B4%EC%A0%81-%EB%B3%B5%EC%82%AC%EB%B3%B8%EC%9D%84-%EB%A7%8C%EB%93%A4%EB%9D%BC)

0 comments on commit e2614dc

Please sign in to comment.