리스트 vs 튜플, 언제 무엇을 쓸까

리스트와 튜플은 둘 다 순서 있는 모음이지만 쓰임이 다릅니다. 변경 가능성, 해시 가능 여부, 그리고 '모음 vs 레코드'라는 의미의 차이로 선택 기준을 정리합니다.

· 8 min read · PALDYN Team

지난 글에서 타입을 묻기보다 능력을 보는 태도를 이야기했다면, 이번에는 가장 자주 마주치는 두 시퀀스 타입을 두고 “어느 쪽을 쓸까”라는 실전 판단을 정리한다. 리스트와 튜플은 둘 다 순서 있는 값의 모음이고, 인덱싱·슬라이싱·순회가 거의 똑같이 동작한다. 그래서 처음에는 “그냥 대괄호 쓸까 소괄호 쓸까”의 취향 문제처럼 보이지만, 둘 사이에는 단순한 문법 차이를 넘는 의미의 차이가 있다.

가장 큰 차이 — 변경 가능 여부

리스트와 튜플을 가르는 근본적인 한 가지는 변경 가능성(mutability)이다. 리스트는 만든 뒤에도 원소를 추가·삭제·교체할 수 있는 가변(mutable) 타입이고, 튜플은 한 번 만들면 내용을 바꿀 수 없는 불변(immutable) 타입이다.

nums = [1, 2, 3]
nums.append(4)      # OK → [1, 2, 3, 4]
nums[0] = 99        # OK → [99, 2, 3, 4]

point = (1, 2, 3)
point.append(4)     # AttributeError: 'tuple' object has no attribute 'append'
point[0] = 99       # TypeError: 'tuple' object does not support item assignment

이 차이는 단순한 제약이 아니라 보장이기도 하다. 어떤 값을 튜플로 받았다면 “이건 누가 중간에 바꿔치기하지 않는다”는 보장이 함께 따라온다. 함수에 넘긴 인자가 호출된 쪽에서 슬쩍 수정되는 사고를 원천적으로 막을 수 있다.

list vs tuple — 한눈에 비교

불변이라서 가능한 일 — 딕셔너리 키와 집합 원소

불변성에는 실용적인 결과가 따라온다. 튜플은 (그 안의 원소들도 모두 불변이라면) 해시 가능(hashable)해서 딕셔너리의 키나 집합의 원소가 될 수 있다. 리스트는 가변이라 해시할 수 없어 이런 자리에 쓰지 못한다.

# 좌표를 키로 쓰는 딕셔너리 — 튜플이라 가능
grid = {}
grid[(0, 0)] = "시작"
grid[(2, 3)] = "보물"

# 리스트를 키로 쓰면 즉시 에러
grid[[0, 0]] = "시작"   # TypeError: unhashable type: 'list'

“여러 값의 조합을 하나의 키로 묶고 싶다”는 상황은 생각보다 자주 생긴다. 좌표, (연도, 월) 쌍, (사용자ID, 권한) 조합 같은 것들이다. 이때 자연스럽게 손이 가는 도구가 튜플이다. 가변 데이터를 키로 쓰면 해시값이 도중에 바뀌어 자료구조가 깨질 수 있으므로, 파이썬은 아예 가변 타입의 해시를 막아 둔 것이다.

의미로 고르기 — 모음인가, 레코드인가

문법과 기능 차이를 넘어, 경험 많은 파이썬 개발자들은 둘을 의미로 구분한다. 흔히 인용되는 기준은 이렇다.

  • 리스트는 같은 종류의 것들이 임의 개수로 모인 동질적 모음(homogeneous collection). 원소 하나를 더 붙여도 말이 된다.
  • 튜플은 자리마다 다른 의미를 가진 고정된 구조의 레코드(heterogeneous record). 자리 자체가 뜻을 담는다.
# 리스트 — 점수들의 모음. 몇 개든 더해도 자연스럽다
scores = [88, 92, 79, 95]
scores.append(100)

# 튜플 — (위도, 경도)라는 고정된 구조
point = (37.5, 127.0)
lat, lng = point   # 0번 자리는 위도, 1번 자리는 경도

이 기준으로 보면 선택이 한결 분명해진다. “원소를 하나 더 붙여도 의미가 통하면” 리스트, “각 자리에 정해진 역할이 있어서 개수가 늘어나면 의미가 깨지면” 튜플이다. ("홍길동", 25, "서울") 같은 값은 길이가 3으로 고정된 한 사람의 레코드이지 “사람 정보들의 모음”이 아니므로 튜플이 어울린다.

의미로 고르기 — 모음인가, 레코드인가

성능은 부차적이지만 알아 둘 것

튜플은 불변이라 내부 구조가 단순하고, 일반적으로 같은 내용의 리스트보다 메모리를 조금 덜 쓰며 생성이 약간 더 빠르다.

import sys
print(sys.getsizeof([1, 2, 3]))   # 예: 88
print(sys.getsizeof((1, 2, 3)))   # 예: 64

다만 이 차이는 대부분의 경우 성능 선택의 기준이 될 만큼 크지 않다. “수백만 개를 만든다”처럼 극단적인 상황이 아니라면, 성능 때문에 튜플을 고르기보다는 위에서 본 의미와 불변성을 기준으로 고르는 편이 옳다. 성능은 그 선택에 따라오는 보너스로 생각하면 된다.

레코드가 커지면 다음 단계로

튜플이 레코드 역할을 하다 보면 data[2]처럼 “2번 자리가 뭐였더라” 하고 헷갈리는 순간이 온다. 자리가 서너 개를 넘고 이름이 필요해지면, 튜플의 불변성과 가벼움은 유지하면서 이름을 붙일 수 있는 NamedTuple이나, 더 나아가 dataclass로 옮겨 가는 것이 자연스러운 다음 단계다.

from typing import NamedTuple

class Point(NamedTuple):
    lat: float
    lng: float

p = Point(37.5, 127.0)
print(p.lat, p.lng)   # 자리 번호 대신 이름으로 접근

정리

리스트와 튜플의 갈림길은 세 가지로 정리된다. 첫째, 내용이 바뀌어야 하면 리스트, 한 번 정해지면 변하지 않아야 하면 튜플. 둘째, 딕셔너리 키나 집합 원소로 써야 하면 (해시 가능한) 튜플뿐이다. 셋째이자 가장 중요하게는, 같은 종류의 모음이면 리스트, 자리마다 의미가 다른 레코드면 튜플이다. 성능 차이는 대개 부차적이니, 평소에는 “이 값이 모음인가 레코드인가, 변해야 하는가”라는 질문으로 고르면 충분하다.


지난 글: 덕 타이핑 vs isinstance — 타입을 묻지 않는 법

다음 글: 얕은 복사 vs 깊은 복사


읽어주셔서 감사합니다. 😊