ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Set에 대하여 1 : 구조와 주요 메서드
    개발 나누고 더하기/자바, 스프링 2023. 6. 23. 23:39

     Set은 내가 좋아하는 자료구조이다. 중복없이 원소들을 담아놓은 집합체이고, 용도도 중복 검사로 주로 쓰이는 아주 군더더기 없는 구성이라 뭔가 깔끔하다. 그런데 이번에 Set을 org.springframework.cache.annotation.Cacheable에 사용하다 버그를 만들어냈다. 왜 그렇게 생각했는지 모르겠는데 레디스가 Set을 지원하니 key에 사용되는 타입도 Set을 지원할 줄 알았다. 하지만 Key는 정직하게 스트링 값으로만 비교를 했고, 의도치 않게 여러가지 버전의 캐시를 만들어 내고 말았다. 대략 아래 코드와 같았다.

    args를 키 인자로 받는 Cacheable 코드

    List로 할까 고민했었는데, 좀 안일했었다. 로컬과 개발(인스턴스 2대) 환경에서 테스트해도 문제가 없었지만 운영환경에서 몇십대(정확한 대수는 공개하기 민감하므로^^)의 인스턴스에서 만들어내는 Set들은 원소는 동일하더라도 순서가 제각각이었다. 그리고 Redis에서는 이 제각각인 순서의 Set을 다 다른 값으로 인식하였다. 문제 해결은 순서가 보장되는 TreeSet으로 바꾸어 간단하게 풀렸다. 

    Redis는 동작방식을 배웠다 치고, Set은 아직 더 공부할게 많은 것 같아 이번에 자세히 알아보겠다.

     

    본격적으로 Set 파헤치기


    Set의 상속구조와 주요 Set 구현체들

    주요 메서드


    int size()

    Set에 저장되어 있는 원소들의 개수를 반환한다. 원소들이 Integer의 최댓값(2의31승 -1)보다 많으면 Integer.MAX_VALUE를 반환한다. 이는 Collection을 상속받는 모든 타입에 대해 적용된다. 그럼 Set, List 같은 자료구조는 Integer 범위를 넘어가는 수량은 저장할 수 없는 것일까? 궁금해서 실험을 해보았다.

    Set에 대량의 원소들 넣고 size 확인

    명세상으로는 사이즈 제한이 없어 Chat GPT에 물어보니 역시 제한은 없지만 구현체별로 제한은 있을 수 있다고 한다. 그래서 위처럼 Integer.Max보다 많은 원소들을 넣어보려고 했으나 OutOfMemoryError로 확인할 수 없었다. 아무튼 어찌어찌해서 엄청난 양의 원소들이 저장되었다 해도 size() 메서드로는 최대 Integer.MAX_VALUE까지만 그 수를 확인할 수 있다.

     

    isEmpty()

    컬렉션 안의 원소들이 없는지 검사한다. 비어 있으면 true, 1개라도 있으면 false를 반환한다.

    HashSet을 예로 들어보자.

    public class HashSet<E>
        extends AbstractSet<E>
        implements Set<E>, Cloneable, java.io.Serializable
    {
        @java.io.Serial
        static final long serialVersionUID = -5024744406713321676L;
    
        private transient HashMap<E,Object> map;
        
        //...
        
        public boolean isEmpty() {
            return map.isEmpty();
        }
        
        //...
    }

    HashSet은 내부적으로 HashMap에 원소들을 저장하는데, 이 map이 비어있는지 검사하고

    public class HashMap<K,V> extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable {
        
        //...
        
        /**
         * The number of key-value mappings contained in this map.
         */
        transient int size;
        
        //...
        
        /**
         * Returns {@code true} if this map contains no key-value mappings.
         *
         * @return {@code true} if this map contains no key-value mappings
         */
        public boolean isEmpty() {
            return size == 0;
        }
        
        //...
    }

    HashMap의 isEmpty는 내부에 저장하고 있는 원소들의 개수를 나타내는 변수인 size값이 0인지 아닌지를 비교해 boolean값으로 반환해준다.

    다른 Set 구현체는 아래에서 조금 더 다뤄보자.

     

    boolean contains(Object o)

    인자로 받은 원소를 Set이 포함하고 있는지 여부를 반환한다.

     

    Iterator<E> iterator()

    원소들의 Iterator를 순서없이 반환한다.

     

    boolean add(E e)

    Set에 원소를 추가한다. 만약 이미 들어가있는 원소 중 Objects.equals로 같은 원소가 있다면 추가되지 않는다. 반환되는 boolean 값은 인자로 넣은 원소가 추가되었는지 여부다. 바꾸어 말하면 중복된 원소가 없었는지 여부다. Set에 어떤 원소를 중복검사해서 넣을땐 그냥 add로 쓰면된다. contains로 굳이 확인할 필요가 없다.

     

    boolean remove(Object o)

    인자로 전달된 원소를 Set에서 제거한다. add와 마찬가지로 Objects.equals로 true가 나오는 원소를 제거한다. 제거된 원소가 있다면 true를 반환하고, 없으면 false를 반환한다.

     

    boolean containsAll(Collection<?> c)

    인자로 받은 Set의 원소들을 다 포함하고 있는지 즉, subSet인지 여부를 알려준다. 아래와 같이 ["a", "b", "c", "d"]와 ["a", "b", "c"]는 포함관계이지만, ["a", "b", "c", "z"]는 포함관계가 아니다.

    containsAll 예시

    boolean addAll(Collection<? extends E> c)

    인자로 받은 Set의 원소들을 추가한다. 즉, add작업을 컬렉션 단위로한다. 추가된 원소가 있으면 true를 없으면 false를 반환한다. 아래와 같이 ["a", "b", "c", "d"]에 ["a", "b", "c"]를 추가하면 이미 있는 원소들이라 아무런 변화가 없고 false를 반환한다. ["a", "b", "c", "z"]를 addAll하면 "z"가 추가되므로 true를 반환한다.

    addAll 예시

    boolean retainAll(Collection<?> c)

    전달받은 컬렉션 원소들만 남기고 모두 제거한다. 제거된 원소가 있으면 true를 아니면 false를 반환한다. A집합과 B집합의 교집합을 구하고자 할 때 사용할 수 있다. ["a", "b", "c", "d"]에서 ["a", "b", "c", "d", "e"]를 빼면 비록 뒤에 있는 집합의 크기가 클지라도 앞에 있는 집합 기준으로는 변화가 없어 false가 반환된다. 물론 앞 기준이기에 "e"원소는 들어가지 않는다.  ["a", "b", "c", "z"]를 빼면 "d"가 앞과 뒤에서 중복되지 않으므로 빠지고 true를 반환한다.

    retailAll 예시

    boolean removeAll(Collection<?> c)

    컬렉션으로 전달 받은 원소들을 모두 제거한다. 제거된 원소가 있으면 true를 아니면 false를 반환한다.retainAll이 교집합을 구하는 것이라면 removeAll은 차집합을 구하는 것이다.  ["a", "b", "c", "d"]에서  ["e", "f"]를 빼면 뺄게 없으므로 false이다.  ["a", "b"]를 빼면 "a", "b"가 제거되므로 true를 반환한다.

    removeAll

    retainAll과 removeAll을 구현한 것을 보면 중복 원소를 검사하는 한 글자 빼고 모두 동일하다.

        public boolean removeAll(Collection<?> c) {
            Objects.requireNonNull(c);
            boolean modified = false;
            Iterator<?> it = iterator();
            while (it.hasNext()) {
                if (c.contains(it.next())) {
                    it.remove();
                    modified = true;
                }
            }
            return modified;
        }
    
        public boolean retainAll(Collection<?> c) {
            Objects.requireNonNull(c);
            boolean modified = false;
            Iterator<E> it = iterator();
            while (it.hasNext()) {
                if (!c.contains(it.next())) {
                    it.remove();
                    modified = true;
                }
            }
            return modified;
        }

    void clear()

    Set의 모든 원소를 제거한다.

     

    static <E> Set<E> of(E... elements)

    인자로 받은 원소들로 불변 Set을 만들어주는 정적 팩토리 메서드이다. null인 원소를 허용하지 않아 null이 들어오면 NullPointerExcetpion이 발생하므로 주의하자.

    Set.of의 인자로 Null이 들어갔을 경우 예시

    <E> Set<E> copyOf(Collection<? extends E> coll)

    Set을 복사해주는 정적 팩토리 메서드이다. 만약 A라는 Set을 복사해 B라는 Set을 만들었다고 치자. 그럼 A와 B는 동등함을 보장한다. 하지만 그 이후 A에 어떤 원소가 추가되었을 때에는 B에도 전이되지 않으므로 동등성이 깨진다.

    copyOf 예시

    주요 메서드만 공부하는데에도 시간이 꽤 오래걸리고 글이 길어졌다. 다음에는 주요 구현체들별 특징과 차이점을 알아봐야겠다.

     

     

    댓글

Designed by Tistory.