50. 적시에 방어적 복사본을 만들라
✅ 핵심 요약
클라이언트가 여러분의 불변식을 깨뜨릴 수도 있다는 가정 하에, 가변 객체를 외부에서 받을 때나 반환할 때는 반드시 방어적 복사를 수행해야 합니다. 이는 클래스 내부 상태의 안전과 일관성을 유지하는 데 필수입니다.
📚 필수 개념 요약
가변 객체
상태 변경 가능 (Date
, List
, Map
, 배열 등)
불변 객체
상태 변경 불가 (String
, Integer
, LocalDate
, Instant
)
캡슐화
외부에서 내부 상태를 제어하지 못하도록 막는 원칙
불변식
클래스가 언제나 만족해야 하는 상태 조건 (start < end
)
TOCTOU 문제
검사 시점(Time of Check)과 사용 시점(Time of Use) 사이에 상태가 변하는 문제
💡 핵심 포인트
생성자에서 받는 모든 가변 객체는 복사해서 사용해야 한다.
유효성 검사보다 먼저 복사해야 TOCTOU 공격을 피할 수 있다.
clone()
은 서브클래스 오염 위험이 있어 사용하지 말 것.Getter로 내부 필드 그대로 반환하면 객체의 캡슐화가 깨진다.
성능이 문제된다면, 대신 클라이언트에게 문서로 책임 전가하되 명시적으로 작성할 것.
🔍 장점 vs 단점
✅ 클래스의 불변성 보장
⛔ 방어적 복사로 인한 성능 오버헤드
✅ 외부 변경으로부터 상태 보호
⛔ 복사 코드 반복 증가 (보일러플레이트)
✅ 멀티스레드 환경에서도 안정적
⛔ 매번 복사에 따른 객체 생성 비용
✅ 실수로 인한 버그 예방
❗ 자주 발생하는 실수
복사를 안 하고 필드에 그대로 할당
유효성 검사 후 복사 → TOCTOU 문제 노출
Getter에서 원본 필드 반환 → 외부에서 내부 상태 변경 가능
clone() 사용 → 타입 오염 발생 위험
복사 비용이 크다고 생략 후 문서화 안 함 → 유지보수자 혼란
🔧 예제 코드 (Date 기반 Period 클래스)
import java.util.Date;
public final class Period {
private final Date start;
private final Date end;
// 생성자에서 방어적 복사 후 검증
public Period(Date start, Date end) {
this.start = new Date(start.getTime()); // clone() 대신 생성자 사용
this.end = new Date(end.getTime());
if (this.start.after(this.end)) {
throw new IllegalArgumentException("시작 시간이 종료 시간보다 늦을 수 없습니다.");
}
}
// getter에서도 복사
public Date getStart() {
return new Date(start.getTime());
}
public Date getEnd() {
return new Date(end.getTime());
}
}
🔍 만약 복사를 하지 않으면?
Period p = new Period(start, end);
end.setYear(1970); // 외부에서 변경해버리면 내부 불변식이 깨짐!
✅ 결론
방어적 복사는 단순한 예외 대응이 아니라, 객체의 일관성과 불변식을 지키기 위한 기본 전략입니다. 객체가 외부와 어떤 데이터를 주고받든 항상 “내부 상태는 내가 책임진다”는 태도로 코드를 작성하세요.
🎯 느낀점 (강사 코멘트)
객체를 "복사해서 쓰는 것"이 왜 중요한지 잘 몰랐습니다. 객체를 공유하면 의도치 않게 내부 상태가 바뀔 수 있다는 걸 알게 됐습니다.
Date 같은 가변 객체를 그대로 넘기면, 외부 코드가 나도 모르게 내 객체 상태를 바꿀 수 있다는 사실.
'클라이언트는 내 코드를 망가뜨릴 수도 있다'는 생각을 전제로 코드를 짜야 한다고 느낌.
실제로 이런 실수를 방지해준다면 꼭 필요한 수고라는 걸 느낌.
방어적 복사는 객체의 안전한 사용법에 대한 중요한 태도
Last updated