Effective Java : 아이템 50. 적시에 방어적 복사를 만들라.
- 카테고리 없음
- 2023. 9. 6.
Effective Java : 아이템 50. 적시에 방어적 복사를 만들라.
- 클라이언트의 악의적인 공격 / 실수에 의해 자바 객체의 불변식이 깨질 수 있음. 방어적 복사를 통해 불변식이 깨지는 것을 막아야함.
- 주의를 기울이지 않으면 다음 경우에 불변식이 깨짐
- 가변 객체를 매개변수로 받고, 그걸 방어적 복사 없이 인스턴스 필드로 사용하는 경우 (외부에서 해당 참조를가지고 있다면 수정 가능)
- 인스턴스의 메서드가 방어적 복사 없이 가변 객체를 반환하는 경우 (외부에서 수정 가능)
- 가변 객체의 예시는?
- 크기가 1이상인 배열
- Collection
- Setter()가 열려있는 녀석들.
- 언제 방어적 복사를 안해도 될까?
- 클라이언트가 생성자에게 전달한 매개변수의 통제권을 클라이언트가 포기할 때
- 내부 필드가 모두 불변 객체일 때
- 클라이언트에게 받은 / 반환한 객체가 수정되어도 괜찮을 때
적시에 방어적 복사를 만들어야 하는 이유
자바는 일반적으로 타입 안전한 언어이기 때문에 불변식이 지켜진다. 하지만 클라이언트의 악의적인 공격 / 실수가 있다면 불변식이 깨지는 경우가 발생할 수 있다. 따라서 가급적이면 방어적 복사를 통해 내가 설계한 클래스의 불변식을 지키도록 노력해야한다.
일반적으로 어떤 객체든 그 객체의 허락이 없다면 외부에서 객체 내부를 수정하는 것을 불가능하다. 이를 테면 Setter가 없는데 어떻게 외부에서 객체 내부를 수정할 수 있을까? 사실은 수정할 수 있는 방법이 있다. 그리고 주의를 기울이지 않으면, 자기도 모르게 내부를 수정하도록 허락하는 경우가 생긴다.
자기도 모르게 내부가 수정되는 경우.
public final class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException();
}
this.start = start;
this.end = end;
}
public Date start() {
return this.start;
}
public Date end() {
return this.end;
}
}
얼핏보면 위 클래스는 불변 클래스처럼 보인다. final 클래스이며, 어떠한 Setter()도 존재하지 않기 때문이다. 하지만 사실은 이 클래스의 내부 상태는 변경이 가능하다. 즉, 불변식이 깨질 수 있다.
public static void main(String[] args) {
Date start = new Date(2023);
Date end = new Date(2023);
Period p = new Period(start, end);
p.end().setYear(78); // p의 내부를 수정함.
}
위에서 Period p의 불변식이 깨진 이유는 크게 두 가지다.
- 객체를 생성한 녀석이 필드로 전달한 매개변수의 수정 권한을 가지고 있음.
- p가 end()를 호출했을 때, 반환한 객체가 수정 가능한 객체다.
이런 이유 때문에 불변 객체라고 생각했던 녀석이 자신도 모르게 수정될 수 있게 된다.
불변 객체로 만들기 → 생성자에서 방어적 복사
public PeriodEnhanced(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();
}
}
첫번째 이유에 대한 방어책으로는 생성자가 매개변수를 받아서 객체를 생성할 때, 방어적 복사를 통해 객체를 생성해서 가지고 있는 것이다. 이 때 특이한 점은 방어적 복사가 완료된 다음에 매개변수 유효성 검사를 진행하는 부분이다.
이것은 멀티 쓰레드 환경에서 시간차 공격을 통해 불변성이 깨질 가능성이 있고, 이것을 예방하기 위함이다. 아래를 고려해보자. 2번 과정에서 다른 스레드에 의해서 문제가 발생할 수 있기 때문이다.
- 객체가 매개변수의 유효성 검사를 함 (유효성 검사 통과)
- 다른 스레드가 매개변수의 값을 수정함. (악의적인 값으로 수정)
- 객체가 생성됨.
불변 객체로 만들기 → 객체 반환 시, 방어적 복사
public Date start() {
return new Date(this.start.getTime());
}
public Date end() {
return new Date(this.end.getTime());
}
앞서서 특정 객체의 메서드가 반환하는 내부객체가 가변가능한 경우, 의도치 않게 불변식이 깨지는 것을 확인할 수 있었다. 이것을 방지하기 위해서 반환 타입을 불변 객체로 하면 내부 객체가 의도하지 않게 바뀌는 것을 방지할 수 있게 된다.
어떤 것들이 가변 객체일까?
- 길이가 1이상인 배열은 무조건 가변임.
- Setter()가 열려있는 객체들.
- Collection 같은 녀석들
이런 녀석들은 상태를 손쉽게 바꿀 수 있다. 따라서 가변객체가 아니다.
방어적 복사를 생략할 때?
방어적 복사는 성능을 희생하는 작업이다. 방어적 복사를 할 때 생성하는 객체들의 비용이 비쌀 가능성도 있기 때문이다. 또한, 항상 방어적 복사를 할 수 있는 상태가 아닐 수도 있다. 이런 상황들을 감안한다면 방어적 복사를 생략할 때도 필요하다. 아래 경우를 가정해 볼 수 있겠다.
- 내부 필드가 모두 불변 객체인 경우 → 방어적 복사 생략 가능
- 클라이언트에게 받은 객체의 경우, 클라이언트가 통제권을 아예 포기할 때
- 클라이언트에게 받은 객체, 반환하는 객체가 수정되어도 괜찮을 때
이런 경우가 아니라면 가급적으로 방어적 복사를 하는 것이 안전한 프로그램을 작성하는데 도움이 된다.