본문 바로가기
Languages/Java

자바 8에서 Arrays.asList() 를 완벽한 불변리스트로 사용하기

by yoon_seon 2025. 1. 27.

회사에서 운영 중인 서비스는 자바 8 버전을 사용하고 있습니다.

때문에 정적 리스트를 생성할 때 자바 9에서 추가된 List.of()를 사용하지 못하고, 대신 Arrays.asList()를 사용하고 있는데요.

하지만 완벽한 불변을 보장하는 List.of()와 달리 Arrays.asList()는 완벽한 불변을 지원하지 않습니다.

자바 8에서 List.of()를 대체할 수 있는 수준으로 Arrays.asList()를 완벽한 불변 리스트로 만드는 방법을 공유하고자 합니다.

 

Arrays.asList()를 불변 리스트로 만들기에 앞서 Arrays.asList()의 기본동작을 보며 완벽한 불변 리스트가 아닌지 살펴보겠습니다.

 

1. Arrays.asList()와 List.of()의 기본동작

Arrays.asList()의 동작

Arrays.asList()는 완벽한 불변 리스트가 아닌 반 불변 리스트(?)입니다. Arrays 클래스를 살펴보면 알 수 있습니다.

 

Arrays.asList()의 코드를 살펴보면, 파라미터로 전달된 인자를 기반으로 ArrayList를 반환합니다.

 

이때 반환되는 리스트는 java.util.ArrayList가 아닌, Arrays 클래스의 내부 클래스인 ArrayList입니다.

 

이 내부 클래스의 구현을 살펴보면, 생성자에서 전달된 배열을 내부에 선언된 배열에 복사하여 관리하고 사용하고 있다는 것을 알 수 있습니다.

즉, Arrays.asList()깊은 복사(Deep Copy)가 아닌, 얕은 복사(Shallow Copy)를 수행하기에 리스트의 크기는 변경할 수 없지만, 내부의 요소를 변경하는 것은 가능합니다.

 

아래와 같이 내부의 요소가 변경 가능한 것을 확인할 수 있습니다.

 

 

Arrays.asList가 반환하는 리스트의 add(), remove()set() 메서드를 보면 더 이해하기 쉽습니다.

 

Arrays 클래스의 내부 클래스인 ArrayList는 AbstractList를 상속하고 있습니다.

set() 메서드는 ArrayList 내부에 정의되어 있고, add()remove() 등의 메서드는 AbstractList에 정의되어 있는데요.

내부 구현을 보면, set() 메서드는 내부 배열에 직접 접근하여 요소를 변경합니다.

반면, 리스트의 크기를 변경하는 add()remove() 등의 메서드는 UnsupportedOperationException을 발생시키는 것을 알 수 있습니다.

 

즉, 리스트의 크기는 변경할 수 없지만, 내부의 요소를 변경하는 것은 가능합니다.

 

아래와 같이 내부의 요소가 변경하지만 리스트의 크기를 변경할 시 예외가 발생하는 것을 확인할 수 있습니다.

 

Arrays.asList() 정리

  • 자바 1.2 버전 이상 부터 지원
  • 리스트 크기 변경 X
  • 내부 요소 변경 O(배열을 사용하며 얕은 복사를 수행하며 내부 요소 변경 시 완벽한 불변이 보장되지 않음)

 

이어서 List.of() 의 기본 동작을 알아보겠습니다.

List.of()의 동작

List.of()는 ImmutableCollections 클래스를 반환합니다.

 

List.of()에 전달된 요소의 크기가 2 이하일 경우

ImmutableCollections 내부의 Inner Class의 생성자를 이용하고 있습니다.

 

이어서, ImmutableCollections 내부의 또 다른 내부 클래스인 AbstractImmutableList를 살펴보면, 이 클래스는 리스트를 변경하는 모든 메서드를 호출할 때 UnsupportedOperationException을 발생시키도록 구현되어 있습니다.

 

List.of()에 전달된 요소의 크기가 3 이상일 경우

새로운 배열에 얕은 복사(Shallow Copy) 후 ListN이라는 Inner Class의 생성자로 전달합니다.

 

Inner Class가 AbstractImmutableList를 상속받기에 얕은 복사를 수행했음에도 불구하고, 리스트를 변경하는 모든 메서드를 호출할 때 UnsupportedOperationException가 발생하게됩니다.

따라서 List.of()는 배열을 변형하지 않으며, 복사된 배열을 기반으로 불변 리스트를 안전하게 반환합니다.

 

아래와 같이 내부의 요소와 리스트의 크기를 변경할 시 예외가 발생하는 것을 확인할 수 있습니다.

 

List.of() 정리

  • 자바 9 이상부터 지원
  • 리스트 크기 변경 X
  • 내부 요소 변경 X(배열을 사용하며 얕은 복사를 수행하지만 리스트 내부 접근 시 예외를 발생 시키기에 완벽한 불변을 보장함)

 

1. Arrays.asList()를 Collections.unmodifiableList로 불변 리스트 만들기

자바 8에서는 List.of()를 사용할 수 없기 때문에, Arrays.asList()로 생성된 리스트를 불변 리스트로 만들기 위해 Collections.unmodifiableList()를 사용할 수 있습니다. 

 

Collections.unmodifiableList()는 UnmodifiableList를 반환하고 있습니다.(UnmodifiableRandomAccessList UnmodifiableList를 상속하고있음)

 

UnmodifiableList는 리스트 요소를 변경하는 set() 메서드 호출 시 UnsupportedOperationException를 반환합니다.

 

따라서 Arrays.asList()를 Collections.unmodifiableList() 로 래핑하면 리스트 내부의 요소 변경도 방지할 수 있습니다.

 

❗️❗️

그러나, Arrays.asList() 자체는 얕은 복사를 수행하기 때문에 원본 리스트가 수정되면 Collections.unmodifiableList()로 반환된 리스트에도 그 변경이 반영됩니다. 즉, 리스트 내부의 요소 변경은 방지되지만, 원본 리스트의 수정으로 인한 참조 변경은 여전히 유효합니다.

 

 

3. 완벽한 불변 리스트 만들기: new ArrayList<>(args)와 unmodifiableList 결합

2번에서의 문제는 Collections.unmodifiableList()가 Arrays.asList가 원본 리스트의 참조를 가지고 있었기 때문이었습니다.

 

완벽한 불변 리스트를 만들기 위해서는 Arrays.asList()로 생성된 리스트를 복사한 후, 그 복사본을 Collections.unmodifiableList()로 감싸는 방법을 사용할 수 있습니다.

 

🎉 new ArrayList<>(Arrays.asList(...))는 args 배열을 복사하여 새로운 리스트를 만들고, Collections.unmodifiableList로 래핑하므로 써, 참조에 인한 변경, 리스트 직접 변경을 완벽히 방지하여 불변 리스트로 만들 수 있습니다.

 

Arrays.asList()List.of() 모두 얕은 복사를 사용하기 때문에, 원본 배열이 객체 배열인 경우, 이들을 unmodifiableList로 감싸면 리스트는 불변이지만, 객체 참조는 그대로 유지됩니다. 즉, 원본 배열이나 객체의 내부 상태가 변경되면, unmodifiableList에 포함된 요소도 변경될 수 있습니다.
이번 포스팅에서는 Arrays.asList()List.of()처럼 사용하고자 하는 목적을 가지고 다루고 있기에 해당 부분은 감안 했습니다.

 

 

저는 회사에서 List.of()를 대체할 목적으로 유틸 클래스에 함수를 만들어 하여 Arrays.asList()를 완벽한 불변으로 만들어서 사용하고 있습니다.

public class CollectionUtils {
    
    private CollectionUtils() {
        throw new UnsupportedOperationException();
    }
    
    public static <E> List<E> listOf(E... origin) {
        return Collections.unmodifiableList(new ArrayList<>(Arrays.asList(origin)));
    }
    
    // ...
}

 

물론 성능적으로 List.of() 보다는 떨어지지만, 자바 8 버전에서 List.of()를 대체할 목적으로는 좋은 대안 이었다고 생각합니다. 😅

 

 

끝.

 

댓글