방어적 복사(Defensive Copy)란?
: 외부에서 전달된 객체나 컬렉션이 내부 데이터에 영향을 주는 것을 막기 위해 복사본을 만들어 사용하는 방법
이 방식은 특히 객체 간 데이터 공유로 인해 발생할 수 있는 예기치 못한 버그나 사이드 이펙트(부작용)를 방지하는 데 유용
왜 필요할까?
자바에서는 객체나 컬렉션을 다른 클래스에 전달하면, 그 참조값(주소)이 공유되기 때문에
외부에서 값을 바꾸면 내부 데이터도 바뀌는 문제가 발생할 수 있습니다.
이를 막기 위해 전달받은 값을 복사해서 사용하는 것이 방어적 복사
방어적 복사를 사용하는 대표적인 시점
- 생성자에서
- 외부에서 넘겨준 값을 객체 내부에 저장할 때, 원본을 직접 사용하지 않고 복사본을 저장
public class Lotto {
private final List<Integer> numbers;
public Lotto(List<Integer> numbers) {
this.numbers = new ArrayList<>(numbers); // 복사본 저장
}
}
// 이렇게 하면 외부에서 넘겨준 numbers 리스트를 변경해도, 내부의 numbers는 영향을 받지 않습니다.
- Getter 메서드에서
- 내부 데이터를 외부로 전달할 때도, 복사본을 반환합니다.
public List<Integer> getNumbers() {
return new ArrayList<>(numbers); // 원본이 아닌 복사본 반환
}
// 이렇게 하면 외부에서 리스트를 받아 변경해도, 내부 데이터는 안전하게 보호됩니다.
실전 예제와 문제점
public class Lotto {
private final List<LottoNumber> numbers;
public Lotto(List<LottoNumber> numbers) {
validateSize(numbers);
this.numbers = new ArrayList<>(numbers); // 방어적 복사
}
}
- 표면적으로는 잘 작성된 코드처럼 보이지만, 다음 두 가지 문제가 존재
문제 1 ) 얕은 복사 : 복사가 얕게 이루어져 내부 객체는 여전히 공유됨
- numbers 리스트는 복사했지만, 리스트 안에 있는 객체는 복사하지 않았기 때문에
외부에서 LottoNumber 객체를 수정하면 Lotto 내부의 값도 같이 바뀝니다.
- 해결 방법
- LottoNumber를 불변(Immutable) 객체로 만들기
- 불변 객체 : 한 번 생성되면 내부 상태가 절대 바뀌지 않는 객체
- LottoNumber를 불변(Immutable) 객체로 만들기
아래 코드
- final 클래스: 상속을 막아 불변성 유지
- 필드에 final: 생성 후 값 변경 방지
- setter 없음: 외부에서 값 수정 불가능
public final class LottoNumber {
private final int number;
public LottoNumber(int number) {
this.number = number;
}
public int getNumber() {
return number;
}
// 변경 메서드 제거! setNumber 같은 메서드가 없어야 불변
}
2. 깊은 복사(Deep Copy)
// 깊은 복사 구현 예시
public class LottoNumber {
private int number;
public LottoNumber(int number) {
this.number = number;
}
public void setNumber(int number) {
this.number = number;
}
public int getNumber() {
return number;
}
// 깊은 복사를 위한 복제 메서드
public LottoNumber copy() {
return new LottoNumber(this.number);
}
}
// Lotto 생성자에서 깊은 복사 사용
public class Lotto {
private final List<LottoNumber> numbers;
public Lotto(List<LottoNumber> numbers) {
// 리스트 복사 + 각 요소도 복사
List<LottoNumber> copy = numbers.stream()
.map(LottoNumber::copy)
.collect(Collectors.toList());
this.numbers = copy;
}
}
문제 2 ) 검증 전에 외부에서 리스트를 변경할 수 있음
- validateSize() 호출 직후에 외부에서 numbers 리스트를 변경할 수 있습니다.
그 결과, 검증은 통과했지만 실제 저장된 값은 유효하지 않을 수 있습니다.
public Lotto(List<LottoNumber> numbers) {
validateSize(numbers); // 검증 통과
this.numbers = new ArrayList<>(numbers); // 그 사이 외부에서 리스트를 바꿈
}
- 해결 방법
- 방어적 복사를 먼저 수행한 후 검증을 진행
public Lotto(List<LottoNumber> numbers) {
List<LottoNumber> copy = new ArrayList<>(numbers);
validateSize(copy);
this.numbers = copy;
}
'IT > CS 공부' 카테고리의 다른 글
| [CS] URI, URL, URN (1) | 2025.05.28 |
|---|---|
| [CS] CPU 스케줄링 (0) | 2025.05.28 |
| [CS] Call By Value와 Call By Reference (0) | 2025.05.22 |
| [CS] Redis가 싱글 스레드로 동작하는 이유 (0) | 2025.05.20 |
| [CS] CAP 정리 (0) | 2025.05.15 |