-
Notifications
You must be signed in to change notification settings - Fork 93
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[#90][B팀] 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라
[#90][B팀] 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라
- Loading branch information
Showing
1 changed file
with
189 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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,189 @@ | ||
<img width="1576" alt="스크린샷 2021-04-07 오후 8 52 01" src="https://user-images.githubusercontent.com/37873745/113862264-250e5a00-97e3-11eb-8092-3d88f5dbdcfe.png"> | ||
|
||
# 아이템 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라 | ||
|
||
Serializable을 구현하기로 결정한 순간, 언어의 생성자 이외의 방법으로도 인스턴스를 생성할 수 있게 된다. | ||
|
||
|
||
- 이전 아이템에서도 계속 언급되지만, 버그 및 보안 문제가 일어날 가능성이 커진다. | ||
|
||
직렬화 프록시 패턴 (Serialization Proxy Pattern)을 이용하면 이러한 위험을 크게 줄여줄 수 있다. | ||
|
||
- 직렬화 프록시는 일반적으로 이전 아이템에서 나온 `readObject` 의 방어적 복사보다 강력하다. | ||
|
||
## 직렬화 프록시 패턴 | ||
|
||
```java | ||
class Period implements Serializable { | ||
// 불변 가능 | ||
private final Date start; | ||
private final Date end; | ||
|
||
public Period(Date start, Date end) { | ||
this.start = start; | ||
this.end = end; | ||
} | ||
|
||
private static class SerializationProxy implements Serializable { | ||
private static final long serialVersionUID = 2123123123; | ||
private final Date start; | ||
private final Date end; | ||
|
||
public SerializationProxy(Period p) { | ||
this.start = p.start; | ||
this.end = p.end; | ||
} | ||
|
||
// Deserialize -> Object 생성 | ||
private Object readResolve() { | ||
return new Period(start, end); | ||
} | ||
} | ||
|
||
// Serialize -> 프록시 인스턴스 반환 | ||
// 결코 바깥 클래스의 직렬화된 인스턴스를 생성해낼 수 없다 | ||
private Object writeReplace() { | ||
return new SerializationProxy(this); | ||
} | ||
|
||
// Period 자체의 역직렬화를 방지 -> 역직렬화 시도시, 에러 반환 | ||
private void readObject(ObjectInputStream stream) throws InvalidObjectException { | ||
throw new InvalidObjectException("프록시가 필요해요."); | ||
} | ||
} | ||
``` | ||
|
||
<br> | ||
|
||
```java | ||
// Period | ||
private Object writeReplace() { | ||
return new SerializationProxy(this); | ||
} | ||
``` | ||
|
||
- Serialize -> 프록시 인스턴스 반환 | ||
- 결코 바깥 클래스의 직렬화된 인스턴스를 생성해낼 수 없다 | ||
|
||
<br> | ||
|
||
|
||
```java | ||
// Period | ||
private void readObject(ObjectInputStream stream) throws InvalidObjectException { | ||
throw new InvalidObjectException("프록시가 필요해요."); | ||
} | ||
``` | ||
|
||
- Period 자체의 역직렬화를 방지 -> 역직렬화 시도시, 에러 반환 | ||
|
||
<br> | ||
|
||
|
||
```java | ||
// SerializationProxy | ||
private Object readResolve() { | ||
return new Period(start, end); | ||
} | ||
``` | ||
|
||
- Deserialize -> Object 생성 | ||
- 공개된 API 만을 사용해 바깥 클래스의 인스턴스를 생성 → 생성자, 정적 팩터리 등을 통해 인스턴스를 생성 및 반환이 가능하다! | ||
- 즉, 아이템 88에서 나오는 것처럼 불변식을 검사하는 수단이 필요가 없다! | ||
- 생성자, 정적 팩터리 메서드에서 불변식 검사를 잘 해주고 있다면. | ||
|
||
|
||
<br> | ||
|
||
|
||
--- | ||
|
||
## 역직렬화한 인스턴스와 원래의 직렬화된 인스턴스의 클래스가 달라도 정상 작동한다 | ||
|
||
말이 너무 어렵다.... | ||
|
||
→ 쉽게 말하면, | ||
|
||
- A 클래스의 인스턴스를 직렬화했다. | ||
- 그 데이터를 가지고 역직렬화를 수행해 B 클래스의 인스턴스를 만들었다. (역직렬화 했다) | ||
- 근데 정상작동한다 ! | ||
|
||
처음엔, 이해도 잘 안되고... 이게 왜...? 라는 생각이 들었다. | ||
<br> | ||
<br> | ||
|
||
예를 보자. | ||
|
||
### EnumSet (Item 36) | ||
|
||
`EnumSet`은 생성자 없이 정적 팩터리들만 제공 | ||
|
||
- 단순히 생각하면 `EnumSet` 인스턴스를 반환하는 것 같지만, 열거 타입의 크기에 따라 다르다. | ||
- 열거 타입의 원소의 개수가 | ||
- 64개 이하면, `RegularEnumSet` 을 반환 | ||
- 그보다 크면, `JumboEnumSet` 을 반환 | ||
<br> | ||
|
||
![1](https://user-images.githubusercontent.com/37873745/113861202-e6c46b00-97e1-11eb-9806-20c405d4d4a1.png) | ||
|
||
<br> | ||
|
||
### 시나리오 | ||
|
||
- 63개의 열거타입을 가진 EnumSet이 있다. | ||
- 이를 직렬화하자. | ||
- 그리고 원소 5개를 추가해 역직렬화하자. | ||
|
||
<br> | ||
당연히 처음엔 `RegularEnumSet` 인스턴스 였다가, 나중엔 `JumboEnumSet` 로 하는 것이 더 효율적이고 좋을 것이다. | ||
|
||
- 직렬화 프록시를 이용하면, 원하는 방향대로 사용이 가능하다. | ||
- 아래는 `EnumSet` 의 실제 코드 - 직렬화 프록시 패턴을 이용 | ||
|
||
```java | ||
private static class SerializationProxy <E extends Enum<E>> implements java.io.Serializable | ||
{ | ||
// EnumSet의 원소 타입 | ||
private final Class<E> elementType; | ||
|
||
// EnumSet 내부 원소 | ||
private final Enum<?>[] elements; | ||
|
||
SerializationProxy(EnumSet<E> set) { | ||
elementType = set.elementType; | ||
elements = set.toArray(ZERO_LENGTH_ENUM_ARRAY); | ||
} | ||
|
||
// 새롭게 원소의 크기에 맞는 EnumSet 생성 | ||
@SuppressWarnings("unchecked") | ||
private Object readResolve() { | ||
EnumSet<E> result = EnumSet.noneOf(elementType); | ||
for (Enum<?> e : elements) | ||
result.add((E)e); | ||
return result; | ||
} | ||
|
||
private static final long serialVersionUID = 362491234563181265L; | ||
} | ||
|
||
Object writeReplace() { | ||
return new SerializationProxy<>(this); | ||
} | ||
|
||
private void readObject(java.io.ObjectInputStream stream) | ||
throws java.io.InvalidObjectException { | ||
throw new java.io.InvalidObjectException("Proxy required"); | ||
} | ||
``` | ||
<br> | ||
<br> | ||
|
||
## 직렬화 프록시 패턴의 한계 | ||
|
||
1. 클라이언트 멋대로 확장할 수 있는 클래스에는 적용할 수 없다. | ||
|
||
2. 객체 그래프에 순환이 있는 클래스에는 적용할 수 없다. | ||
|
||
3. (책엔 한계라고 적혀있지는 않지만) 속도가 느리다 | ||
|
||
- 책에서 말하길, `Period` 예제의 경우 방어적 복사를 사용하는 것보다 14% 느려졌다고 한다. |