public LottoNumbers(final List<LottoNumber> lottoNumbers) {
validateLottoNumbers(lottoNumbers);
this.lottoNumbers = ensureSorted(lottoNumbers);
}
`LottoNumbers`라는로또 미션을 수행하던 중에 구두로 리뷰를 받은 적이 있다.
"외부에서 전달된 `List`를 그대로 내부 필드에 할당하면, 외부에서 리스트를 변경할 때 내부 필드도 함께 영향을 받는 문제가 발생할 수 있지 않을까요?"
맞는 말이었다. 왜 내가 고려하지 않았을까?
생각보다 관성적으로 코드를 작성하고 있는 순간들이 많다는 것을 다시 한 번 깨달았다.
🚨 외부에서 리스트를 그대로 내부 필드에 할당하면 발생하는 문제점 (Side Effect)
지금과 같이 코드를 작성하면 아래와 같이 문제가 발생한다.
List<LottoNumber> numbers = new ArrayList<>(Arrays.asList(
new LottoNumber(1),
new LottoNumber(2),
new LottoNumber(3)
));
LottoNumbers lottoNumbers = new LottoNumbers(numbers);
// 외부 리스트 변경
numbers.add(new LottoNumber(45));
// 내부 리스트도 변경됨 😱
System.out.println(lottoNumbers.getLottoNumbers());
// 출력: [1, 2, 3, 45] (의도치 않은 내부 변경)
이렇게 되면 캡술화위반이며, 불변성(Immutablility)이 깨지게 된다.
그러면 어떻게 하면 좋을까??
💊 방어적 복사( Defensive Copy)
1️⃣ 생성자에서 불변 리스트로 복사
public class LottoNumbers {
private final List<LottoNumber> lottoNumbers;
public LottoNumbers(List<LottoNumber> lottoNumbers) {
this.lottoNumbers = List.copyOf(lottoNumbers); // 불변 리스트로 복사
}
}
❓ `List.copyOf()`
1. 자바 10이상에서 사용 가능
2. 내부적으로 불변 리스트(immutable List)를 반환
3. 외부 변경 시 내부 리스트는 영향을 받지 않음
List<LottoNumber> numbers = new ArrayList<>(Arrays.asList(
new LottoNumber(1),
new LottoNumber(2),
new LottoNumber(3)
));
LottoNumbers lottoNumbers = new LottoNumbers(numbers);
// 외부 리스트 변경 시도
numbers.add(new LottoNumber(45));
// 내부 리스트는 안전함 🚀
System.out.println(lottoNumbers.getLottoNumbers());
// 출력: [1, 2, 3] (변경되지 않음)
2️⃣ 생성자에서 `ArrayList`로 복사
만약 Java 8 이하를 사용한다면, `ArrayList`로 명시적 복사를 사용하면 된다.
public class LottoNumbers {
private final List<LottoNumber> lottoNumbers;
public LottoNumbers(List<LottoNumber> lottoNumbers) {
this.lottoNumbers = new ArrayList<>(lottoNumbers);
}
}
이렇게 하면 독립된 복사본이기 때문에 안전하지만 반환된 리스트는 수정이 가능하기 때문에 그렇게 추천하는 방법은 아니다.
결국 리턴을 하고나서도 수정이 불가능하게하려면 리턴할 때 다시 `new ArrayList<>()`로 복사하고 리턴해야한다.
바로 이때 `new ArraayList<>()`로 리턴 할 때마다 복사하기 때문에 성능도 느려진다.
3️⃣ Collections.unmodifiableList()로 불변처리
리스트가 불변인지 보장하고 싶다면, `Collections.unmodifiableList()`를 사용하면 된다.
public class LottoNumbers {
private final List<LottoNumber> lottoNumbers;
public LottoNumbers(List<LottoNumber> lottoNumbers) {
this.lottoNumbers = Collections.unmodifiableList(new ArrayList<>(lottoNumbers));
}
}
`unmodifiableList()`는 수정시 `UnsupportedOperationException`이 발생하기 때문에 내부리스트는 여전히 복사본으로 안전하게 보호된다.
이 방법은 불변 리스트로 포장하는 것이기 때문에 내부 필드의 안정성은 보장되며 리턴 시 수정도 불가능 하기 때문에 더 좋다.
또한 리턴 시 성능도 내부 참조만 반환하기 때문에 new ArrayList<>()로 하는 것보다 더 빠르다.
'JAVA > 개념' 카테고리의 다른 글
yyyy-MM-dd vs YYYY-MM-dd (feat. 자바 날짜 포맷팅) (0) | 2025.02.21 |
---|---|
private 생성자를 왜 만드시나요? (0) | 2025.02.15 |
언제 equals를 재정의 해야할까? (0) | 2024.11.22 |
Collection.forEach vs Stream.forEach (1) | 2024.11.20 |
stream.forEach (feat. 종단 연산) (1) | 2024.11.18 |