글을 쓰게 된 이유
우테코 두 번째 미션을 진행하면서 리뷰를 기다리는 동안 다른 분들의 리뷰를 가끔 구경하러 갔다,,
그런데 "unmodifiableList를 사용하면 무조건 불변을 보장할 수 있을까요?"에 대한 리뷰였다.
나는 첫번째 자동차 미션에서 Cars 일급 컬렉션 클래스에서 List<Car>를 반환할 때, unmodifiableList를 통해 반환했다. 당연히 불변이 보장되는 줄 알고 말이다,, 그 리뷰를 보고 주섬주섬 자동차 미션을 꺼내서 테스트를 해보고 공부해 보았다.
unmodifiableList 보기
description을 보면 수정 불가능한 보기를 반환합니다. 즉, 읽기 전용의 List를 반환한다는 뜻이겠지요? 수정을 시도할 경우에는 UnsupportedOperationException이 발생한다고 한다면 불변성을 보장하는 게 아닐까요?
더 봐보겠습니다.
UnmodifiableList를 통해서 List를 다시 한번 포장하여 수정을 불가능하게 됩니다.
set, add, remove, addAll 등 List를 조작하는 메서드를 사용하려 하면 UnsupportedOperationException을 반환하는 것을 알 수 있습니다.
하지만 중요한 부분은 "생성자"입니다.
생성자를 보면 인자로 받은 list가 그대로 UnmodifiableList의 인스턴스 변수인 list에 들어가게 됩니다.
즉, 이렇게 되면 원본 객체의 수정을 막지 못한다는 것입니다.
테스트를 통해 확인해 보겠습니다.
테스트로 확인
public class Cars {
private final List<Car> cars;
public Cars(final List<Car> cars) {
this.cars = cars;
}
public List<Car> getCars() {
return Collections.unmodifiableList(cars);
}
}
@Test
void test_mutable() throws Exception {
//given
List<Car> carList = new ArrayList<>();
carList.add(new Car(Name.fromName("aa")));
carList.add(new Car(Name.fromName("bb")));
Cars cars = new Cars(carList);
List<Car> mutableCars = cars.getCars();
int beforeCarsSize = mutableCars.size();
//when
carList.remove(0); //원본 List 수정
int afterCarsSize = mutableCars.size();
//then
assertAll(
() -> assertNotEquals(beforeCarsSize, afterCarsSize),
() -> assertThat(beforeCarsSize).isEqualTo(2),
() -> assertThat(afterCarsSize).isEqualTo(1),
() -> assertThatThrownBy(() -> mutableCars.remove(0))
.isInstanceOf(UnsupportedOperationException.class)
);
}
원본 List를 수정하니 beforeCarsSize와 afterCarsSize가 다른 것을 알 수 있습니다.
당연히 UnmodifiableList로 감싸진 mutableCars를 수정하게 되면 UnsupportedOpertaionException이 발생하게 되는 거고요.
방어적 복사(copyOf)
그러면 어떻게 해야 원본 객체를 수정해도 Cars에 있는 List에 영향이 없으면서 불변하게 할 수 있을까요?
결과부터 말씀드리자면 List에서 제공하는 copyOf 메서드를 사용하면 됩니다.
copyOf는 ImmutableCollections 클래스에 있는 listCopy 메서드를 사용합니다.
listCopy 메서드를 보면 인자로 받은 Collection을 List.of를 통해서 다시 재생성합니다.
즉, List를 새로 생성하면서 기존 원본 List와의 참조를 끊고, List.of()를 통해 읽기 전용 리스트로 만듭니다.
테스트로 확인
public class Cars {
private final List<Car> cars;
public Cars(final List<Car> cars) {
this.cars = cars;
}
public List<Car> getCars() {
return List.copyOf(cars);
}
}
@Test
void test_immutable() throws Exception {
//given
List<Car> carList = new ArrayList<>();
carList.add(new Car(Name.fromName("aa")));
carList.add(new Car(Name.fromName("bb")));
Cars cars = new Cars(carList);
List<Car> immutableCars = cars.getCars();
int beforeCarsSize = immutableCars.size();
//when
carList.remove(0);
int afterCarsSize = immutableCars.size();
//then
assertAll(
() -> assertEquals(beforeCarsSize, afterCarsSize),
() -> assertThat(beforeCarsSize).isEqualTo(2),
() -> assertThat(afterCarsSize).isEqualTo(2),
() -> assertThatThrownBy(() -> immutableCars.remove(0))
.isInstanceOf(UnsupportedOperationException.class)
);
}
그러면 언제 방어적 복사를 해야 할까?
공부를 해보면서 Cars 객체를 생성할 때 방어적 복사를 해야 할까? getCars()를 할 때 방어적 복사를 해야 할까?라는 고민이 생겼습니다.
생성자에서 방어적 복사를 하게 되면 getCars()를 해도 방어적 복사가 된 읽기 전용의 List를 반환하기 때문에 불변성을 보장해야 한다면 생성자에서 방어적 복사를 해주는 게 안전하다고 생각합니다.
'Java' 카테고리의 다른 글
try-with-resources 는 왜 사용해야할까? (0) | 2023.03.28 |
---|---|
파라미터에 Optional은 왜 안티패턴? (0) | 2023.03.25 |
Generic & Wildcard (0) | 2022.12.29 |
Generic in Java (2) | 2022.12.17 |
JDK ? JRE ? (0) | 2022.09.15 |