본문 바로가기
TYLER SONGBlog
블로그 목록
LLM Research

tuple·dict·set: 자료구조를 잘 고르면 코드가 100배 빨라진다

리스트만으로 모든 걸 해결하려 하면 데이터가 커질수록 코드가 느려진다. 오늘은 tuple(불변), dict(키-값 저장), set(중복 없는 모음)이 각각 언제 유리한지, 그리고 "찾기" 연산이 자료구조에 따라 왜 속도가 다른지 감각을 잡는다.

송민성6분 읽기

오늘의 목표

파이썬에서 데이터를 어떻게 담을지 상황에 맞게 고를 수 있게 되는 것이 오늘의 목표다. 예를 들어 "값이 바뀌면 안 되는 좌표"는 tuple, "이름으로 전화번호를 찾고 싶다"는 dict, "중복 없이 원소만 확인하고 싶다"는 set이 적합하다는 감각을 만든다.

개념과 직관

tuple (튜플, 한 번 만들면 바꿀 수 없는 리스트)

리스트 [1, 2, 3]는 나중에 내용물[0] = 99처럼 바꿀 수 있지만, 튜플 (1, 2, 3)은 한 번 만들면 못 바꾼다. 마치 도장 찍힌 계약서 같은 것이다. 좌표 (x, y)나 "이 값들은 절대 실수로라도 바뀌면 안 돼" 하는 데이터에 쓴다.

python
point = (3, 5) # point[0] = 10 # 에러 발생! TypeError

dict (딕셔너리, 열쇠-내용물 저장소)

리스트는 순서(인덱스 0, 1, 2...)로 값을 찾지만, dict는 내가 정한 "키(key)"로 값을 찾는다. 사물함에 번호 대신 이름표를 붙이는 것과 비슷하다.

python
phonebook = {"철수": "010-1111-2222", "영희": "010-3333-4444"} print(phonebook["철수"]) # 010-1111-2222 print(phonebook.get("민수")) # 없으면 에러 대신 None 반환 print("영희" in phonebook) # True (키가 있는지 확인) for name, number in phonebook.items(): print(name, number)

여기서 .get()은 키가 없을 때 프로그램이 죽지 않고 None을 돌려준다는 게 핵심이다. phonebook["민수"]는 없으면 KeyError로 바로 죽는다.

set (집합, 중복을 허용하지 않는 모음)

set은 "이 안에 이 값이 있냐 없냐"만 관심 있고, 순서도 중복도 없다. 학교 반에 "출석한 학생 명단"을 생각하면, 같은 학생이 두 번 출석 체크될 필요가 없다.

python
a = {1, 2, 3, 3, 3} print(a) # {1, 2, 3} - 중복 자동 제거 s1 = {1, 2, 3, 4} s2 = {3, 4, 5, 6} print(s1 & s2) # 교집합 {3, 4} print(s1 | s2) # 합집합 {1,2,3,4,5,6} print(s1 - s2) # 차집합 {1, 2}

"찾기" 속도가 왜 다른가 (시간복잡도 감각)

리스트에서 특정 값이 있는지 확인하려면(값 in 리스트), 파이썬은 처음부터 끝까지 하나씩 비교해야 한다. 리스트 길이가 n이면 최악의 경우 n번 비교한다. 이걸 O(n) (원소 개수에 비례해서 시간이 늘어난다)이라고 쓴다.

반면 dict나 set은 "해시(hash)"라는 방법으로 값의 위치를 계산해서 바로 찾아간다. 마치 도서관에서 책을 처음부터 끝까지 뒤지는 게 아니라, 책 번호(청구기호)로 바로 그 자리로 가는 것과 같다. 이건 데이터가 아무리 많아져도 거의 일정한 시간이 걸린다. 이걸 O(1) (원소 개수와 상관없이 거의 일정한 시간)이라고 부른다.

숫자로 감 잡기: 리스트 원소가 100만 개면 최악의 경우 100만 번 비교하지만, dict/set은 몇 번의 계산만으로 끝난다. 데이터가 커질수록 이 차이는 무시할 수 없게 된다.

코드로 직접 해보기

1) 단어 빈도수 세기 (dict)

python
text = "the quick brown fox jumps over the lazy dog the fox runs" words = text.split() freq = {} for w in words: if w in freq: freq[w] += 1 else: freq[w] = 1 print(freq) # {'the': 3, 'quick': 1, 'brown': 1, 'fox': 2, ...}

더 짧게 쓰는 방법도 있다:

python
freq2 = {} for w in words: freq2[w] = freq2.get(w, 0) + 1

get(w, 0)은 "w라는 키가 있으면 그 값, 없으면 0을 기본값으로 써라"는 뜻이다. if문 없이 한 줄로 끝난다.

2) 두 리스트의 공통 원소 (set)

python
list_a = [1, 2, 3, 4, 5] list_b = [4, 5, 6, 7, 8] common = set(list_a) & set(list_b) print(common) # {4, 5}

리스트를 이중 반복문으로 비교하면 O(n*m)이 걸리지만, set으로 바꾸면 훨씬 빠르게 교집합을 구할 수 있다.

3) 전화번호부 (dict)

python
phonebook = {} phonebook["철수"] = "010-1111-2222" phonebook["영희"] = "010-3333-4444" name = "영희" if name in phonebook: print(f"{name}의 번호: {phonebook[name]}") else: print(f"{name}는 명단에 없습니다")

오늘의 실험

리스트에서 찾기와 set에서 찾기의 속도 차이를 직접 눈으로 확인한다.

python
import time n = 1_000_000 big_list = list(range(n)) big_set = set(range(n)) target = n - 1 # 가장 마지막 값이라 최악의 경우 start = time.time() result = target in big_list print("list in:", time.time() - start, "초") start = time.time() result = target in big_set print("set in:", time.time() - start, "초")
  • n을 10,000 / 100,000 / 1,000,000으로 바꿔가며 실행 시간이 어떻게 변하는지 기록한다.
  • 리스트는 n이 커질수록 시간이 비례해서 늘어나고, set은 거의 그대로인 걸 확인하는 게 목표다.
  • 결과를 표나 메모로 남겨두면 나중에 "왜 dict/set을 써야 하는가"를 스스로에게 설득할 근거가 된다.

왜 이걸 배우나 (LLM / Post-training 연결)

LLM을 학습시킬 때는 토큰(token, 텍스트를 잘게 쪼갠 단위)이 수십억 개 등장하는데, 특정 토큰이 어휘 사전(vocabulary)에 있는지 확인하거나, 중복 데이터를 제거(deduplication)하거나, 단어/문서 빈도를 세는 작업이 실제로 매일 일어난다. 이때 리스트로 짜면 실행이 끝나지 않을 정도로 느려지고, dict/set 기반으로 짜면 실용적인 시간 안에 끝난다. 오늘 배운 "찾기 속도" 감각은 데이터 전처리 코드를 짤 때 그대로 판단 기준이 된다.

초보자가 막히는 지점

  • listset을 섞어서 연산하려다 에러가 나는 경우가 많다. set 연산(&, |, -)은 양쪽 다 set이어야 한다. list_a & list_b는 안 되고 set(list_a) & set(list_b)로 바꿔야 한다.
  • dict에서 없는 키에 그냥 접근하면(phonebook["없는이름"]) KeyError가 나서 당황하기 쉽다. 안전하게 꺼내려면 .get()을 쓰거나 in으로 먼저 확인한다.
  • set과 dict는 원소/키의 "순서"를 보장하지 않는다고 배웠던 시절 습관 때문에 헷갈릴 수 있는데, 최신 파이썬(3.7+)에서는 dict가 입력 순서를 유지한다. 다만 set은 여전히 순서를 신경 쓰면 안 되는 자료구조다.
  • 튜플을 하나만 만들 때 (1)이라고 쓰면 그냥 정수 1이 된다. 원소가 하나인 튜플은 (1,)처럼 콤마를 꼭 붙여야 한다.
  • 시간 측정 실험에서 target을 리스트/set의 맨 앞 값으로 하면 리스트도 순식간에 찾아서 차이가 안 보인다. 반드시 존재하지 않는 값이나 맨 뒤 값으로 테스트해야 최악의 경우를 볼 수 있다.

알고리즘 (병행 1시간)

LeetCode Easy 중 해시(dict)·집합(set) 개념을 쓰는 문제 2개를 풀어본다. 예를 들어 "Two Sum"(배열에서 합이 특정 값이 되는 두 수 찾기)이나 "Contains Duplicate"(배열에 중복 원소가 있는지 확인) 같은 유형이 오늘 배운 내용과 잘 맞는다. 실제로 어떤 문제를 골랐는지는 본인 풀이 기록에 남긴다.

풀이할 때 아래 세 가지를 반드시 기록한다:

  • 접근: 처음에 리스트로 무식하게(brute-force) 풀었는지, dict/set을 써서 풀었는지
  • 시간복잡도: 내가 짠 코드가 O(n)인지 O(n^2)인지, 왜 그런지 한 줄로 설명
  • 틀린 이유: 만약 틀렸다면 무엇 때문이었는지 (예: 빈 리스트 처리 안 함, 키 없는 경우 에러 등)

이 기록은 나중에 비슷한 실수를 반복하지 않기 위한 자산이 된다.

© 2026 Tyler Song