List와 Set의 차이는 기본 중에 기본이다.
범죄 신고는 112, 화재 신고는 119 같은 기본 중에 기본이다..
Set은 데이터 중복을 허용하지 않고 순서도 보장하지 않는다.
- Set은 Hash table로 구현되는데, 그 이유는 ha h table이 Set과 같은 특징을 갖고 있기 때문이다.
Hash table에서는 Key가 중복될 수 없다. 그리고 데이터는 순차적이 아니라 랜덤하게 저장된다. 이런 hash table의 특징이 Set의 특징과 일치한다. 따라서 Set을 구현할 때는 Hash table의 Key에 데이터를 저장하는 형태로 구현하게 된다.
O(1)인 Set은 얼마나 많은 데이터를 hash table에 저장하고 있는지와 상관없이 늘 빠르게 키를 검색할 수 있다.
따라서 데이터 검색은 List보다 Set에서 수행하는 것이 시간적으로 훨씬 유리하다.
먼저 List와 Set은 둘 다 여러 개의 데이터를 담는 컬렉션이지만, 동작 방식이 다르다.
먼저 List는 순서가 있는 리스트다. 예를 들어 ArrayList를 쓰면 데이터를 넣는 순서대로 유지되고, 중복도 허용된다. 그러니까 [A, A, B] 이렇게 넣으면 그대로 [A, A, B]로 남는다. 인덱스에 접근할 수 있어서 list.get(0)하면 첫 번째 요소인 A를 꺼낼 수 있다.
반면, Set은 중복을 허용하지 않는 집합임. HashSet을 예로 들면 [A, A, B]를 넣어도 [A, B]만 남고, 기본적으로 순서도 보장 안된다. 넣은 순서대로 안 나올 수 있다. 대신 중복 체크가 빠르고, '이게 있는지?'를 확인할 때 유용하다. 예를 들면 Set은 생일 파티 초대 리스트다. '철수, 철수, 영희'라고 써도 철수는 한 명만 오고, 철수와 영희 중 누가 먼저 오는지는 보장되지 않는다.
List<String> list = new ArrayList<>();
list.add("A");
list.add("A");
System.out.println(list); // [A, A]
Set<String> set = new HashSet<>();
set.add("A");
set.add("A");
System.out.println(set); // [A]
중요한 이유 : 성능, 의도 명확성, 버그 예방
상황에 맞는 최적의 도구를 골라서 성능을 높이고, 코드를 읽기 쉽게 만들고, 문제를 미리 막는 데 실질적인 도움이 되기 때문에 중요하다.
1. 성능 측면
List는 중복을 허용하고 순서를 유지하니까, 예를 들어 ArrayList를 쓰면 인덱스 접근이 O(1)이니까 순차 접근이나 순서가 중요한 데이터 처리에 유리하다.
반면 Set, 특히 HashSet은 중복을 제거하고 검색(contains)이 O(1)이라 고유한 데이터 집합을 다룰 때 효율적이다.
실무에서 데이터 크기가 커질수록 잘못된 선택은 성능 병목을 만들 수 있어서, 예를 들어 중복 제거가 필요 없는 상황에서 Set을 사용하면 불필요한 해시 연산 오버헤드가 생길 수 있고, 반대로 중복 체크가 필요한데 List를 사용하면 루프를 돌며 확인해야 해서 비효율적일 수 있다.
List는 순서대로 접근하거나 중복 데이터가 필요할 때 좋다. 예를 들어, 주문 내역을 순서대로 보여줄 때 ArrayList를 사용하면 빠르게 인덱스로 접근 가능하다. 근데 Set은 중복 제거나 빠른 검색이 필요할 때 강력하다. 또 유저 ID목록에서 중복 없애려면 HashSet이 훨씬 효율적이다. 이런 자료구조를 잘못 고르면 데이터가 많을 때 느려질 수 있다.
2. 코드 명확성 측면
List를 쓰면 '순서가 있고 중복이 있을 수 있다.'라는 의도가 드러나야 하고, Set을 쓰면 '중복이 없어야 한다.'는 요구사항을 명확히 전달한다.
예를 들어, 책 목록을 순서대로 보여줘야 한다면 List<String>이 자연스럽고, 고유한 태그 집합을 관리한다면 Set(String)이 직관적이다.
자료구조 선택 하나로 설계 의도를 파악할 수 있으니 유지보수성이 높아진다.
내가 List를 쓰는지 Set을 쓰는지 보면 코드로 뭘 하려는지 바로 보인다. List<Map>으로 주문 데이터를 담으면 '순서가 있고 중복이 있을 수 있다.' 는 거고, Set<String> 으로 태그를 담으면 '고유한 값만 필요하다.'는 의도가 명확해진다. 나중에 코드를 읽을 때 고민을 덜 하게 된다.
3. 버그 예방 측면
List를 썼는데 중복을 의도치 않게 허용해서 데이터가 꼬일 수 있고, Set을 썼는데 순서가 중요한 로직에서 순서가 뒤섞여져서 예상 밖 결과가 나올 수도 있다. 예를 들어, 주문 목록을 Set으로 관리하면 동일 주문이 사라질 수 있고, 유저 ID를 List로 관리하면 중복 체크를 깜빡해서 문제가 생길 수 있다.
이 차이를 알면 요구사항에 맞는 구조를 선택해서 그런 실수를 줄일 수 있다.
요구사항을 잘못 이해하면 버그가 발생한다. 예를 들어, 중복되면 안되는 데이터를 List로 관리하면 중복 체크를 따로 해야하고 실수할 가능성이 커진다. 반대로 순서가 중요한 걸 Set으로 하면 출력 순서가 엉망이 될 수 있다.
사용 예 :
- API 응답에서 고유 ID만 뽑을 때 HashSet 사용
- List<Map> 사용 중 중복 제거가 필요하면 Set<Map>으로 변경
만약 도서 목록 데이터를 받아서 처리하는 API가 있을 때,
그걸 List<Map<Integer, String>>으로 받았다면, 이는 순서대로 보여주고 중복 책도 허용하기 위해서 그랬다고 해석할 수 있다.
반대로 도서 태그를 고유하게 관리해야 한다면 Set<String>으로 바꿔서 중복 태그를 없애고 빠르게 확인할 수 있게 하면 된다.
이런 선택이 서비스 성능과 코드 품질에 직접 영향을 주므로 아는 것이 중요하다...
ArrayList
동적배열로 객체 참조를 배열에 순차적으로 저장함.
기본 크기로 시작해 필요시 크기를 확장함 (현재 크기의 1.5배)
(10 → 15 → 22 → 33 → 49 -> 73 -> 109)
이렇게 확장할 때마다 Arrays.copyOf()로 기존 배열의 값을 새 배열에 복사함.
확장시켜 새로운 배열이 생성되면, 기존 배열에 대한 참조는 더이상 유지되지 않고
가비지 컬렉터의 대상이 되어 메모리에서 제거됨.
ex: 쿼리 결과로 100개 로우가 조회되고, 이를 리스트에 담으면
1. 초기 용량: 10
2. 11번째 요소 추가 시도 → 용량 부족 → 확장 (10 → 15)
3. 16번째 요소 추가 시도 → 용량 부족 → 확장 (15 → 22)
4. 23번째 요소 추가 시도 → 용량 부족 → 확장 (22 → 33)
5. 34번째 요소 추가 시도 → 용량 부족 → 확장 (33 → 49)
6. 50번째 요소 추가 시도 → 용량 부족 → 확장 (49 → 73)
7. 74번째 요소 추가 시도 → 용량 부족 → 확장 (73 → 109)
이렇게 총 6번의 Arrays.copyOf() 호출이 발생
배열 복사는 성능에 영향을 줄 수 있음. 만약 최종 크기를 미리 알고 있다면, ArrayList 생성 시 초기 용량을 지정하는 것이 효율적
// 처음부터 충분한 용량으로 생성
ArrayList<Row> rows = new ArrayList<>(100);
// 이렇게 하면 확장이 필요 없어 Arrays.copyOf() 호출이 없음
for (int i = 0; i < 100; i++) {
rows.add(new Row(...));
})
HashSet
내부적으로 HashMap을 사용해 구현.
추가되는 요소는 HashMap의 키로 저장되고, 값에는 모든 요소가 공유하는 더미 객체가 들어감.
Set의 중복 방지 특성은 HashMap의 키 중복 불허용 특성을 활용해 자연스럽게 구현됨.
'기타' 카테고리의 다른 글
Vim 마스터의 길... (0) | 2025.03.03 |
---|---|
순서복잡도 (Time Complexity) (0) | 2025.03.02 |
어떻게 공부할 것인가.... 1 (0) | 2025.02.11 |
Deque (1) | 2025.01.16 |
Redis의 Sorted Set이 조회 횟수를 저장하는 원리 (검색 순위) (0) | 2025.01.12 |