collection 복사 시 구현 형태를 유지하고 싶으면 copyOf를 쓰지 말자

Jihoon Oh·2022년 3월 12일
1
post-custom-banner

자바를 쓰는 사람이라면 다들 알다시피, 기본적으로 Set과 Map은 자료의 순서를 보장하지 않는다. 1, 2, 3 순서로 자료를 넣어도 3, 2, 1 순서로 출력이 될 지, 2, 3, 1 순서로 출력이 될지 모른다는 얘기다. 순서를 보장하고 싶다면 LinkedHashMap, LinkedHashSet과 같이 Linked 형태로 된 구현체를 사용해야 한다.

블랙잭 미션을 하다 보니 Map을 사용할 일이 있었다. 플레이어의 승,무,패 결과를 플레이어를 key로, 결과를 value로 넣은 뒤에 출력시에는 플레이어 순서대로 출력을 해주기 위해 LinkedHashMap을 사용했다. 그런데 결과 출력을 위해 view에다가 넘겨줘야 하니까 getter로 꺼낼 때는 불변의 참조가 끊긴 Map을 만들어주기 위해 Map.copyOf를 사용해서 넘겨주었다. LinkedHashMap을 복사해서 주니까 복사본도 LinkedHashMap일거라고 생각했다.

그런데 웬걸, view에서 Map을 받아서 출력을 해보니 순서가 뒤죽박죽. 매 번 출력시마다 순서가 달라졌다. 결국 라이브러리 코드를 직접 까보고 나서야 문제를 알았다.

Map.copyOf는 ImmutableCollections에서 자체적으로 정의된 MapN을 반환한다.

Map.copyOf는 인자로 들어온 Map이 LinkedHashMap이든, HashMap이든, EnumMap이든 ImmutableCollections.MapN 이라는 자료형을 반환한다. (빈 맵이면 EmptyMap을, entry가 1이면 Map1을 반환) 그런데, MapN은 LinkedHashMap처럼 순서를 보장하지 않는다.

따라서 LinkedHashMap을 copyOf로 복사해서 반환해주면 LinkedHashMap을 유지하지 못하고 그냥 HashMap을 반환해 주는 것과 다름이 없어지는 것이다.

나는 결국 이 부분을 해결하기 위해서 귀찮은 작업을 해주어야 했다. 복사하고자 하는 Map을 먼저 LinkedHashMap 생성자에 다시 인자로 넣어주고, Collections.unmodifiableMap 메서드에 집어넣어서 참조가 끊어진 unmodifiable을 만들어 주는 방법으로 반환해 주었다.

Map<String, Integer> testMap = new LinkedHashMap<>();

return Collections.unmodifiableMap(new LinkedHashMap<>(testMap));

이렇게 하면 원본 참조를 끊고, 수정이 불가능하면서도 LinkedHashMap의 성질을 유지하고 있는 Map을 반환할 수 있다.

그런데 Collections.unmodifiableMap은 어떻게 LinkedHashMap의 성질을 유지할까?

답은 Collections 객체의 내부 코드를 직접 찾아보고 나서 찾게 되었다.

public class Collections {

    ...
    
    public static <K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m) {
        return new UnmodifiableMap<>(m);
    }
    
    private static class UnmodifiableMap<K,V> implements Map<K,V>, Serializable {
        ...

        private final Map<? extends K, ? extends V> m;

        UnmodifiableMap(Map<? extends K, ? extends V> m) {
            if (m==null)
                throw new NullPointerException();
            this.m = m;
        }
        
        public int size()                        {return m.size();}
        public boolean isEmpty()                 {return m.isEmpty();}
        public boolean containsKey(Object key)   {return m.containsKey(key);}
        public boolean containsValue(Object val) {return m.containsValue(val);}
        public V get(Object key)                 {return m.get(key);}

        public V put(K key, V value) {
            throw new UnsupportedOperationException();
        }
        public V remove(Object key) {
            throw new UnsupportedOperationException();
        }
        public void putAll(Map<? extends K, ? extends V> m) {
            throw new UnsupportedOperationException();
        }
        public void clear() {
            throw new UnsupportedOperationException();
        }
        
        ...
    }
    ...
}

Collections.unmodifiableMap은 자체적으로 구현된 UnmodifiableMap 이라는 구현체를 반환하게 되는데, 이 UnmodifiableMap 구현체는 copyOf 생성자로 받아오는 Map안의 요소를 가져와서 Map을 재구성하는 것이 아니라, 인자로 받는 Map의 참조를 필드로 저장하고, 필드로 저장된 Map의 수정 메서드들을 호출하지 못하도록 Map의 메서드들을 다시 implements 해준다. 그래서 UnmodifiableCollection이 참조를 끊지 못한다고 말했던 것이다.

다시 본론으로 돌아와서, 생성자에서 인자로 받는 Map을 그대로 필드에 저장하기 때문에, 해당 Map이 LinkedHashMap이든 EnumMap이든 또는 다른 어떤 Map이든 UnmodifiableMap 안에서 해당 Map의 메서드를 호출하면 되므로 그 성질을 그대로 사용할 수 있게 되는 것이다.

참고
설명은 Map으로 했지만 Map 뿐 아니라 다른 collection에 대해서도 똑같이 적용된다. 따라서 collection의 성질을 유지하면서 복사해야 할 일이 있다면 Set.copyOf를 사용하지 말도록 하자.

profile
Backend Developeer
post-custom-banner

1개의 댓글

comment-user-thumbnail
2023년 3월 10일

오늘 엄청 궁금했던내용인데 도움 많이얻어갑니다💪

답글 달기