다음과 같은 코드가 있다.
import java.util.*;
import java.io.*;
public class LottoTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
List<LottoNumber> list=new ArrayList<>();
LottoNumber ln1=new LottoNumber(1);
LottoNumber ln2=new LottoNumber(2);
LottoNumber ln3=new LottoNumber(3);
list.add(ln1); list.add(ln2); list.add(ln3);
Lotto lotto=new Lotto(list);
System.out.println("before");
System.out.println(lotto.getLotto().toString());
System.out.println("after");
list.get(2).setValue(10000);
System.out.println(lotto.getLotto().toString());
}
}
해당 코드에서는 lottoNumber로 만들어진 리스트를 만들고 이를 이용해서 주입해서 lotto객체를 만들었다.
이 이후 lottoNumber를 변경하면 lottoNumber를 주입해서 만든 Lotto객체 역시 바뀌어있다.
before
[LottoNumber [value=1], LottoNumber [value=2], LottoNumber [value=3]]
after
[LottoNumber [value=1], LottoNumber [value=2], LottoNumber [value=10000]]
이를 막기 위해서 우리는 방어적 복사를 이용한다.
방어적 복사란 원본과의 참조를 끊은 복사본을 만들어서 주입하거나, 반환하는 방식으로, 위처럼 원본의 변경에 의한 예상치 못한 side effect를 방지해서 개발자가 의도한 코드를 짤 수 있다.
주로 생성자에서 필드를 초기화할 때와, getter메서드에 필드를 반환 할 때, 직접 값을 이용해서 복사본을 만들어서 사용한다.
컬렉션 자료구조를 반환하는 경우라면 자바에서는 Unmodified Collection
을 이용해서, 변경시 예외를 발생시킬 수 있다.
public Person(List<String> friends) {
// 원본을 복사하고, 불변 리스트로 변환
this.friends = Collections.unmodifiableList(new ArrayList<>(friends));
}
기존의 Lotto 코드는 다음과 같다.
import java.util.ArrayList;
import java.util.List;
public class Lotto {
private List<LottoNumber> lottos;
public Lotto(List<LottoNumber> lottos) {
this.lottos=lottos;
}
public List<LottoNumber> getLotto(){
return this.lottos;
}
}
이 코드는 생성자에 주입할 때도 참조가 되고, getter를 통해서 반환할 때도 참조를 반환해서 lottos
를 변경시 Lotto객체 내부의 lottos역시 함께 변한다.
이 문제를 해결하기 위해서 다음과 같이 코드를 짤 수 있다.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Lotto {
private final List<LottoNumber> lottos;
public Lotto(List<LottoNumber> lottos) {
// 리스트 내부 객체까지 복사 (깊은 복사)
List<LottoNumber> copiedNumbers = new ArrayList<>();
for (LottoNumber number : lottos) {
copiedNumbers.add(new LottoNumber(number.getValue())); // 새로운 LottoNumber 생성
}
this.lottos = Collections.unmodifiableList(copiedNumbers); // 불변 리스트로 변환
}
public List<LottoNumber> getLotto() {
return lottos; // 불변 리스트 반환
}
}
내부는 깊은 복사로 참조를 막고, 외부는 unmodifiedList를 사용하는 방법이다.
그냥 변경되면 안되는 컬렉션이면, final키워드를 사용하는 게 훨씬 낫지 않나 라는 개인적인 생각이다.
하지만 만약 변경이 필수적이고 그 내부에 컬렉션을 주입한다면, 위처럼 직접 복사를 이용하는게 맞다고 보여진다.
public class LottoNumber {
final int value;
public LottoNumber(int v) {
this.value=v;
}
public int getValue() {
return value;
}
@Override
public String toString() {
return "LottoNumber [value=" + value + "]";
}
}
public class Lotto {
private final List<LottoNumber> lottos;
public Lotto(List<LottoNumber> lottos) {
this.lottos=new ArrayList<>(lottos);
}
public List<LottoNumber> getLotto(){
return new ArrayList<>(lottos);
}
}
이 코드는 리스트 내부에 있는 값들에 대한 복사가 이루어지는 것이 아니라서 완전한 방어적 복사라고 보기는 힘들다.