변경 가능한 키 사용 피하기

변경 가능한 키를 사용하는 것은 패턴에 반하는 행위이므로 절대 피해야 합니다. 그렇게 할 경우 발생할 수 있는 부작용은 끔찍합니다. 결국 Map의 콘텐츠에 도달할 수 없게 될 수 있습니다.

이를 보여주기 위해 예제를 설정하는 것은 아주 쉽습니다. 다음은 String의 변경 가능한 래퍼인 Key 클래스입니다. equals()hashCode() 메서드는 IDE에서 생성할 수 있는 코드로 재정의되었음을 참고하세요.

//
// !!!!! This an example of an antipattern !!!!!!
// !!! do not do this in your production code !!!
//
class Key {
    private String key;
 
    public Key(String key) {
        this.key = key;
    }
 
    public String getKey() {
        return key;
    }
 
    public void setKey(String key) {
        this.key = key;
    }
 
    @Override
    public String toString() {
        return key;
    }
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Key key = (Key) o;
        return Objects.equals(this.key, key.key);
    }
 
    @Override
    public int hashCode() {
        return key.hashCode();
    }
}

이 래퍼를 사용하여 키-값 쌍을 넣을 Map을 만들 수 있습니다.

Key one = new Key("1");
Key two = new Key("2");
 
Map<Key, String> map = new HashMap<>();
map.put(one, "one");
map.put(two, "two");
 
System.out.println("map.get(one) = " + map.get(one));
System.out.println("map.get(two) = " + map.get(two));

지금까지 이 코드는 정상이며 다음과 같이 출력됩니다:

map.get(one) = one
map.get(two) = two

누군가 키를 변이시키면 어떻게 되나요? 변이에 따라 달라집니다. 다음 예시를 통해 값을 다시 가져오려고 할 때 어떤 일이 발생하는지 확인해 보세요.

다음 예에서는 기존 키 중 하나를 이미 존재하는 키와 일치하지 않는 새로운 값으로 변경하는 경우입니다.

one.setKey("5");
 
System.out.println("map.get(one) = " + map.get(one));
System.out.println("map.get(two) = " + map.get(two));
System.out.println("map.get(new Key(1)) = " + map.get(new Key("1")));
System.out.println("map.get(new Key(2)) = " + map.get(new Key("2")));
System.out.println("map.get(new Key(5)) = " + map.get(new Key("5")));

결과는 다음과 같습니다. 동일한 객체를 사용하더라도 더 이상 키에서 값을 가져올 수 없습니다. 또한 원래 값을 보유하고 있는 키에서 값을 가져오는 것도 실패합니다. 이 키-값 쌍이 손실됩니다.

map.get(one) = null
map.get(two) = two
map.get(new Key(1)) = null
map.get(new Key(2)) = two
map.get(new Key(5)) = null

키를 다른 기존 키에 사용되는 값으로 변경하면 결과가 달라집니다.

one.setKey("2");
 
System.out.println("map.get(one) = " + map.get(one));
System.out.println("map.get(two) = " + map.get(two));
System.out.println("map.get(new Key(1)) = " + map.get(new Key("1")));
System.out.println("map.get(new Key(2)) = " + map.get(new Key("2")));

이제 결과는 다음과 같습니다. 변이된 키에 바인딩된 값을 가져오면 다른 키에 바인딩된 값이 반환됩니다. 그리고 이전 예제에서와 마찬가지로 더 이상 변이된 키에 바인딩된 값을 가져올 수 없습니다.

map.get(one) = two
map.get(two) = two
map.get(new Key(1)) = null
map.get(new Key(2)) = two

아주 간단한 예시에서도 알 수 있듯이, 첫 번째 키는 더 이상 올바른 값에 액세스할 수 없으며 그 과정에서 값이 손실될 수 있습니다.

요컨대, 정말 변경 가능한 키 사용을 피할 수 없다면 변경하지 마세요. 하지만 최선의 선택은 변경 불가능한 키를 사용하는 것입니다.

 

HashSet의 구조 자세히 알아보기

이 섹션에서 HashSet 클래스에 대해 이야기하는 것이 왜 흥미로운지 궁금할 수 있습니다. 사실 HashSet 클래스는 내부의 HashMap 위에 구축된 것으로 밝혀졌습니다. 따라서 두 클래스는 몇 가지 공통된 기능을 공유합니다.

다음은 HashSet 클래스의 add(element) 코드입니다:

private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
 
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

보시다시피 HashSet은 실제로 객체를 HashMap에 저장합니다(transient 키워드는 관련이 없습니다). 객체는 이 HashMap의 키이며 값은 아무런 의미가 없는 객체, 즉 플레이스홀더에 불과합니다.

여기서 기억해야 할 중요한 점은 객체를 Set에 추가한 후 객체를 변경하면 애플리케이션에서 수정하기 어려운 이상한 버그가 발생할 수 있다는 것입니다.

변경 가능한 Key 클래스가 있는 이전 예제를 다시 살펴봅시다. 이번에는 이 클래스의 인스턴스를 Set에 추가하겠습니다.

Key one = new Key("1");
Key two = new Key("2");
 
Set<Key> set = new HashSet<>();
set.add(one);
set.add(two);
 
System.out.println("set = " + set);
 
// You should never mutate an object once it has been added to a Set!
one.setKey("3");
System.out.println("set.contains(one) = " + set.contains(one));
boolean addedOne = set.add(one);
System.out.println("addedOne = " + addedOne);
System.out.println("set = " + set);

이 코드를 실행하면 다음과 같은 결과가 생성됩니다:

set = [1, 2]
set.contains(one) = false
addedOne = true
set = [3, 2, 3]

실제로 첫 번째 요소와 세트의 마지막 요소가 동일하다는 것을 알 수 있습니다:

List<Key> list = new ArrayList<>(set);
Key key0 = list.get(0);
Key key2 = list.get(2);
 
System.out.println("key0 = " + key0);
System.out.println("key2 = " + key2);
System.out.println("key0 == key2 ? " + (key0 == key2));

이 마지막 코드를 실행하면 다음과 같은 결과를 얻을 수 있습니다:

key0 = 3
key2 = 3
key0 == key2 ? true

이 예제에서는 개체를 한 번 Set에 추가한 후 변경하면 동일한 개체가 이 Set에 두 번 이상 포함될 수 있다는 것을 보았습니다. 간단히 말해서 그렇게 하지 마세요!