안녕하세요! 오늘은 자바 컬렉션 프레임워크의 중요한 인터페이스인 Set과 그 구현체들(HashSet, LinkedHashSet, TreeSet)에 대해 깊이 있게 알아보겠습니다. 중복 없는 데이터 집합이 필요할 때 유용한 Set 자료구조의 특징과 활용법, 그리고 내부 구현 원리까지 파헤쳐 보겠습니다.
목차
- Set 인터페이스 소개
- HashSet: 빠른 검색을 위한 선택
- LinkedHashSet: 순서가 중요할 때
- TreeSet: 정렬이 필요한 상황
- 트리 자료구조와 이진 탐색 트리
- Set 연산 활용하기
- Iterator로 Set 순회하기
- 성능 비교 및 선택 가이드
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: 중복 제거와 함께 항상 정렬된 상태를 유지해야 할 경우
최적화 팁
- 초기 용량 지정: 대략적인 요소 수를 알고 있다면 초기 용량을 지정하여 재해싱 횟수 줄이기
- 재해싱 이해: 데이터가 해시 테이블 크기의 75%를 초과하면 테이블 크기가 2배로 증가하고 재해싱이 발생
- equals()와 hashCode() 올바르게 구현: 커스텀 객체를 저장할 때 필수
- 불변 객체 사용: 해시 기반 컬렉션의 키로는 불변(immutable) 객체 사용 권장
- 실무 선택: 일반적으로 HashSet을 기본으로 사용하고, 요구사항에 따라 다른 구현체 선택
결론
자바의 Set 컬렉션은 중복 없는 데이터 집합을 관리하는 다양한 방법을 제공합니다. 각 구현체는 서로 다른 특성과 성능 특징을 가지고 있으므로, 상황에 맞는 최적의 구현체를 선택하는 것이 중요합니다.
- HashSet: 빠른 검색이 필요할 때
- LinkedHashSet: 요소의 삽입 순서가 중요할 때
- TreeSet: 정렬된 데이터가 필요할 때
Set 인터페이스를 제대로 이해하고 활용한다면, 더 효율적이고 깔끔한 코드를 작성할 수 있을 것입니다. 특히 데이터의 유일성이 중요한 비즈니스 로직(예: 사용자 ID 관리, 중복 제거 등)에서 Set 컬렉션은 필수적인 도구가 될 것입니다.
이 글이 자바의 Set 컬렉션에 대한 이해를 높이는 데 도움이 되셨나요? 궁금한 점이나 추가적인 내용에 대한 질문이 있으시면 댓글로 남겨주세요! 여러분의 피드백과 질문은 더 나은 콘텐츠를 만드는 데 큰 도움이 됩니다. 😊
'📚 학습 기록 > Java 기초 & 중급' 카테고리의 다른 글
자바 컬렉션 프레임워크 실전 가이드: Map과 Deque의 숨겨진 활용법 (0) | 2025.04.30 |
---|---|
자바 컬렉션 프레임워크 완전정복: Map, Stack, Queue 및 Deque (2) | 2025.04.29 |
자바 HashSet 완벽 가이드: equals()와 hashCode()의 중요성과 제네릭 활용법 (1) | 2025.04.22 |
자바 HashSet 완전 정복: 해시 알고리즘과 충돌 해결 전략 구현하기 (1) | 2025.04.21 |
자바 컬렉션 프레임워크: Set과 해시 알고리즘 이해하기 (1) | 2025.04.16 |