iBetter Books
수정

중복 없는 집합 — Set

List는 순서를 중요하게 여기고, 같은 값이 여러 번 들어올 수 있습니다. 그런데 어떤 상황에서는 그게 문제가 됩니다. 예를 들어 "이미 방문한 페이지 목록"을 관리한다고 생각해보세요. 같은 페이지를 세 번 방문했다고 목록에 세 번 넣을 필요는 없습니다. 이럴 때 Set이 딱입니다.

Set은 중복을 허용하지 않는 컬렉션입니다. 같은 값을 두 번 추가하려 하면 두 번째는 그냥 무시됩니다. 그리고 순서를 보장하지 않습니다. 값이 있느냐 없느냐만 중요합니다.

Set 만들기

중괄호 {}를 쓰면 Set이 됩니다. 단, 비어 있는 {}는 Map으로 해석되니 주의가 필요합니다.

void main() {
  // Set 리터럴
  Set<String> colors = {'빨강', '초록', '파랑'};
  print(colors); // {빨강, 초록, 파랑}

  // 중복 값 추가 시도
  var numbers = {1, 2, 3, 2, 1};
  print(numbers); // {1, 2, 3} — 중복 제거됨

  // 빈 Set (반드시 타입 명시 또는 Set<>() 사용)
  var empty = <String>{};
  print(empty.isEmpty); // true

  // 생성자로 만들기
  var fromList = Set.from([1, 2, 3, 2, 1]);
  print(fromList); // {1, 2, 3}
}

빈 Set을 만들 때 var empty = {}처럼 쓰면 Dart는 이것을 Map<dynamic, dynamic>으로 해석합니다. <String>{}처럼 타입을 명시하거나 Set<String>()을 써야 합니다.

요소 추가와 삭제

void main() {
  var fruits = {'사과', '바나나', '딸기'};

  // 추가 (중복이면 무시)
  fruits.add('포도');
  fruits.add('사과'); // 이미 있으므로 무시
  print(fruits); // {사과, 바나나, 딸기, 포도}

  // 여러 개 추가
  fruits.addAll({'멜론', '수박'});
  print(fruits); // {사과, 바나나, 딸기, 포도, 멜론, 수박}

  // 삭제
  fruits.remove('바나나');
  print(fruits); // {사과, 딸기, 포도, 멜론, 수박}

  // 포함 여부 확인
  print(fruits.contains('딸기')); // true
  print(fruits.contains('배'));   // false

  // 길이
  print(fruits.length); // 5
}

집합 연산 — Set의 진짜 힘

Set이 List보다 빛나는 순간이 바로 집합 연산입니다. 수학에서 배운 합집합, 교집합, 차집합을 그대로 코드로 쓸 수 있습니다.

void main() {
  var a = {1, 2, 3, 4, 5};
  var b = {3, 4, 5, 6, 7};

  // 합집합 (union) — 두 집합의 모든 요소
  print(a.union(b)); // {1, 2, 3, 4, 5, 6, 7}

  // 교집합 (intersection) — 공통 요소만
  print(a.intersection(b)); // {3, 4, 5}

  // 차집합 (difference) — a에 있고 b에 없는 요소
  print(a.difference(b)); // {1, 2}
  print(b.difference(a)); // {6, 7}
}

실제로 어디에 쓰일까요? 예를 들어 두 수업을 동시에 수강하는 학생을 찾아내는 상황입니다.

void main() {
  var classA = {'김철수', '이영희', '박민준', '최지원'};
  var classB = {'이영희', '최지원', '한승우', '정다은'};

  // 두 수업 모두 수강하는 학생
  var both = classA.intersection(classB);
  print('두 수업 동시 수강: $both');
  // {이영희, 최지원}

  // 전체 수강생 목록
  var all = classA.union(classB);
  print('전체 수강생: $all');
  // {김철수, 이영희, 박민준, 최지원, 한승우, 정다은}

  // A 수업만 듣는 학생
  var onlyA = classA.difference(classB);
  print('A 수업만 수강: $onlyA');
  // {김철수, 박민준}
}

코드가 짧고 의도가 명확합니다. List로 같은 것을 구현하려면 루프를 여러 번 돌려야 합니다.

고유 값 필터링 — 중복 제거 도구로 활용

Set의 중복 제거 특성을 실용적으로 활용하는 방법입니다. List에서 중복을 없애고 싶을 때 Set을 거치면 됩니다.

void main() {
  // 설문 응답에서 중복 제거
  var responses = ['Python', 'Dart', 'Python', 'Java', 'Dart', 'Python', 'Go'];
  var unique = responses.toSet().toList();
  print(unique); // [Python, Dart, Java, Go]

  // 태그 시스템 — 같은 태그가 두 번 붙지 않도록
  var tags = <String>{};
  tags.add('Flutter');
  tags.add('Dart');
  tags.add('Flutter'); // 이미 있음
  tags.add('Mobile');
  print(tags); // {Flutter, Dart, Mobile}

  // 방문한 URL 추적 (중복 방문은 한 번으로)
  var visited = <String>{};
  void visit(String url) {
    bool isNew = visited.add(url); // add는 추가 성공 시 true 반환
    if (isNew) {
      print('새 페이지 방문: $url');
    } else {
      print('이미 방문한 페이지: $url');
    }
  }

  visit('https://dart.dev');
  visit('https://flutter.dev');
  visit('https://dart.dev'); // 중복
}

출력 결과입니다.

새 페이지 방문: https://dart.dev
새 페이지 방문: https://flutter.dev
이미 방문한 페이지: https://dart.dev

add() 메서드가 bool을 반환한다는 점도 눈여겨보세요. 실제로 추가됐으면 true, 이미 있어서 무시됐으면 false입니다.

부분집합 여부 확인

void main() {
  var all = {1, 2, 3, 4, 5};
  var sub = {2, 3};

  // sub가 all의 부분집합인지
  print(sub.every((e) => all.contains(e))); // true

  // containsAll — 모든 요소를 포함하는지
  print(all.containsAll(sub)); // true
  print(sub.containsAll(all)); // false
}

Set vs List — 언제 무엇을 쓸까

상황 선택
순서가 중요할 때 List
중복이 있어도 괜찮을 때 List
인덱스로 접근해야 할 때 List
중복을 허용하면 안 될 때 Set
포함 여부를 빠르게 확인할 때 Set
집합 연산이 필요할 때 Set

contains() 성능 차이도 중요합니다. List의 contains는 처음부터 끝까지 하나씩 비교합니다(O(n)). Set의 contains는 해시 테이블을 이용하므로 요소 수와 관계없이 빠릅니다(O(1)). 수십만 개의 요소 중에서 포함 여부를 자주 확인해야 한다면 Set을 쓰는 것이 훨씬 유리합니다.

void main() {
  // 금지어 목록 — 자주 contains 확인이 필요한 상황
  // List보다 Set이 적합
  final blockedWords = <String>{'욕설1', '욕설2', '욕설3', '금지어1'};

  String input = '이건 욕설2가 포함된 문장입니다.';
  bool hasBlocked = blockedWords.any((word) => input.contains(word));
  print(hasBlocked ? '게시 불가' : '게시 가능'); // 게시 불가
}

다음 챕터에서는 키와 값을 짝지어 저장하는 Map을 배웁니다. "이름 → 점수"처럼 연결 관계를 표현하는 컬렉션입니다.