본문 바로가기
Java

[Java] List 복사 방법과 불변 리스트, unmodifiableList와 copyOf

by 당코 2024. 3. 4.

자바에서 불변이라는 개념은 상당히 중요하다.

외부에서 해당 객체나 리스트에 접근하여 데이터를 수정, 삭제하도록 열어둘 경우 예상치 못한 오류가 발생할 수 있기 때문이다.

 

자동차의 이름을 List로 가지고 있는 Cars 클래스가 있다고 하자.

Cars cars = new Cars("capy1","capy2","capy3");
List<String> prevCars = cars.getNames();

prevCars.add("newCapy");

List<String> nextCars = cars.getNames();

 

Cars 객체를 생성하고 이름의 목록을 List로 가져온 뒤에, 반환받은 prevCars에 새로운 이름을 추가하였다.

반환받은 prevCars 리스트와 cars 객체가 가지고 있는 리스트가 서로 연결되어 있지 않다면  cars에서 새로 이름의 목록을 반환받았을 때 capy1, capy2, capy3만 반환받아야 할 것이다.

하지만 리스트의 데이터를 살펴보게 되면 다음과 같이 원본 객체에도 추가되어 있는 것을 볼 수 있다.

prevCarsNames = capy1
prevCarsNames = capy2
prevCarsNames = capy3

afterCarsNames = capy1
afterCarsNames = capy2
afterCarsNames = capy3
afterCarsNames = newCapy

 

이는 getNames()가 원본 리스트의 참조를 그대로 반환하기 때문인데, 이렇게 원본 리스트의 참조를 그대로 반환하게 되면 예상치 못한 결과가 발생할 수 있다.

이를 막기 위해 원본 리스트의 참조를 직접 반환하는 대신 불변 리스트를 반환하기 위해 사용하는 대표적인 2가지 방식이 있는데 Collections.unmodifiableList와 List.copyOf이다.

 

Collections.unmodifiableList

Collections.unmodifiableList는 리스트를 불변 리스트로 만들어 반환한다. 내부 코드를 통해 어떻게 작동하는지 살펴보자.

 

이미 리스트가 불변리스트라면 다시 불변으로 만들 필요가 없기 때문에 그대로 반환한다.

불변이 아니라면 UnmodifiableRandomAccessList <> 또는 UnmodifiableList <>로 리스트를 감싸 반환한다.

이 둘은 RandomAccess 인터페이스를 구현하고 있는지 여부에 따라 구분되는데, 이를 쉽게 표현하자면 index를 통한 배열의 접근을 허용하는지를 말한다.

ArrayList, Vector는 RandomAccess 인터페이스를 구현하여 인덱스로 배열의 데이터에 접근할 수 있는 반면에

LinkedList는 내부적으로 노드를 통해 구현되어 있어 데이터를 탐색하기 위해서는 리스트의 앞부터 순차적으로 탐색해야 한다.

 

Collections.unmodifiableList는 어떻게 리스트의 불변을 보장하는 것일까?

 

내부적으로 읽기 기능을 제외한 쓰기 기능에 대해서는 UnsupportedOperationException을 던지는 것을 볼 수 있다.

즉 원본 리스트에 대한 읽기 전용 뷰를 제공하여 외부에서 리스트에 대한 추가, 삭제를 막는다는 것이다.

 

그렇다면 Collections.unmodifiableList를 사용하면 리스트는 항상 불변인 것일까?

여기에는 2가지 주의해야 할 점이 있다.

 

주의할 점

1. 원본 리스트의 수정

UnmodifiableList의 내부 구현을 다시 한번 살펴보자.

 

내부적으로 원본 리스트를 멤버 변수로 가지고 있는 것을 볼 수 있다.

즉, 반환받은 불변 리스트는 원본 리스트를 참조하고 있다는 것이다.

따라서 만약 원본 리스트가 수정이 되게 된다면, 이를 참조하고 있는 불변 리스트의 데이터도 그에 따라 변하게 된다.

List<Car> cars = new ArrayList<>();
List<Car> unmodifiableCars = Collections.unmodifiableList(cars);

//불변 리스트의 size는 0
Assertions.assertThat(unmodifiableCars.size()).isEqualTo(0);

//원본 리스트에 데이터 추가
cars.add(new Car("capy"));

//원본 리스트에 데이터를 추가한 후 불변 리스트의 size는 1
Assertions.assertThat(unmodifiableCars.size()).isEqualTo(1);

 

원본 리스트에 데이터를 추가하기 전과 후를 봤을 때 Collections.unmodifiableList를 통해 반환받은 불변 리스트에도 데이터가 추가되어 있는 것을 볼 수 있다.

 

이제 Collections.unmodifiableList를 통해 만든 불변 리스트 자체에서 리스트에 데이터를 추가하거나 삭제하는 것을 막을 수 있다는 것은 알고 있다.

하지만 불변 리스트에 저장되어 있는 데이터 자체의 수정도 막는 것일까?

 

2. 불변 리스트 내부 객체의 수정

List<Car> cars = new ArrayList<>();
cars.add(new Car("capy"));
List<Car> unmodifiableCars = Collections.unmodifiableList(cars);

//원본 리스트에 저장된 Car의 이름은 capy
Assertions.assertThat(cars.get(0).getName()).isEqualTo("capy");

//불변 리스트의 저장된 Car의 이름을 변경
unmodifiableCars.get(0).setName("changedCapy");

//원본 리스트의 저장된 Car의 이름이 changedCapy로 변경됨
Assertions.assertThat(cars.get(0).getName()).isEqualTo("changedCapy");

 

위의 코드를 통해 살펴본 것처럼, Car를 저장하는 리스트가 불변이더라도 Car 객체 자체가 불변이 아니라면 저장되어 있는 데이터의 수정을 막지 못한다.

이는 Collections.unmodifiableList의 내부 구현에서 원본 리스트 자체의 쓰기 기능만 막고 내부 데이터들에 대한 처리는 하지 않고 있기 때문이다.

원본 리스트에 저장된 데이터가 변했을 때 불변 리스트에 저장된 데이터가 변하는 것만 테스트했지만 그 반대도 성립한다.

따라서 불변 리스트의 내부 데이터의 불변까지 보장하기 위해서는 저장하는 데이터가 불변이어야 한다.

 

List.copyOf

List.copyOf도 불변 리스트를 반환하는 기능을 하는 메서드이다. 이번에도 내부코드를 살펴보겠다.

 

List.copyOf 는 내부적으로 ImmutableCollections.listCopy()를 호출한다.

listCopy는 주어진 리스트가 List12의 인스턴스이거나 ListN 인스턴스이면서 null을 허용하지 않는 경우는 해당 리스트를 그대로 반환한다. 이 경우는 이미 불변 리스트이기 때문에 별도의 작업 없이 리스트를 반환한다.

그 외의 경우 주어진 리스트를 toArray()를 통해 배열로 변환하고, 이 배열을 List.of()의 인자로 전달하여 불변 리스트를 생성한다.

 

또한 List.copyOf 역시  동일하게 리스트에 데이터를 추가, 삭제하는 메서드에 대해서 UnsupportedOperationException를 던지는 것을 볼 수 있다.

 

주의할 점

1. 원본 리스트의 수정

원본 리스트를 참조하는 것이 아닌 List.of()를 통해 새로운 리스트를 생성하여 원본 데이터를 복사하여 반환하기 때문에 Collections.unmodifiableList와 달리 원본 리스트에 데이터가 추가, 삭제되어도 반환받은 불변 리스트는 변하지 않는다.

List<Car> cars = new ArrayList<>();
List<Car> copyOfCars = List.copyOf(cars);

//불변 리스트의 size는 0
Assertions.assertThat(copyOfCars.size()).isEqualTo(0);

//원본 리스트에 데이터 추가
cars.add(new Car("capy"));

//원본 리스트에 데이터를 추가해도 불변 리스트의 size는 0
Assertions.assertThat(copyOfCars.size()).isEqualTo(0);

 

원본 리스트에 데이터를 추가하는 동일한 테스트를 진행하였을 때 List.copyOf를 통해 생성한 불변 리스트는 변하지 않는다.

그렇다면 Collections.unmodifiableList와 다르게  불변 리스트에 저장되어 있는 데이터 자체의 수정도 막는 것일까?

2. 불변 리스트 내부 객체의 수정

List<Car> cars = new ArrayList<>();
cars.add(new Car("capy"));
List<Car> copyOfCar = List.copyOf(cars);

//원본 리스트에 저장된 Car의 이름은 capy
Assertions.assertThat(cars.get(0).getName()).isEqualTo("capy");

//불변 리스트의 저장된 Car의 이름을 변경
copyOfCar.get(0).setName("changedCapy");

//원본 리스트의 저장된 Car의 이름이 changedCapy로 변경됨
Assertions.assertThat(cars.get(0).getName()).isEqualTo("changedCapy");

 

Collections.unmodifiableList와 마찬가지로 원본 리스트에 저장되어 있는 데이터 자체가 변할 경우 불변 리스트에 저장되어 있는 데이터도 변하게 된다.

이는 List.copyOf의 내부 구현에서 List.of()로 원본 리스트와 다른 새로운 리스트를 생성하지만 안에 저장하는 데이터는 동일하게 저장하기 때문이다.

따라서 Collections.unmodifiableList를 사용할 때와 마찬가지로  불변 리스트의 내부 데이터의 불변까지 보장하기 위해서는 저장하는 데이터가 불변이어야 한다.

 

결론

Collections.unmodifiableList

  • 원본 리스트에 데이터가 추가, 삭제되면 불변 리스트도 추가, 삭제된다.
  • 원본 리스트에 저장되어 있는 데이터 자체가 수정되면 불변 리스트에도 반영이 된다.(반대도 성립)

List.copyOf

  • 원본 리스트에 데이터가 추가, 삭제되어도 불변 리스트는 변하지 않는다.
  • 원본 리스트에 저장되어 있는 데이터 자체가 수정되면 불변 리스트에도 반영이 된다.(반대도 성립)

 

따라서 원본 리스트가 수정될 여지가 없다면 Collections.unmodifiableList, List.copyOf 중 어느 것을 써도 무방하지만,

원본 리스트가 수정될 가능성이 있고 수정된 결과를 반영하고 싶지 않으면 List.copyOf를 사용하자.

만약 불변 리스트 내부의 데이터의 불변도 보장하고 싶다면 내부 데이터를 불변 객체로 만들거나,

Collections.unmodifiableList, List.copyOf를 사용하지 않고 원본 리스트 내부의 데이터의 각각의 필드를 복사하여 새로운 객체를 만든 뒤 새로운 리스트에 담아 반환하자.

'Java' 카테고리의 다른 글

[Java] Random vs ThreadLocalRandom  (2) 2024.02.23