📚 학습 기록/Java 기초 & 중급

자바 Set 컬렉션 완전 정복: HashSet, LinkedHashSet, TreeSet 비교와 활용

zenjoydev 2025. 4. 24. 00:31

안녕하세요! 오늘은 자바 컬렉션 프레임워크의 중요한 인터페이스인 Set과 그 구현체들(HashSet, LinkedHashSet, TreeSet)에 대해 깊이 있게 알아보겠습니다. 중복 없는 데이터 집합이 필요할 때 유용한 Set 자료구조의 특징과 활용법, 그리고 내부 구현 원리까지 파헤쳐 보겠습니다.

목차

  1. Set 인터페이스 소개
  2. HashSet: 빠른 검색을 위한 선택
  3. LinkedHashSet: 순서가 중요할 때
  4. TreeSet: 정렬이 필요한 상황
  5. 트리 자료구조와 이진 탐색 트리
  6. Set 연산 활용하기
  7. Iterator로 Set 순회하기
  8. 성능 비교 및 선택 가이드

1. Set 인터페이스 소개

Set은 컬렉션 프레임워크에서 중복 저장이 없는 순서가 없는 자료 구조를 의미합니다. 수학의 집합 개념을 구현한 자료구조로, 주로 유일한 값의 집합이 필요할 때 사용합니다.

주요 메서드

  • add(E e): 중복 없이 요소를 추가합니다 (성공 시 true, 중복 시 false 반환)
  • addAll(Collection<? extends E> c): 지정된 컬렉션의 모든 요소를 추가합니다 (다중)
  • remove(Object o): 지정된 요소를 제거합니다
  • removeAll(Collection<?> c): 지정된 컬렉션의 모든 요소를 제거합니다
  • retainAll(Collection<?> c): 지정된 컬렉션에 포함된 요소만 유지하고 나머지는 제거합니다
  • contains(Object o): 지정된 요소가 포함되어 있는지 확인합니다
  • size(): Set의 크기를 반환합니다

2. HashSet: 빠른 검색을 위한 선택

HashSet은 해시 테이블을 사용하여 구현된 Set으로, 평균적으로 가장 빠른 검색 성능을 제공합니다.

특징

  • 구현: 해시 테이블(해시맵) 사용
  • 순서: 삽입 순서 보장하지 않음
  • 시간 복잡도: 대부분 O(1)의 성능
  • 용도: 유일성만 중요하고 순서는 중요하지 않을 때
  •  
public class JavaSetMain {
    public static void main(String[] args) {
        // HashSet 예제
        run(new HashSet<>());
    }

    private static void run(Set<String> set) {
        System.out.println("set = " + set.getClass());
        set.add("C");
        set.add("B");
        set.add("A");
        set.add("1");
        set.add("2");

        Iterator<String> iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next() + " ");
        }
        System.out.println();
    }
}

HashSet은 순서를 보장하지 않으므로, 출력 결과는 삽입 순서와 다를 수 있습니다.

커스텀 객체 사용 예시

HashSet에 커스텀 객체를 저장할 때는 equals()와 hashCode() 메서드를 올바르게 구현해야 합니다:

public class Rectangle {
    private int width;
    private int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public boolean equals(Object object) {
        if (object == null || getClass() != object.getClass()) return false;
        Rectangle rectangle = (Rectangle) object;
        return width == rectangle.width && height == rectangle.height;
    }

    @Override
    public int hashCode() {
        return Objects.hash(width, height);
    }

    @Override
    public String toString() {
        return "Rectangle{" +
                "width=" + width +
                ", height=" + height +
                '}';
    }
}
public class RectangleTest {
    public static void main(String[] args) {
        Set<Rectangle> rectangleSet = new HashSet<>();
        rectangleSet.add(new Rectangle(10,10));
        rectangleSet.add(new Rectangle(20,20));
        rectangleSet.add(new Rectangle(20,20));  // 중복값은 저장되지 않음

        for (Rectangle rectangle : rectangleSet) {
            System.out.println("rectangle = " + rectangle);
        }
    }
}

3. LinkedHashSet: 순서가 중요할 때

LinkedHashSet은 HashSet에 LinkedList를 결합한 형태로, 요소의 삽입 순서를 보존합니다.

특징

  • 구현: HashSet + LinkedList (이중 연결 리스트)
  • 순서: 삽입 순서 유지
  • 시간 복잡도: HashSet과 거의 동일 (약간의 오버헤드 있음)
  • 용도: 유일성과 삽입 순서 모두 중요할 때
  •  
public class UniqueNamesTest2 {
    public static void main(String[] args) {
        Integer[] inputArr = {30, 20, 20, 10, 10};
        LinkedHashSet<Integer> set = new LinkedHashSet<>();
        
        for (Integer i : inputArr) {
            set.add(i);
        }

        // 삽입 순서대로 출력됨
        Iterator<Integer> iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next() + " ");
        }
        System.out.println();
    }
}

LinkedHashSet은 삽입 순서를 유지하므로 30, 20, 10 순서로 출력됩니다.

4. TreeSet: 정렬이 필요한 상황

TreeSet은 이진 탐색 트리(Red-Black Tree)를 기반으로 구현되어 요소들을 정렬된 순서로 유지합니다.

특징

  • 구현: 이진 탐색 트리(레드-블랙 트리) 사용
  • 순서: 자연 순서(natural ordering) 또는 Comparator에 의한 정렬
  • 시간 복잡도: O(log n) - 이진 탐색 트리의 장점
  • 용도: 데이터의 정렬된 순서 유지가 필요할 때
public class UniqueNamesTest3 {
    public static void main(String[] args) {
        Integer[] inputArr = {30, 20, 20, 10, 10};
        
        Set<Integer> set = new TreeSet<>();
        for (int i = 0; i < inputArr.length; i++) {
            set.add(inputArr[i]);
        }
        
        // 정렬된 순서로 출력됨
        Iterator<Integer> iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next() + " ");
        }
        System.out.println();
    }
}

TreeSet은 요소를 정렬하므로 10, 20, 30 순서로 출력됩니다.

5. 트리 자료구조와 이진 탐색 트리

TreeSet이 내부적으로 사용하는 이진 탐색 트리에 대해 조금 더 깊이 알아보겠습니다.

트리 구조의 기본 개념

  • 트리: 부모와 자식 노드로 구성된 계층적 자료구조
  • 이진 트리: 각 노드가 최대 2개의 자식을 가질 수 있는 트리
  • 이진 탐색 트리: 왼쪽 자식 노드는 더 작은 값을, 오른쪽 자식 노드는 더 큰 값을 가지는 트리

레드-블랙 트리

TreeSet은 자가 균형 이진 탐색 트리인 레드-블랙 트리를 사용합니다. 이는 트리의 균형을 유지하여 최악의 경우에도 O(log n)의 시간 복잡도를 보장합니다.

  • 균형 유지 원리: 노드의 색(빨강/검정)을 관리하여 트리의 한쪽으로 치우치지 않게 함
  • 장점: 트리가 불균형해지면 자동으로 재구성되어 검색 효율성 유지
  • 사용 사례: 정렬이 중요한 데이터 관리, 범위 검색이 필요한 경우

6. Set 연산 활용하기

Set은 수학적 집합의 개념을 구현하므로, 합집합, 교집합, 차집합 등의 집합 연산을 수행할 수 있습니다.

public class SetOperationTest {
    public static void main(String[] args) {
        Set<Integer> set1 = new HashSet<>(List.of(1, 2, 3, 4, 5));
        Set<Integer> set2 = new HashSet<>(List.of(3, 4, 5, 6, 7));

        // 합집합
        Set<Integer> union = new TreeSet<>();
        union.addAll(set1);
        union.addAll(set2);
        System.out.println("합집합 : " + union);

        // 교집합
        TreeSet<Integer> intersection = new TreeSet<>(set1);
        intersection.retainAll(set2);
        System.out.println("교집합 : " + intersection);

        // 차집합
        TreeSet<Integer> difference = new TreeSet<>(set1);
        difference.removeAll(set2);
        System.out.println("차집합 : " + difference);
    }
}

실행 결과:

합집합 : [1, 2, 3, 4, 5, 6, 7]
교집합 : [3, 4, 5]
차집합 : [1, 2]

7. Iterator로 Set 순회하기

Set은 인덱스로 접근할 수 없으므로, 요소를 순회하기 위해 Iterator를 사용합니다.

public class JavaSetMain {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        set.add("Java");
        set.add("Python");
        set.add("JavaScript");
        
        // Iterator 사용 순회
        Iterator<String> iterator = set.iterator();
        while (iterator.hasNext()) {
            String element = iterator.next();
            System.out.println(element);
        }
        
        // 향상된 for문(for-each) 사용 순회
        for (String element : set) {
            System.out.println(element);
        }
    }
}

Iterator의 주요 메서드:

  • hasNext(): 다음 요소가 있는지 확인
  • next(): 다음 요소를 반환
  • remove(): 마지막으로 반환된 요소를 제거

8. 성능 비교 및 선택 가이드

각 Set 구현체의 성능을 비교하고, 언제 어떤 구현체를 사용해야 하는지 알아보겠습니다.

성능 비교

작업HashSetLinkedHashSetTreeSet

추가/검색/삭제 O(1) O(1) O(log n)
순서 유지 X O (삽입 순서) O (정렬 순서)
메모리 사용량 낮음 중간 높음
반복 성능 무작위 삽입 순서 정렬 순서

선택 가이드

  • HashSet: 단순히 중복을 제거하고 빠른 검색이 필요한 경우
  • LinkedHashSet: 중복 제거와 함께 삽입 순서를 기억해야 할 경우
  • TreeSet: 중복 제거와 함께 항상 정렬된 상태를 유지해야 할 경우

최적화 팁

  1. 초기 용량 지정: 대략적인 요소 수를 알고 있다면 초기 용량을 지정하여 재해싱 횟수 줄이기
  2. 재해싱 이해: 데이터가 해시 테이블 크기의 75%를 초과하면 테이블 크기가 2배로 증가하고 재해싱이 발생
  3. equals()와 hashCode() 올바르게 구현: 커스텀 객체를 저장할 때 필수
  4. 불변 객체 사용: 해시 기반 컬렉션의 키로는 불변(immutable) 객체 사용 권장
  5. 실무 선택: 일반적으로 HashSet을 기본으로 사용하고, 요구사항에 따라 다른 구현체 선택

결론

자바의 Set 컬렉션은 중복 없는 데이터 집합을 관리하는 다양한 방법을 제공합니다. 각 구현체는 서로 다른 특성과 성능 특징을 가지고 있으므로, 상황에 맞는 최적의 구현체를 선택하는 것이 중요합니다.

  • HashSet: 빠른 검색이 필요할 때
  • LinkedHashSet: 요소의 삽입 순서가 중요할 때
  • TreeSet: 정렬된 데이터가 필요할 때

Set 인터페이스를 제대로 이해하고 활용한다면, 더 효율적이고 깔끔한 코드를 작성할 수 있을 것입니다. 특히 데이터의 유일성이 중요한 비즈니스 로직(예: 사용자 ID 관리, 중복 제거 등)에서 Set 컬렉션은 필수적인 도구가 될 것입니다.


이 글이 자바의 Set 컬렉션에 대한 이해를 높이는 데 도움이 되셨나요? 궁금한 점이나 추가적인 내용에 대한 질문이 있으시면 댓글로 남겨주세요! 여러분의 피드백과 질문은 더 나은 콘텐츠를 만드는 데 큰 도움이 됩니다. 😊