Ruby의 Set(집합)은 무엇일까요?
Set은 배열처럼 여러 요소를 저장하는 클래스입니다.
하지만 특정 상황에서 무려 10배 이상 빠른 성능을 발휘하는 특별한 특성들을 가지고 있습니다.
게다가 결정적인 차이점 하나가 더 있습니다.
Set에 담기는 모든 요소는 항상 중복 없이 유일함이 보장됩니다.
이번 Ruby 튜토리얼에서 배울 내용은 다음과 같습니다.
- Set을 언제, 어떻게 사용하면 가장 큰 효과를 얻을 수 있는지
- Set과 배열(Array)의 차이점
- 실무에서 유용하게 쓰이는 Set 메서드 목록
그럼 바로 시작해 보겠습니다!
Ruby Set 기본 예제
Set은 중복되지 않는 고유한 요소들의 목록을 만들 때 도움이 되는 Ruby 클래스입니다.
어떤 상황에서 유용한지 예를 들어 보겠습니다.
수많은 상품 목록을 다루고 있다고 가정해 봅시다.
그런데 이 목록에는 중복된 항목이 섞여 있고, 오직 고유한 상품들만 필요한 상황입니다.
이때 상품들을 Set에 넣으면, 별도의 추가 작업 없이도 Set이 알아서 상품 목록의 중복을 제거하고 유일성을 유지해 줍니다.
실제 코드로 확인해 보겠습니다.
require 'set'
products = Set.new
products << 1
products << 1
products << 2
products
# Set: {1, 2}
또 하나의 큰 장점은 요소 검색 속도가 매우 빠르다는 점입니다.
products.include?(1) # true
이렇게 빠른 이유는 검색이 상수 시간(O(1))에 수행되기 때문입니다.
Set vs Array – 두 자료구조의 차이 이해하기
그렇다면 이런 궁금증이 생길 수 있습니다.
Set과 배열은 정확히 어떤 점이 다를까?
가장 큰 차이는 Set은 인덱스로 요소에 직접 접근할 수 없다는 것입니다.
products[0] # undefined method `[]'
이것이 두 자료구조의 핵심적인 차이입니다.
하지만 걱정할 필요는 없습니다. Set은 언제든지 배열로 변환할 수 있기 때문입니다.
products.to_a # [1, 2]
결국 Set을 사용하는 진짜 이유는 다음 두 가지 특별한 특성 때문입니다.
include?를 통한 매우 빠른 조회 속도- 값의 중복 방지(유일성 보장)
이 두 가지가 필요하다면 Set을 사용하는 것이 확실한 성능 향상을 가져다줍니다. 더 이상 고유한 요소가 필요할 때마다 배열에 uniq를 호출할 필요도 없어집니다.
Set vs Array 벤치마크 비교
다음은 배열과 Set의 include? 메서드 성능 차이를 보여주는 벤치마크 결과입니다.
# Ruby 2.5.0 set include: 8381985.2 i/s array include: 703305.5 i/s - 11.92x slower
이러한 차이가 발생하는 이유는 배열은 모든 요소를 하나씩 전부 확인해야 하기 때문입니다.
100만 개의 요소를 가진 배열이라면 include?를 호출할 때마다 최악의 경우 100만 개의 요소를 일일이 검사해야 합니다.
반면 Set은 그럴 필요가 전혀 없습니다.
Ruby Set 주요 메서드와 연산자
Set 사이에서 수행할 수 있는 다양한 연산과 메서드들이 있습니다. 수학을 조금이라도 공부해 보셨다면 익숙하게 느껴질 겁니다.
합집합 연산자 (Union)
가장 유용한 Set 메서드 중 하나가 바로 합집합 연산자입니다.
products | (1..10)
# Set: {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
이 연산자는 두 집합의 고유한 요소들을 모두 하나의 집합으로 합쳐 줍니다.
참고로 이 연산자는 Set 클래스 객체뿐만 아니라 배열, 범위(Range), 해시(Hash) 같은 모든 Enumerable 객체에서도 동작합니다.
차집합 연산자 (Difference)
한 집합에서 다른 집합의 요소들을 제거하고 싶다면 차집합 연산자를 사용하면 됩니다.
예시는 다음과 같습니다.
products - (3..4)
# Set: {1, 2, 5, 6, 7, 8, 9, 10}
교집합 연산자 (Intersection)
그리고 마지막으로 교집합 연산자가 있습니다.
Set.new(1..3) & Set.new(2..5)
# Set: {2, 3}
이 연산자는 두 집합에 공통으로 포함된 요소들만 반환합니다.
이 세 가지 연산자(합집합, 차집합, 교집합)는 배열에서도 동일하게 사용할 수 있습니다.
슈퍼셋(Superset)과 서브셋(Subset)
슈퍼셋(Superset)이란 다른 집합의 모든 요소를 포함하고 있는 집합을 의미합니다.
어떤 집합이 다른 집합의 슈퍼셋인지 확인할 수 있습니다.
사용 방법은 다음과 같습니다.
Set.new(10..40) >= Set.new(20..30)
10..40 범위는 그 안에 20..30을 완전히 포함하고 있습니다.
반대로 서브셋(Subset)은 다른 집합의 일부분으로 구성된 집합을 말합니다.
Set.new(25..27) <= Set.new(20..30)
SortedSet으로 항상 정렬된 집합 만들기
항상 정렬된 상태를 유지하는 집합이 필요하다면 SortedSet 클래스를 사용할 수 있습니다.
단, 이 클래스를 사용하기 위해서는 몇 가지 조건이 있습니다.
- 집합에 추가하는 객체는 반드시 <=>(우주선 연산자) 메서드를 구현해야 합니다.
- 객체들은 서로 비교 가능해야 합니다(정수끼리, 문자열끼리 비교).
예제 코드를 살펴보겠습니다.
sorted_numbers = SortedSet.new
sorted_numbers << 5
sorted_numbers << 2
sorted_numbers << 1
sorted_numbers
# SortedSet: {1, 2, 5}
마무리 정리
이번 글에서는 Ruby에서 Set을 활용해 더 나은 성능과 간결한 코드를 작성하는 방법을 배웠습니다. 또한 배열과 Set의 차이점도 함께 살펴보았습니다.
핵심을 요약하면 다음과 같습니다.
- 중복 제거가 필요하거나 빈번한 멤버십 검사(
include?)가 있다면 Set이 최적의 선택입니다. - 합집합(|), 차집합(-), 교집합(&) 연산자로 집합 간 연산을 직관적으로 처리할 수 있습니다.
- 정렬이 필요하다면 SortedSet을 활용하세요.
이 글이 도움이 되었다면 주변에 널리 공유해 주세요 🙂
읽어주셔서 감사합니다!