정말 많이 사용되지만 혼동하기 쉬운 Array.asList와 List.of에 대해 정리해보려 합니다.
불변 리스트
불변 리스트는 생성한 후 수정할 수 없는 리스트를 의미합니다. 말 그대로 불변성을 가진 리스트를 말하는데요.
불변 리스트는 값이 변경되지 않기 때문에 일반적인 리스트에 비해 안정성이 높습니다. 여러 쓰레드에서 동시에 리스트에 접근하더라도 읽기 작업만을 할 수 있기 때문에 동기화를 신경 쓸 필요가 없고, 의도치 않은 변경을 막을 수 있습니다.
java에서는 크게 3가지 방법을 이용해 불변 리스트를 만들 수 있습니다.
- Arrays.asList
- List.of
- unmodifiableList
순서대로 하나씩 사용 방법에 대해 알아봅시다.
가짜 불변 리스트: Arrays.asList
// 인자를 하나씩 전달
List<Number> asList = Arrays.asList(1, 2, 3);
// Number, Integer 배열 전달
Number[] array = {1, 2, 3};
List<Number> list = Arrays.asList(array);
Arrays.asList는 여러 개의 인자, 또는 배열을 받아 불변 리스트를 만듭니다.
이렇게 만들어진 리스트는 더 이상 값을 추가하거나 삭제할 수 없게 됩니다. 즉, 크기가 고정됩니다.
하지만 Arrays.asList에는 몇 가지 문제점이 있습니다.
첫 번째 문제점: 값 수정은 가능한데요?
그렇다면 값이 변경될 때는 어떨까요? Arrays.asList는 값의 변경을 자유롭게 할 수 있습니다.
Arrays.asList는 추가, 삭제와는 달리 변경에 대해서는 아무런 제약을 두지 않습니다.
List<Number> asList = Arrays.asList(1, 2, 3);
asList.add(1); // UnsupportOperationException
asList.set(0, 3); // 가능
크기만 변경할 수 없고, 값은 변경될 수 있기 때문에 Arrays.asList는 불변 리스트로 볼 수 없습니다.
두 번째 문제점 : 얕은 복사와 원본 배열의 참조
Number[] arr = {1, 2, 3};
List<Number> asList = Arrays.asList(arr);
arr[0] = 999;
System.out.println(asLsit.getFirst()); // 999
Arrays.asList는 내부적으로 깊은 복사가 아닌 얕은 복사를 이용합니다. 즉, 원본값이 변경되면 Arrays.asList의 값도 함께 변경되버립니다. 이는 의도하지 않은 동작을 초래할 수 있습니다.
Arrays 내부에 생성되는 부분을 보면 얕은 복사를 하고 있음을 확인할 수 있습니다.
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable{
private static final long serialVersionUID = ...;
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
@Override
public int size() {
return a.length;
}
@Override
public Object[] toArray() {
return Arrays.copyOf(a, a.length, Object[].class);
}
@Override
public Object[] toArray() {
int size = size();
if (a.length < size)
return Arrays.copyOf(this.a, size,
(Class<? extends T[]>) a.getClass());
System.arraycopy(this.a, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
}
기대했던 불변 리스트: List.of
// 인자를 하나씩 전달
List<Number> listOf = List.of(1, 2, 3);
// Integer, Number 전달
Number[] arr = {1, 2, 3};
List<Number> listOf = List.of(arr);
그렇다면 List.of는 어떨까요?
List.of는 Arrays.asList에서 발생할 수 있는 문제점을 모두 해결합니다.
먼저, List.of는 Arrays.asList와 달리 값의 추가, 삭제, 수정을 모두 제한합니다.
// List.of
List<Number> listOf = List.of(1, 2, 3);
listOf.add(1); // UnsupportOperationException
listOf.set(0, 3); // 불가능
두 번째로, asList에서 발생할 수 있는 원본 배열 참조 문제도 List.of를 사용하면 일어나지 않습니다.
이는 List.of의 내부 생성자를 통해 알 수 있는데요.
@SafeVarargs
static <E> List<E> listFromArray(E... input) {
// copy and check manually to avoid TOCTOU
@SuppressWarnings("unchecked")
E[] tmp = (E[])new Object[input.length]; // implicit nullcheck of input
for (int i = 0; i < input.length; i++) {
tmp[i] = Objects.requireNonNull(input[i]);
}
return new ListN<>(tmp, false);
}
List.of는 얕은 복사를 하는 asList와는 다르게 직접 값을 새로운 배열 변수에 대입해주기 때문에 참조값에 영향을 받지 않게 됩니다.
따라서, List.of는 진정한 의미의 불변 리스트라고 할 수 있습니다.
그럼 unmodifiableList는 뭘까?
Collections.unmodifiableList는 불변 리스트로 이해하는 것보다는 뷰로 이해하는 것이 좋습니다. 데이터베이스의 뷰를 생각하면 될 것 같습니다.
뷰를 통해서는 읽기만 가능하지만 참조하는 값이 변경되면 뷰의 내용도 변경됩니다. 즉, unmodifiableList도 우리가 생각하는 불변 리스트와는 거리가 있는 것을 알 수 있습니다.
실제 라이브러리에 있는 unmodifiableList의 설명을 보면 뷰를 제공해준다는 것을 확인할 수 있습니다.
지정된 목록에 대한 수정할 수 없는 뷰(view)를 반환합니다.
반환된 리스트에 대한 읽기 쿼리는 원본 배열에 대해 수행(read through)됩니다. 반환된 목록을 직접적으로 또는 반복자를 통해 수정하려고 하면 지원되지 않는 OperationException이 발생합니다.
...(이하 생략)
결론
불변 리스트를 사용해야 하는 경우에는 List.of를 사용하는 것이 맞습니다.
값의 변경이 일어나면 안되는 상황에서는 Arrays.asList가 아닌 List.of를 사용해야 합니다.
Arrays.asList는 원본 배열을 계속해서 참조하고 있기 때문에 반드시 이를 주의하고 사용해야 합니다.
Collections.unmodifiableList는 불변 리스트로 이해하는 것보다는 리스트에 대한 뷰로 이해하는 것이 좋습니다. 읽기만 가능하지만, 원본 배열이 수정되면 뷰도 함께 수정됩니다.
'language > java' 카테고리의 다른 글
Lombok의 @Builder와 @SuperBuilder (0) | 2025.01.23 |
---|---|
Java Map의 동시성 문제 (HashMap vs. ConcurrentHashMap vs. Hashtable) (0) | 2025.01.18 |