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

이펙티브 자바

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

package com.github.sejoung.codetest.serialization;

import java.io.Serializable;
import java.util.Date;

public final class Period implements Serializable {

private Date start;
private Date end;

public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
}

public Date start() {
return new Date(start.getTime());
}

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

}


위에 코드를 역직렬화 하면 시작일이 종료일보다 늦게 생성 될수도 있다 그것을 방지하기 위해 readObject 메소드를 방어적으로 작성한 경우이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

package com.github.sejoung.codetest.serialization;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Date;

public final class Period implements Serializable {

private Date start;
private Date end;

public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
}

public Date start() {
return new Date(start.getTime());
}

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

private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
if (start.compareTo(end) > 0) {
throw new InvalidObjectException(start + " after " + end);
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

package com.github.sejoung.codetest.serialization;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Date;

public class MutablePeriod {

private Period period;

private Date start;

private Date end;

public MutablePeriod() {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);

// Serialize a valid Period instance
out.writeObject(new Period(new Date(), new Date()));

byte[] ref = {0x71, 0, 0x7e, 0, 5}; // Ref #5
bos.write(ref); // The start field
ref[4] = 4; // Ref # 4
bos.write(ref); // The end field

// Deserialize Period and "stolen" Date references
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
period = (Period) in.readObject();
start = (Date) in.readObject();
end = (Date) in.readObject();
} catch (Exception e) {
throw new AssertionError(e);
}
}

public static void main(String[] args) {
MutablePeriod mp = new MutablePeriod();

Period p = mp.period;
Date pEnd = mp.end;

pEnd.setTime(78);
System.out.println(p);

pEnd.setTime(68);
System.out.println(p);

}


}

실행결과

1
2
3
4
5
6

Period(start=Thu Mar 28 14:57:54 KST 2019, end=Thu Jan 01 09:00:00 KST 1970)
Period(start=Thu Mar 28 14:57:54 KST 2019, end=Thu Jan 01 09:00:00 KST 1970)

Process finished with exit code 0

위에서 참조를 훔쳐 와서 mp의 데이터를 변경했지만 p에 데이터가 변경되었다

위에 코드를 좀더 방어적 복사를 사용해서 방어를 하면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

package com.github.sejoung.codetest.serialization;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Date;
import lombok.ToString;


@ToString
public final class Period implements Serializable {

private Date start;
private Date end;

public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
}

public Date start() {
return new Date(start.getTime());
}

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

private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
start = new Date(start.getTime());
end = new Date(end.getTime());
if (start.compareTo(end) > 0) {
throw new InvalidObjectException(start + " after " + end);
}
}

}


위에 코드를 바꾸고 다시 공격하면

1
2
3
4
5
6

Period(start=Thu Mar 28 14:59:46 KST 2019, end=Thu Mar 28 14:59:46 KST 2019)
Period(start=Thu Mar 28 14:59:46 KST 2019, end=Thu Mar 28 14:59:46 KST 2019)

Process finished with exit code 0

위에처럼 정상 수행된다.

객체를 역직렬화할 때는 클라이언트가 소유해서는 안되는 객체 참조를 갖는 필드를 모두 반드시 방어적으로 복사해야 된다.

참조