도입
자바에서 데이터를 저장한다고 하면 많은 사람이 먼저 List를 떠올리지만, 실제 문제를 풀다 보면 “중복이 있으면 안 된다”는 조건이 훨씬 자주 등장합니다. 이때 가장 자연스럽게 사용하는 구조가 바로 Set입니다.
예를 들어 방문한 페이지 목록, 중복 없는 태그 모음, 중복 제거된 단어 집합, 이미 처리한 번호 목록 같은 데이터는 Set과 매우 잘 맞습니다. 그래서 Set은 단순한 저장소가 아니라, 중복 제거와 존재 여부 판단을 빠르게 처리하는 도구로 이해하는 것이 중요합니다.
정의
자바의 Set은 java.util.Set 인터페이스로 제공되며, 같은 값이 여러 번 들어오는 것을 허용하지 않는 컬렉션입니다.
즉, 이미 들어 있는 값과 같은 값을 다시 넣으려고 하면 새로운 원소가 추가되지 않습니다. 이 특성 때문에 Set은 “중복 없는 집합”을 표현할 때 가장 적합합니다.
- Set은 중복 저장이 불가능하다.
- Set은 “몇 번째”보다 존재 여부가 더 중요하다.
- Set은 중복 제거와 포함 여부 확인에 매우 강하다.
필요성
데이터를 다룰 때 자주 마주치는 요구사항 중 하나가 “이미 본 값인지”, “중복된 값은 제거하고 싶다”, “유일한 값만 모으고 싶다”는 것입니다. 이런 요구를 List로 처리하면 보통 반복 탐색이 들어가서 코드도 길어지고 비효율적이 됩니다.
반면 Set은 이런 상황을 위해 존재하는 구조이기 때문에, 중복 제거와 포함 여부 확인 문제를 훨씬 자연스럽고 빠르게 해결할 수 있습니다.
- 중복 제거가 매우 쉽다.
- 이미 존재하는지 확인하는 로직이 단순해진다.
- 방문 기록, 태그, 고유 값 관리 같은 문제와 잘 맞는다.
- 코딩테스트에서 중복 체크용으로 매우 자주 등장한다.
List와 차이
| 항목 | List | Set |
|---|---|---|
| 순서 | 중요 | 구현체에 따라 다름 |
| 중복 | 허용 | 불가 |
| 인덱스 접근 | 가능 | 불가 |
| 주요 목적 | 순서 있는 데이터 관리 | 중복 제거, 포함 여부 확인 |
따라서 “값을 저장한다”는 공통점만 보고 List와 Set을 비슷하게 생각하면 안 됩니다. List는 위치와 순서가 중심이고, Set은 유일성과 존재 여부가 중심입니다.
기본 사용법
import java.util.HashSet;
import java.util.Set;
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
set.add("apple"); // 중복 -> 추가되지 않음
System.out.println(set.size()); // 2
System.out.println(set.contains("apple")); // true
set.remove("banana");
System.out.println(set);
add(value): 값 추가contains(value): 값 존재 여부 확인remove(value): 값 삭제size(): 원소 개수 확인isEmpty(): 비어 있는지 확인clear(): 전체 삭제
주요 구현체
| 구현체 | 특징 | 순서/정렬 |
|---|---|---|
| HashSet | 가장 기본적이고 빠른 Set | 순서 보장 안 함 |
| LinkedHashSet | 삽입 순서 유지 | 입력 순서 보장 |
| TreeSet | 자동 정렬 | 정렬 순서 유지 |
가장 일반적으로 사용하는 Set 구현체입니다. 빠른 포함 여부 확인과 중복 제거가 중요할 때 기본 선택으로 많이 사용됩니다.
Set의 중복 제거 특성을 유지하면서도 입력 순서를 기억합니다. “중복은 제거하되 입력 순서는 유지해야 한다”는 상황에 잘 맞습니다.
저장할 때부터 값을 정렬된 상태로 유지합니다. 정렬된 유일 값 집합이 필요한 경우에 적합합니다.
내부 동작 원리
자바에서 가장 많이 쓰는 Set은 보통 HashSet입니다. HashSet은 내부적으로 해시 기반 구조를 활용해, 어떤 값이 이미 존재하는지 빠르게 검사합니다.
즉, Set의 “중복 허용 안 함”이라는 성질은 단순히 저장 전에 전부 비교하는 것이 아니라, 해시를 기반으로 효율적으로 판단하는 방식으로 구현됩니다.
equals와 hashCode
Set에 사용자 정의 객체를 넣을 때 가장 중요한 것은 “무엇을 같은 값으로 볼 것인가”입니다. HashSet은 이 판단을 위해 hashCode()와 equals()를 사용합니다.
따라서 두 객체가 논리적으로 같은 값을 뜻한다면, equals()와 hashCode()도 그 기준에 맞게 함께 재정의되어야 합니다.
import java.util.Objects;
public class Tag {
private final String name;
public Tag(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Tag)) return false;
Tag tag = (Tag) o;
return Objects.equals(name, tag.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
시간 복잡도
| 구현체 | 추가 | 포함 여부 확인 | 삭제 |
|---|---|---|---|
| HashSet | 평균 O(1) | 평균 O(1) | 평균 O(1) |
| LinkedHashSet | 평균 O(1) | 평균 O(1) | 평균 O(1) |
| TreeSet | O(log n) | O(log n) | O(log n) |
따라서 정렬이 필요 없고 빠른 중복 체크가 핵심이면 HashSet, 입력 순서를 기억하고 싶으면 LinkedHashSet, 자동 정렬이 필요하면 TreeSet이 자연스러운 선택입니다.
순회 방법
for (String value : set) {
System.out.println(value);
}
Set은 List처럼 get(0) 같은 인덱스 접근이 없기 때문에 “순서대로 꺼내는 구조”가 아니라 “집합 전체를 순회하거나 포함 여부를 확인하는 구조”로 이해하는 편이 맞습니다.
실전 사용 예시
- 중복 단어 제거
- 방문한 노드/정점 체크
- 이미 처리한 값인지 확인
- 태그 목록의 유일성 유지
- 중복 없는 사용자 ID 모음
Set<Integer> visited = new HashSet<>();
for (int num : nums) {
if (visited.contains(num)) {
System.out.println("중복 발견");
}
visited.add(num);
}
List / Map과의 관계
| 구조 | 핵심 목적 |
|---|---|
| List | 순서 있는 값 저장 |
| Set | 중복 없는 값 저장 |
| Map | 키로 값 찾기 |
즉, Set은 “값 하나하나를 유일하게 관리하는 구조”이고, Map은 “키를 이용해 값을 찾는 구조”라는 점에서 구분해야 합니다.
주의할 점
- Set은 인덱스 접근이 불가능하다.
- HashSet은 순서를 보장하지 않는다.
- 중복 판별 기준은 객체의 equals/hashCode에 달려 있다.
- 가변 객체를 넣고 내부 상태를 바꾸면 문제가 생길 수 있다.
- 단순 방문 체크라면 Set이 Map보다 더 적절한 경우가 많다.
디버깅
요약
- ✅ Set은 중복을 허용하지 않는다.
- ✅ 중복 제거와 포함 여부 확인 문제에 매우 강하다.
- ✅ 대표 구현체는 HashSet, LinkedHashSet, TreeSet이다.
- ✅ 순서가 필요 없으면 보통 HashSet부터 시작한다.
- ✅ 사용자 정의 객체를 넣을 때는 equals와 hashCode가 중요하다.