Search
Duplicate

11.직렬화

목차

85. 자바 직렬화의 대안을 찾으라.

직렬화는 위험하다.

자바의 역직렬화는 명백하고 현존하는 위험이다. 이 기술은 지금도 애플리케이션에서 직접 혹은, 자바 하부 시스템(RMI(Remote Method Invocation), JMX(Java Management Extension), JMS(Java Messaging System) 같은) 을 통해 간접적으로 쓰이고 있기 때문이다. 신뢰할 수 없는 스트림을 역직렬화하면 원격 코드 실행(remote code execution, RCE), 서비스 거부(denial-of-service, DoS)등의 공격으로 이어질 수 있다. 잘못한 게 없는 애플리케이션이라도 이런 공격에 취약해질 수 있다. - CERT 조정 센터의 기술관리자 로버트 시커드(Robert Seacord)
자바에선 ObjectInputStream의 readObject 메서드를 통해 객체 그래프가 역직렬화 되는데, 문제는 바이트 스트림을 역직렬화 하는 과정에서 그 타입들 안의 모든 코드를 수행할 수 있다는 것인데, 이는 코드 전체가 공격범위에 들어간다는 것이다.
모든 직렬화 가능 클래스들을 공격에 대비하도록 작성해도 애플리케이션을 취약해질 수 있다.
다음은 바우터르 쿠카르츠(Wouter Coekaerts)라는 기술자가 만든 역직렬화 폭탄(deserialization bomb) 코드이다.
static byte[] bomb() { Set<Object> root = new HashSet<>(); Set<Object> s1 = root; Set<Object> s2 = new HashSet<>(); for (int i = 0; i < 100; i++) { Set<Object> t1 = new HashSet<>(); Set<Object> t2 = new HashSet<>(); t1.add("foo"); s1.add(t1); s1.add(t2); s2.add(t1); s2.add(t2); s1 = t1; s2 = t2; } return serialize(root); //직렬화 메서드 코드(serialize)는 생략 }
Java
복사
역직렬화 폭탄(deserialization bomb)
이 bomb() 메서드의 root 객체 그래프는 201개의 HashSet 인스턴스로 구성되어 각각 3개 이하의 객체 참조를 가진다. 전체 크기는 크지 않지만, 역직렬화는 영원히 끝나지 않을 것이다. 그 이유는 각각의 원소들의 해시코드를 계산해야 하는 데 걸리는 시간 때문인데, 깊이가 100단계이고 각각의 원소가 HashSet인 컬렉션이 되는데 이 HashSet을 역직렬화 하기 위해서는 hashCode 메서드를 21002^{100}번 넘게 호출해야 한다. 그럼 이러한 문제들은 어떻게 해야 할까?

직렬화를 사용하지 말자.

신뢰불가한 바이트 스트림을 역직렬화 하는 작업은 스스로를 공격에 노출하는 행위로 애초에 역직렬화나 직렬화를 사용하지 않는게 가장 안전할 수 밖에 없다.
요즘 객체와 바이트 시퀀스를 변환해주는 다른 기술들이 많은데, 직렬화 시스템 혹은 크로스-플랫폼 구조화된 데이터 표현(cross-platform structured-data representation)이라 한다.
이러한 방식은 임의 객체 그래프를 직렬화/역직렬화 하는 대신 기본 타입 몇 개와 배열 타입만 지원한다. 하지만, 이정도의 추상화로도 충분히 사용할 수 있고 직렬화의 문제도 회피할 수 있다. 이런 데이터 표현의 선두주자로는 JSON과 프로토콜 버퍼(Protocol Buffers, protobuf)가 있다.

JSON

더클라스 크록퍼드(Douglas Crockford)가 브라우저와 서버의 통신용으로 설계
자바 스크립트용으로 만들어졌다.
텍스트 기반이라 사람이 읽을 수 있다.
오직 데이터를 표현하기 위해 사용된다.

프로토콜 버퍼

구글이 서버 사이에 데이터를 교환하고 저장하기 위해 설계.
C++ 용으로 만들어 졌다.
이진 표현이라 효율이 높다.
문서를 위한 스키마(타입)를 제공하고 올바로 쓰도록 강요한다.
그럼 자바 직렬화를 배제하기 힘든 레거시 프로젝트에서는 어떻게 해야할까?

신뢰할 수 없는 데이터를 역직렬화 하지 않는것.

그렇다면 신뢰도조차 확신할 수 없는 경우라면 어떻게 해야할까?

객체 역직렬화 필터링(java.io.ObjectInputFilter)를 사용하자.

자바 9에 추가된 객체 역직렬화 필터링은 데이터 스트림이 역직렬화되기 전에 필터를 설치하는 기능으로 클래스 레벨에서 특정 클래스를 허용 혹은 거부할 수 있다.
이 경우 전부 거절하는 환경에서 안전하다고 보장되는 화이트리스트만 작성하는 방식을 추천한다.
화이트리스트 자동 생성 도구는 SWAT(Serial Witelist Application Trainer)같은 도구가 있다.

86. Serializable을 구현할지는 신중히 결정하라.

직렬화는 클래스에서 Serializable 만 구현하게 implements로 덧붙히면 된다.
그래서 얼핏 쉬워보이지만, 아무생각없이 적용했다간 골치아픈 상황이 올 수 있다.
어째서 그런지 하나씩 알아보자.

릴리즈한 뒤에는 수정하기 어렵다.

Serializable을 구현하면 직렬화된 바이트 스트림 인코딩(직렬화 형태)도 하나의 공개 API가 되버린다.
private, package-private 인스턴스 필드도 API로 공개되버린다.
캡슐화도 깨지고, 아이템 15에서 말했던 필드의 접근을 최소화하라는 내용도 깨진다.
클래스가 변경되면 직렬화 형태가 변경되어 역직렬화간의 실패를 겪을 수 있다.

클래스 개선을 방해한다.

모든 직렬화된 클래스는 스트림 고유 식별자, 직렬 버전 UID(Serial version UID)를 부여 받는데 static final long serialVersionUID 필드로, 이 번호를 명시하지 않을 경우 시스템에서 런타임에 암호 해시 함수 (SHA-1) 을 적용해 자동으로 클래스에 생성해 넣는다. 이 UID는 클래스 이름, 구현 인터페이스등 대부분의 클래스 멤버들이 고려된다.
나중에 편의 메서드를 추가하거나 클래스가 수정된다면 UID값도 변경되고, 이는 호환성이 깨진다는것을 의미하고 런타임에 InvalidClassException을 던지게 된다.

버그와 보안 구멍이 생길 위험이 높아진다.

객체는 생성자를 사용해 만드는 것이 기본인데, 직렬화는 언어의 기본 메커니즘을 우회하는 생성 기법이다.
기존의 불변식이 깨질 수 있고 허가되지 않는 접근제어자도 다 무시하고 쉽게 접근 및 노출될 수 있다.

신버전을 릴리즈할 때 테스트할 것이 늘어난다.

직렬화 가능 클래스가 수정되면 신버전과 구버전간에 직렬화/역직렬화가 가능한지 검사해야 한다.
테스트의 양이 직렬화 가능 클래스와 릴리즈 횟수에 비례해 증가하게 된다.

상속용으로 설계된 클래스는 대부분 Serializable을 구현하지 말자.

이는 인터페이스도 마찮가지이다. 이 규칙을 따르지 않으면 해당 클래스나 인터페이스의 구현체에게 큰 부담을 지우게 된다. 정말 Serializable을 구현한 클래스만 지원하는 프레임워크를 사용하는 경우가 아니라면 이 규칙을 지키도록 하자.
하지만, 그럼에도 불구하고 우리가 작성할 클래스의 인스턴스가 직렬화, 확장이 모두 가능해야 한다면 다음 사항을 주의해야 한다.
1.
인스턴스 필드 중 불변식을 보장해야 하는 필드가 있다면 하위 클래스에서 finalize 메서드를 재정의하지 못하게 해야 한다. (상위 객체에서 finalize를 final로 선언하면 된다.)
⇒ finalizer 공격을 막기 위해서다.
2.
인스턴스 필드 중 기본값(0, false, null, ...)으로 초기화되면 안되는 불변식이 있는 클래스는 readObjectNoData 메서드를 추가해야 한다.
private void readObjectNoData() throws InvalidObjectException { throw new InvalidObjectException("스트림 데이터가 필요합니다."); }
Java
복사
자바 4에 추가된 readObjectNoData 메서드는 직렬화 가능 클래스에 직렬화 가능 상위 클래스를 추가하는 경우를 위한 메서드다.

내부 클래스는 직렬화를 구현하지 말아야 한다.

내부 클래스에선 바깥 인스턴스의 참조와 유효 범위안의 지역변수 값들을 저장하기 위해 컴파일러가 생성한 필드들이 자동으로 추가된다.
내부 클래스의 대한 기본 직렬화 형태는 분명하지 않기에 직렬화를 구현하지 말아야 한다.
정적 멤버 클래스(static class innerClass{...})는 Serialziable을 구현해도 상관없다.

87. 커스텀 직렬화 형태를 고려해보라

개요

일반적인 직렬화 방식은 클래스에 Serializable을 구현하면 된다. 몹시 간편한데, 이런 방식이 아닌 커스텀 직렬화 형태를 고민해봐야 할 때는 언제일까?

기본 직렬화를 사용해도 되는 경우

객체의 물리적 표현과 논리적 내용이 같은 경우
public class Name implements Serializable { private final String lastName; private final String firstName; private final String middleName; ... }
Java
복사
기본 직렬화를 사용하기 적절한 구조
: 이름, 성, 중간이름이라는 3개의 문자열로 구성된 이름 클래스는 논리적 구성요소도 정확히 반영되어있다.
위와 같은 경우여도 불변식 보장과 보안을 위해 readObject 메서드를 제공해야 할 때가 많다.
위 Name 클래스는 lastName과 firstName이 not null임을 보장해야 한다.

기본 직렬화를 사용하기 적합하지 않은 경우

객체의 물리적 표현과 논리적 표현의 차이가 큰 경우
public final class StringList implements Serializable { private int size = 0; private Entry head = null; private static class Entry implements Serializable { String data; Entry next; Entry previous; } }
Java
복사
기본 직렬화가 적합하지 않은 구조
이 문자열 목록 클래스에 기본 직렬화를 사용하면 각 노드의 연결정보를 포함한 모든 엔트리 정보를 기록하기에 적절하지 않다.

기본 직렬화를 적합하지 않게 사용할 경우 문제점

: 위와 같이 기본 직렬화 형태를 사용하기 적합하지 않은 경우( 객체의 물리적 표현과 논리적 표현의 차이가 큰 경우)에도 기본 직렬화를 사용하면 다음과 같은 문제가 생긴다.
1.
공개 API가 현재의 내부 표현 방식에 영구히 묶인다.
: private 접근자를 가진 중첩 클래스인 Entry가 공개 API가 되버리는데, 다음 릴리즈에서 연결리스트가 아니라 다른 자료구조로 변경되더라도 StringList 클래스는 연결리스트로 표현된 입력도 처리할 수 있어야 한다. 즉, 실제로 변경되어 사용하지 않더라도 해당 기능을 지원해야 한다.
2.
너무 많은 공간을 차지할 수 있다.
: 엔트리와 연결정보는 내부 구현에 해당하기에 직렬화에 포함할 필요가 없는데, 포함되버리게 되고, 이렇게 직렬화의 크기가 커지면 필연적으로 속도(디스크 저장, 네트워크 전송)가 느려진다.
3.
시간이 너무 많이 걸릴 수 있다.
: 직렬화 로직에는 객체 그래프의 위상정보가 포함되지 않기에 그래프를 직접 순회하는데, 이 경우 시간이 오래 걸릴 수 있다.
4.
스택 오버플로를 일으킬 수 있다.
: 기본적인 형태의 직렬화 과정은 객체 그래프를 재귀 순회하는데, 이 과정이 스택오버플로(StackOverflowError)를 일으키기 쉽다. 게다가 스택 오버플로를 일으키는 최소한의 크기가 실행시마다, 플랫폼마다 달라지게 되니 일관성도 없어 문제다.

커스텀 직렬화 구조 사용

그럼 이런 기본직렬화 구조를 사용하기 힘들 경우 커스텀 직렬화를 어떻게 사용해야 할까?
이는 단순하게 모든 객체 그래프를 탐색하는게 아닌 논리적인 구성만 담도록 하는 것이다.
위에서 작성했던 StringList 클래스를 활용해보자.
public final class StringList implements Serializable { private transient int size = 0; private transient Entry head = null; private static class Entry { String data; Entry next; Entry previous; } private void writeObject(ObjectOutputStream s) throws IOException { s.defaultWriteObject(); s.writeInt(size); for(Entry e = head; e != null; e = e.next){ s.writeObject(e.data); } } private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); int numElements = s.readInt(); for(int i = 0; i< numElements; i++){ add((String) s.readObject()); } } }
Java
복사
커스텀 직렬화 구조 사용 코드
transient 키워드는 필드가 직렬화 형태에 포함되지 않는다는 표시이다.
⇒ 클래스의 필드가 모두 transient라도 writeObject, readObject메서드는 defaultXXXXObject 메서드를 호출한다. 직렬화 명세에서는 이 작업을 무조건 하라고 명시한다. 이렇게 해야 향후 릴리즈에서 transient가 아닌 필드가 추가될 경우 상호 호환되기 때문이다.
writeObject 메서드는 private이지만, 직렬화 형태에 포함되어 공개 API라 할 수 있고, 공개 API는 모두 문서화해야 하기 때문이다.

기타 주의사항

어떤 직렬화 형태를 사용하던 주의해야할 부분들도 있다.
transient 한정자를 붙혀도 되는 인스턴트 필드에는 모두 붙히도록 하자.
캐시된 해시 값처럼 다른 필드에서 유도되는 필드도 포함된다.
JVM 실행마다 달라지는 필드(ex: long)도 포함된다.
객체의 논리적 상태와 무관한 필드라고 확신할 때만 transient한정자를 생략해야 한다.
기본 직렬화를 사용하며transient 필드는 기본값으로 초기화된다.
객체 참조: null
숫자 기본 타입: 0
boolean : false
이런 기본값을 원치 않을 경우 readObject에서 defaultReadObject를 호출한 다음 해당 필드를 원하는 값으로 복원하면 된다.
객체의 전체 상태를 읽는 메서드에 적용해야 하는 동기화 메커니즘을 직렬화에도 적용해야 한다.
private synchronized void writeObject(ObjectOutputStream s) throws IOException { s.defaultWriteObject(); }
Java
복사
기본 직렬화를 사용하는 동기화된 클래스를 위한 writeObject
writeObject메서드 안에서 동기화가 필요한 경우 클래스의 다른 부분에서 사용하는 락 순서를 동일하게 따라야 하며, 그렇지 않을 경우 자원 순서 교착상태(resource-ordering deadlock)에 빠질 수 있다.
직렬화 가능 클래스에 모두 직렬 버전 UID를 명시하자.
잠재적인 호환성 문제가 사라진다.
성능도 조금이지만 향상된다.
private static final long serialVersionUID = <무작위로 고른 long>;
Java
복사
구버전으로 직렬화된 인스턴스들과 호환성을 끊으려는 경우가 아니라면 직렬버전 UID를 수정하지 말자.

88. readObject 메서드는 방어적으로 작성하라.

개요

readObject 메서드는 실질적으로는 또 다른 종류의 public 생성자라 할 수 있다.
그렇기에 readObject 메서드에도 생성자에 작성하는 수준의 주의(유효성 검사, 방어적 복사, ...)를 고려하지 않는다면, 클래스의 불변식을 깨트리게 될 수 있다. 그렇기에 readObject 메서드를 생성자라 생각하고 방어적으로 작성해야 한다.

일종의 생성자인 readObject

readObject 메서드는 매개변수로 바이트 스트림을 받는 생성자라 할 수 있다.
그렇기에, 불변식을 깨트리기 위해 임의로 생성항 바이트 스트림을 건넬 경우 정상적인 생성자로는 생성할 수 없는 객체를 생성해낼 수 있다.

가변 공격

readObject에 유효성 검사 로직을 넣더라도 문제는 존재한다.
정상적인 바이트 스트림 끝에 private 객체필드의 참조를 추가하면, 가변 객체 인스턴스를 만들어 낼 수 있다. 공격자는 ObjectInputStream에서 인스턴스 정보를 읽어 스트림 끝에 추가된 객체 참조를 읽어서 객체의 내부 정보를 얻을 수 있고 수정도 가능하게 됨으로써 불변성을 깨트릴 수 있다.
public class MutablePeriod { public final Period period; public final Date start; public final Date end; public MutablePeriod() { try(ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bos)) { out.writeObject(new Period(new Date(), new Date()); byte[] ref = {0x71, 0, 0x7e, 0, 5}; //참조 #5 bos.write(ref); //시작(strat)필드 ref[4] = 4; //참조 #4 bos.write(ref); //종료(end) 필드 ObjectInputStream in = new ObjectInputStream( new ByteArrayInputStream(bos.toByteArray())); period = (Period) in.readObjedcdt(); start = (Date) in.readObject(); end = (Date) in.readObject(); } catch(IOException | ClassNotFoundException e) { throw new AssertionError(e); } } }
Java
복사
가변 공격의 예
public static void main(String[] args) { MutablePeriod mp = new MutablePeriod(); Period p = mp.period; Date pEnd = mp.end; pEnd.setYear(78); pEnd.setYear(69); }
Java
복사

가변 공격 방어하기

private void readObject(ObjectOutputStream s) throws IOException, ClassNotFoundException { s.defaultWriteObject(); //가변 요소 방어적 복사 start = new Date(start.getTime()); end = new Date(end.getTime()); //유효성 검사... //... }
Java
복사

가변 공격 방어시 주의사항

방어적 복사가 유효성 검사보다 앞서 수행되며 clone 메서드는 사용하지 않는다.
final 필드는 방어적 복사가 불가능하기에 주의해야 한다.
readObject를 사용하려면 final을 제거해야한다.
trade off를 생각해봤을 때 제거하는게 더 유리하다.

기본 readObject를 써도 되는 경우

그렇다고 항상 커스텀해야하는것은 아니다. 기본적인 readObject를 사용해도 좋은지 판단하는 경우는 다음과 같다.
transient 필드를 제외하고 모든 필드의 값을 매개변수로 받아 유효성 검사 없이 필드에 대입하는 public 생성자를 만들어도 괜찮은 경우

readObject 메서드도 재정의 가능 메서드를 호출해서는 안된다.

직, 간접접 어떤 방식이든 재정의 가능 메서드를 호출해서는 이전에 소개했던 내용(아이템 19)처럼 하위 클래스가 역직렬화되기 전 하위 클래스에서 재정의된 메서드가 실행되며 오작동이 생길 수 있다.

89. 인스턴스 수를 통제해야 한다면 readResolve 보단 열거타입을 사용하라.

싱글턴 패턴에 직렬화를 넣는다면?

public class Singletone { public static final Singletone INSTANCE = new Singletone(); private Singletone(){...} //... }
Java
복사
위와 같은 싱글턴 패턴의 클래스가 있다고 할 때 여기에 implements Serializable 을 추가하면 어떻게될까? 이제 더 이상 Singletone 클래스는 싱글턴 클래스가 아니게 된다.
readObject를 제공하고 내용을 어떻게 작성하든 이 클래스가 초기화될 때 만들어진 인스턴스와는 다른 참조주소를 가진 인스턴스를 반환하게 될 것이다. 이는 우리가 의도했던 싱글턴과는 달라지게 된다.

readResolve

readResolve 기능을 이용해 readObject가 만들어낸 인스턴스를 대체할 수 있다.
readResolve 메서드를 적절히 정의해뒀다면, 역직렬화 된 후 새로 생성된 객체를 인수로 이 메서드가 호출되고, 이 메서드가 반환한 객체 참조가 새로 생성된 객체를 대신해 반환하게 되며, 새로 생성된 객체는 참조되는 곳이 없어 가비지 컬렉션 대상이 되어 사라지게 된다.
private Object readResolve() { return INSTANCE; }
Java
복사
이렇게 되면 직렬화 형태가 어떠한 데이터를 가질 필요도 없기에 모든 인스턴스 필드는 transient 한정자를 붙혀야 한다. 그렇지 않을 경우 공격자에게 공격할 구실을 만들어줄 수 있다.

도둑 클래스를 이용한 공격

싱글턴 패턴 클래스에 인스턴스 필드 중 transient한정자가 없는 필드가 있다면 아이템 88에서 했던 방식처럼 공격을 할 수 있는 여지가 생긴다.
public class Singletone implements Serializable { public static final Singletone INSTANCE = new Singletone(); private Singletone(){...} private String[] normalFields = {"a", "b"} private Object readResolve() { return INSTANCE; } public void print(){ System.out.println(Arrays.toString(normalFields)); } }
Java
복사
잘못된 싱글턴 - transient가 아닌 필드(normalFields)가 있다
public class SingletoneStealer implements Serializable { static Singletone impersonator; private Singletone payload; private Object readResolve() { //resolve되기 전의 객체 인스턴스 참조 저장 impersonator = payload; //normalFields 필드에 맞는 타입의 객체 반환. return new String[] { "C" }; } private static final long serialVersionUID = 0; }
Java
복사
도둑 클래스
public class SingletoneImpersonator { private static final byte[] serializedForm = {...}; public static void main(String[] args) { Singletone instance = (Singletone) deserrialize(serializedForm); Singletone impersonator = SingletoneStealer.impersonator; instance.print(); impersonator.print(); } }
Java
복사
직렬화의 허점을 이용한 싱글턴 객체 2개 생성 방법
이런 공격을 막기 위해서는 normalFields 에 transient 한정자를 붙혀주면 해결할 수 있지만, 열거 타입으로 바꾸는게 더 나은 선택이다. 열거 타입으로 구현을 하면 선언한 상수 외의 다른 객체는 존재하지 않음을 자바가 보장해주기 때문이다.

정리

readResolve 메서드의 접근성은 매우 중요하다.
final 클래스에선 private 접근 제어자를 사용해야 한다.
final 이 아닌 클래스에서는 하위 클래스를 고려해서 작성할 필요가 있다.
불변식을 지키기 위해 인스턴스를 통제해야 한다면, 열거타입을 권장한다.
열거타입을 사용할 수 없는 상황에서 직렬화와 인스턴스 통제가 필요하다면, readResolve 메서드를 작성하고, 모든 필드를 transient 한정자로 선언해야 한다.

90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라.

개요

이전 아이템에서도 소개했듯이 Serializable을 구현하는 순간 클래스에는 바이트 스트림을 매개변수로받는 또 하나의 생성자가 생긴다고 볼 수 있다. 그렇기에 버그나 보안 문제가 발생할 가능성이 높아질 수 있는데, 이를 직렬화 프록시 패턴(serialization proxy pattern)을 이용해 어느정도 위험을 해소할 수 있다는 점을 소개한다.

직렬화 프록시 패턴(serialization proxy pattern)

바깥 클래스의 논리적 상태를 정밀하게 표현하는 중첩 클래스를 설계해 private static으로 선언한다. 그럼 이 중첩 클래스가 바깥 클래스의 직렬화 프록시다.

특징

중첩 클래스의 생성자는 단 하나여야 한다.
바깥 클래스를 매개변수로 받아야 한다.
생성자는 단순히 인수로 넘어온 인스턴스의 데이터를 복사한다.
바깥 클래스, 직렬화 프록시 모두 Serializable을 구현한다고 선언해야 한다.
예제 코드
public final class Period implements Serializable{ private final Date start; private final Date end; public Period(Date start, Date end) { if(start.compareTo(end) > ){ throw new IllegalArgumentException(start + "가" + end + "보다 늦다."); } this.start = start; this.end = end; } // 직렬화 프록시 private static class SerializationProxy implements Serializable { private final Date start; private final Date end; SerilizationProxy(Period p) { this.start = p.start; this.end = p.end; } private Object readResolve() { return new Period(start, end); } private static final long serialVersionUID = 234098243823486285L; } //직렬화 프록시 패턴용 writeReplcae 메서드 private Object writeReplace() { return new SerializationProxy(this); } public Date start() { return start; } public Date end() { return end; } }
Java
복사
Period 클래스에 적용한 직렬화 프록시 패턴
writeReplace 메서드는 자바의 직렬화 시스템이 바깥 클래스의 인스턴스 대신 SerializationProxy 의 인스턴스를 반환하게 하는 역할을 한다. 즉, 이 메서드로 인해 직렬화 시스템은 바깥 클래스의 직렬화된 인스턴스를 생성할 수 없다.
readResolve 메서드는 공개된 API만을 사용해 바깥 클래스의 인스턴스를 생성하는데, 이 패턴은 직렬화가 생성자를 이용하지 않고 인스턴스를 생성하는 부분을 이와 같이 상당량 해결해준다.

한계점

직렬화 프록시 패턴에도 한계점이 두 가지 있다.
1.
클라이언트가 마음대로 확장할 수 있는 클래스에는 적용할 수 없다.
2.
객체 그래프에 순환이 있는 클래스에도 적용할 수 없다.
3.
방어적 복사를 할 때보다 성능 부분에서 떨어진다.

이전 챕터로