리스트·튜플·딕셔너리·집합의 차이와 선택 기준

리스트·튜플·딕셔너리·집합의 차이와 선택 기준#

변수 하나에 값 하나를 저장하는 방법만으로는 장바구니, 학생 명단, 상품 정보 같은 데이터를 다루기 어렵습니다. 상품이 100개라면 item1부터 item100까지 변수를 만드는 대신 여러 값을 하나의 구조로 관리해야 합니다.

이때 사용하는 것이 자료구조입니다. 입문 단계에서 자주 만나는 형태는 리스트, 튜플, 딕셔너리, 집합입니다.

네 자료구조는 모두 여러 데이터를 다룰 수 있지만, 사용 목적은 다릅니다.

  • 리스트: 순서대로 값을 모으고, 항목을 추가하거나 수정할 때
  • 튜플: 순서대로 묶은 값의 구성을 바꾸지 않을 때
  • 딕셔너리: 이름표에 해당하는 키로 값을 찾아야 할 때
  • 집합: 중복 없는 값들을 관리하거나 포함 여부를 확인할 때

이 글은 Python 예제로 차이를 설명합니다. 자료구조를 선택할 때 순서가 필요한가, 중복을 허용하는가, 무엇으로 값을 찾을 것인가를 묻는 방법은 다른 언어에도 적용할 수 있습니다.

네 가지 자료구조 한눈에 비교하기#

자료구조 Python 표기 예 순서·접근 방식 중복 생성 후 내용 변경
리스트 ["사과", "우유"] 순서가 있으며 인덱스로 접근 허용 가능
튜플 ("사과", "우유") 순서가 있으며 인덱스로 접근 허용 튜플의 항목 교체·추가 불가
딕셔너리 {"name": "사과"} 키로 접근하며 삽입 순서 유지 키 중복 불가, 값 중복 가능 가능
집합 {"사과", "우유"} 인덱스가 없으며 순서를 기준으로 사용하지 않음 허용하지 않음 원소 추가·삭제 가능

이 표에서 순서는 두 뜻으로 구분해야 합니다. 리스트와 튜플에서는 “첫 번째 항목을 인덱스 0으로 꺼낸다”는 의미가 중요합니다. 딕셔너리는 삽입 순서를 유지하지만 값을 찾을 때는 주로 위치가 아니라 키를 사용합니다. 집합은 순서나 인덱스를 사용하는 자료구조가 아닙니다.

리스트: 순서 있는 여러 값을 관리하기#

리스트는 값을 순서대로 저장하고 항목을 추가, 삭제, 수정할 수 있는 자료구조입니다. 장바구니 상품이나 학생 명단처럼 목록의 내용이 바뀔 수 있을 때 자연스럽습니다.

shopping_cart = ["사과", "우유", "계란"]

print(shopping_cart)
# ['사과', '우유', '계란']

인덱스로 항목에 접근하기#

리스트의 각 위치에는 인덱스가 있습니다. Python의 인덱스는 0부터 시작합니다.

위치 값 인덱스
첫 번째 "사과" 0
두 번째 "우유" 1
세 번째 "계란" 2
print(shopping_cart[0])  # 사과
print(shopping_cart[1])  # 우유
print(shopping_cart[2])  # 계란

마지막 항목은 음수 인덱스로도 접근할 수 있습니다.

print(shopping_cart[-1])  # 계란

항목이 세 개인 목록에서 shopping_cart[3]에 접근하면 범위를 벗어나 IndexError가 발생합니다.

항목 추가·수정·삭제하기#

shopping_cart.append("휴지")
print(shopping_cart)
# ['사과', '우유', '계란', '휴지']

shopping_cart[1] = "저지방 우유"
print(shopping_cart)
# ['사과', '저지방 우유', '계란', '휴지']

shopping_cart.remove("계란")
print(shopping_cart)
# ['사과', '저지방 우유', '휴지']
  • append()는 끝에 항목을 추가합니다.
  • shopping_cart[1] = ...는 해당 위치의 값을 바꿉니다.
  • remove()는 지정한 값과 같은 항목 중 처음 발견한 항목을 삭제합니다.

목록의 항목을 하나씩 처리할 때는 반복문을 사용합니다.

for item in shopping_cart:
    print(f"장바구니 상품: {item}")

중복도 저장할 수 있다#

shopping_cart = ["사과", "우유", "사과"]

print(shopping_cart)
# ['사과', '우유', '사과']

리스트는 같은 값이 여러 번 나와도 유지합니다. 하지만 “사과 2개”를 관리하는 실제 장바구니라면 상품명을 두 번 저장하는 것보다 상품과 수량을 명시적으로 표현하는 편이 적절할 수 있습니다.

shopping_cart = [
    {"name": "사과", "quantity": 2},
    {"name": "우유", "quantity": 1},
]

리스트 안에 딕셔너리를 넣은 형태입니다. 자료구조는 하나만 골라 쓰는 것이 아니라 필요에 맞게 조합할 수 있습니다.

리스트가 적합한 질문#

다음 질문에 해당한다면 리스트를 먼저 고려해 보세요.

  • “상품을 추가하고 삭제해야 하는가?”
  • “첫 번째, 두 번째 항목처럼 위치가 중요한가?”
  • “모든 항목을 순서대로 처리해야 하는가?”
  • “같은 값이 여러 번 등장할 수 있는가?”

튜플: 순서 있는 값을 하나로 묶기#

튜플은 여러 값을 순서대로 담는다는 점에서 리스트와 비슷합니다. 차이는 튜플 자체의 항목을 생성 후 추가하거나 교체하거나 삭제할 수 없다는 것입니다.

location = (37.5, 127.0)

print(location[0])  # 37.5
print(location[1])  # 127.0

좌표처럼 정해진 순서로 두 값을 함께 전달할 때 사용할 수 있습니다.

값을 풀어서 사용할 수 있다#

latitude, longitude = location

print(latitude)   # 37.5
print(longitude)  # 127.0

첫 번째 값은 latitude, 두 번째 값은 longitude에 할당됩니다. 이를 언패킹이라고 합니다.

함수에서 두 값을 반환할 때도 볼 수 있습니다.

def get_min_max(numbers):
    return min(numbers), max(numbers)


lowest, highest = get_min_max([8, 3, 12])

print(lowest)   # 3
print(highest)  # 12

쉼표로 묶인 두 결과는 튜플로 반환되고, 호출한 쪽에서 각각의 변수로 받습니다.

튜플의 항목은 직접 바꿀 수 없다#

location = (37.5, 127.0)

# location[0] = 38.0
# TypeError가 발생한다.

새 좌표가 필요하다면 새 튜플을 만듭니다.

location = (38.0, 127.0)

여기서 변수 location이 다른 튜플을 참조하게 된 것입니다. 기존 튜플의 첫 번째 항목을 수정한 것은 아닙니다.

튜플이 ‘내부의 모든 값을 절대 바꿀 수 없다’는 뜻은 아니다#

튜플 안에 리스트 같은 변경 가능한 객체가 들어 있다면, 그 안쪽 객체의 내용은 바뀔 수 있습니다.

record = ("김철수", [80, 90])

record[1].append(100)

print(record)
# ('김철수', [80, 90, 100])

튜플의 두 번째 자리에 있던 리스트를 다른 리스트로 교체할 수는 없지만, 그 리스트 자체를 수정할 수는 있습니다. “튜플은 불변”이라는 말은 튜플이 가진 항목의 구성을 바꿀 수 없다는 뜻으로 이해해야 합니다.

항목이 하나인 튜플의 문법#

괄호 안에 값 하나만 적으면 튜플이 아닙니다.

not_a_tuple = ("사과")
one_item_tuple = ("사과",)

print(type(not_a_tuple))    # <class 'str'>
print(type(one_item_tuple)) # <class 'tuple'>

값 하나짜리 튜플에는 쉼표가 필요합니다.

튜플이 적합한 질문#

  • “이 값들은 정해진 개수와 순서로 함께 움직이는가?”
  • “목록에 항목을 추가하거나 삭제할 일이 없는가?”
  • “이 값의 위치에 명확한 의미를 부여할 수 있는가?”

항목이 많아 item[0], item[1], item[2]가 무엇인지 기억하기 어렵다면, 이름 있는 필드를 제공하는 딕셔너리나 클래스를 사용하는 편이 읽기 쉬울 수 있습니다.

딕셔너리: 키로 값 찾기#

딕셔너리는 키와 값을 짝으로 저장합니다. 데이터의 “몇 번째 값인가?”보다 “어떤 이름의 값인가?”가 중요할 때 사용합니다.

product = {
    "name": "키보드",
    "price": 25_000,
    "stock": 8,
}
키 연결된 값
"name" "키보드"
"price" 25000
"stock" 8

값을 읽을 때는 키를 사용합니다.

print(product["name"])   # 키보드
print(product["price"])  # 25000

product[0]으로 첫 번째 값을 읽는 구조가 아닙니다. 이 예제의 키는 문자열 "name", "price", "stock"입니다.

값을 바꾸고 새 키를 추가하기#

product["stock"] = 7
product["category"] = "주변기기"

print(product)
# {'name': '키보드', 'price': 25000, 'stock': 7, 'category': '주변기기'}

이미 존재하는 키에 값을 할당하면 기존 값이 바뀝니다. 없는 키에 값을 할당하면 새 항목이 추가됩니다.

키가 없으면 어떻게 될까?#

# print(product["manufacturer"])
# KeyError가 발생한다.

반드시 있어야 하는 키라면 이 오류를 통해 데이터가 예상과 다르다는 사실을 확인할 수 있습니다.

선택 항목이라서 없을 수도 있다면 get()을 사용할 수 있습니다.

manufacturer = product.get("manufacturer")

print(manufacturer)  # None

기본값도 지정할 수 있습니다.

manufacturer = product.get("manufacturer", "제조사 미등록")

print(manufacturer)  # 제조사 미등록

단, get()만 사용하면 “키가 없는 경우”와 “키는 있지만 값이 None인 경우”가 둘 다 None으로 보일 수 있습니다. 두 상황을 구분해야 한다면 키의 존재를 직접 확인합니다.

if "manufacturer" in product:
    print("제조사 키가 있습니다.")
else:
    print("제조사 키가 없습니다.")

딕셔너리의 키는 중복되지 않는다#

product = {
    "price": 25_000,
    "price": 30_000,
}

print(product["price"])  # 30000

키 "price"로 두 값을 따로 보관하지 않습니다. 마지막에 지정된 값이 남습니다. 같은 이름의 정보를 여러 개 저장해야 한다면 다른 구조를 설계해야 합니다.

반면 값은 중복될 수 있습니다.

scores = {
    "김철수": 90,
    "이영희": 90,
}

서로 다른 키에 같은 점수 90을 저장하는 것은 가능합니다.

딕셔너리는 삽입 순서를 유지하지만 키로 찾는다#

Python의 딕셔너리는 항목을 추가한 순서를 유지합니다.

product = {
    "name": "키보드",
    "price": 25_000,
    "stock": 8,
}

for key, value in product.items():
    print(key, value)

반복하면 위에서 넣은 순서대로 키와 값을 볼 수 있습니다. 하지만 딕셔너리를 선택하는 주된 이유는 순번으로 값을 찾기 위해서가 아니라 키에 이름을 붙여 정보에 접근하기 위해서입니다.

딕셔너리가 적합한 질문#

  • “이 값은 위치보다 이름으로 찾는 편이 자연스러운가?”
  • “상품의 가격, 재고, 이름처럼 서로 다른 의미의 정보를 묶는가?”
  • “식별자나 코드로 해당 값을 조회해야 하는가?”

예를 들어 상품 ID별로 상품 정보를 찾고 싶다면 딕셔너리를 중첩할 수 있습니다.

products_by_id = {
    "P001": {"name": "노트", "price": 3_000},
    "P002": {"name": "키보드", "price": 25_000},
}

print(products_by_id["P002"]["name"])  # 키보드

"P002"는 상품을 찾는 키이고, 그 안의 "name"은 상품 정보의 키입니다.

집합: 중복 없는 값 관리하기#

집합은 같은 값을 중복해서 보관하지 않는 자료구조입니다. 어떤 값이 포함되어 있는지 확인하거나, 두 값 묶음의 공통 요소를 구할 때 유용합니다.

visitor_ids = {"user_a", "user_b", "user_a"}

print(visitor_ids)
# {'user_a', 'user_b'}와 같은 내용

"user_a"를 두 번 적어도 집합에는 하나만 남습니다. 출력될 때 두 원소의 표시 순서는 보장하지 않습니다.

값 추가와 포함 여부 검사#

allowed_roles = {"admin", "editor"}

allowed_roles.add("viewer")

print("editor" in allowed_roles)  # True
print("guest" in allowed_roles)   # False

add()로 원소를 추가합니다. 이미 존재하는 값을 추가해도 같은 값이 두 개로 늘어나지 않습니다.

목록에서 중복 제거하기#

tags = ["Python", "입문", "Python", "자료구조"]

unique_tags = set(tags)

print(unique_tags)
# {'Python', '입문', '자료구조'}와 같은 내용

중복은 제거되지만 원래 목록의 순서를 그대로 보존하는 방법으로 집합 변환을 사용해서는 안 됩니다. 중복 제거와 원래 순서 유지가 모두 필요하다면 다른 방법을 선택해야 합니다.

예를 들어 딕셔너리의 키가 중복되지 않고 삽입 순서가 유지된다는 성질을 이용할 수 있습니다.

unique_tags_in_order = list(dict.fromkeys(tags))

print(unique_tags_in_order)
# ['Python', '입문', '자료구조']

교집합과 합집합#

집합은 여러 값 묶음의 관계를 계산하는 데도 사용할 수 있습니다.

morning_attendees = {"김철수", "이영희", "박지성"}
afternoon_attendees = {"이영희", "박지성", "손흥민"}

both = morning_attendees & afternoon_attendees
either = morning_attendees | afternoon_attendees

print(both)
# {'이영희', '박지성'}와 같은 내용

print(either)
# {'김철수', '이영희', '박지성', '손흥민'}과 같은 내용
  • &: 두 집합에 모두 들어 있는 값
  • |: 두 집합 중 어느 한쪽에라도 들어 있는 값

이름이 같은 서로 다른 사람을 구분해야 한다면 이름 대신 고유한 사용자 ID를 저장해야 합니다. 집합은 값이 같으면 같은 원소로 취급하기 때문입니다.

빈 집합은 {}로 만들지 않는다#

empty_dict = {}
empty_set = set()

print(type(empty_dict))  # <class 'dict'>
print(type(empty_set))   # <class 'set'>

{}는 빈 딕셔너리입니다. 빈 집합을 만들 때는 set()을 사용합니다.

같은 데이터를 네 구조로 표현하면?#

“학생과 점수”라는 정보를 무엇을 하려는지에 따라 다르게 표현할 수 있습니다.

학생 이름을 등록 순서대로 보관하기: 리스트#

student_names = ["김철수", "이영희", "박지성"]

첫 번째 등록 학생을 꺼내거나 학생을 추가할 수 있습니다.

이름과 점수를 한 건으로 묶기: 튜플#

student_record = ("김철수", 90)

첫 번째 항목은 이름, 두 번째 항목은 점수입니다. 다만 필드가 늘어나면 위치의 의미가 불분명해질 수 있습니다.

학생 ID로 정보 찾기: 딕셔너리#

students_by_id = {
    "S001": {"name": "김철수", "score": 90},
    "S002": {"name": "이영희", "score": 85},
}

print(students_by_id["S001"]["score"])  # 90

학생 ID가 조회 기준입니다.

출석한 학생 ID를 중복 없이 기록하기: 집합#

attended_ids = {"S001", "S002"}

print("S001" in attended_ids)  # True

같은 학생의 출석 이벤트가 두 번 들어와도 “출석 여부”만 필요하다면 집합이 목적에 맞습니다. 반대로 몇 번 출석 이벤트가 발생했는지 세어야 한다면 집합만으로는 정보가 사라집니다.

자료구조 선택은 “어떤 자료구조가 더 좋은가?”가 아니라 **“이 데이터로 어떤 질문에 답해야 하는가?”**에서 시작합니다.

선택 기준: 무엇을 기준으로 고르면 될까?#

다음 순서로 생각해 보세요.

1. 값을 이름으로 찾는가, 위치로 찾는가?#

  • "price"처럼 키 이름으로 찾는다 → 딕셔너리
  • 첫 번째, 두 번째처럼 위치가 중요하다 → 리스트 또는 튜플

2. 중복을 보존해야 하는가?#

  • 중복된 항목 각각이 의미 있다 → 리스트 또는 튜플
  • 고유한 값만 필요하다 → 집합

3. 생성 후 내용을 바꿔야 하는가?#

  • 항목을 추가하거나 수정해야 한다 → 리스트 또는 딕셔너리
  • 정해진 순서의 항목 구성을 유지한다 → 튜플 검토
  • 고유한 값들을 추가하거나 제거한다 → 집합

4. 각 값의 의미가 위치만으로 분명한가?#

("김철수", 90)은 두 항목이라면 이해할 수 있습니다. 하지만 다음은 무엇이 나이이고 무엇이 점수인지 기억해야 합니다.

student = ("김철수", 2, 90, True, "컴퓨터공학")

정보가 많아지면 딕셔너리처럼 이름을 붙이는 편이 읽기 쉽습니다.

student = {
    "name": "김철수",
    "grade": 2,
    "score": 90,
    "enrolled": True,
    "major": "컴퓨터공학",
}

데이터와 함께 성적 추가, 학년 변경 같은 동작까지 묶어 관리해야 한다면 이후에는 클래스로 표현하는 방법도 검토할 수 있습니다.

상황별 빠른 선택표#

필요한 일 먼저 검토할 구조 이유
장바구니 상품을 순서대로 표시하고 항목 추가 리스트 순서와 변경이 필요
위도·경도 한 쌍을 전달 튜플 정해진 순서의 값 묶음
상품 ID로 가격과 재고 찾기 딕셔너리 키로 조회
방문한 사용자 ID 중복 제거 집합 고유한 값만 유지
학생 명단과 각 학생의 속성 관리 리스트 안의 딕셔너리 또는 ID별 딕셔너리 목록 순회와 이름 붙은 정보의 조합

실무에서 주의할 점#

리스트를 다른 변수에 할당해도 복사되지는 않는다#

first_cart = ["사과", "우유"]
second_cart = first_cart

second_cart.append("계란")

print(first_cart)
# ['사과', '우유', '계란']

두 변수는 같은 리스트 객체를 참조합니다. second_cart에서 값을 추가하면 first_cart로 볼 때도 변경된 목록이 보입니다.

별도의 리스트가 필요하면 복사할 수 있습니다.

first_cart = ["사과", "우유"]
second_cart = first_cart.copy()

second_cart.append("계란")

print(first_cart)   # ['사과', '우유']
print(second_cart)  # ['사과', '우유', '계란']

다만 .copy()는 얕은 복사입니다. 리스트 안에 또 다른 리스트나 딕셔너리가 있으면 내부 객체는 공유될 수 있습니다. 중첩된 데이터의 복사는 별도로 확인해야 합니다.

딕셔너리 키와 집합 원소에는 아무 값이나 넣을 수 없다#

딕셔너리 키와 집합 원소로는 해시할 수 있는 값이 필요합니다. 문자열, 정수, 적절한 튜플 등은 사용할 수 있지만, 일반적인 리스트는 직접 넣을 수 없습니다.

scores_by_student = {
    "S001": 90
}

# 잘못된 예
# scores_by_student[["S001"]] = 90
# TypeError: unhashable type: 'list'

집합도 마찬가지입니다.

# 잘못된 예
# selected = {[1, 2]}

중첩된 값의 구성을 키로 써야 한다면 요구사항에 따라 튜플 같은 구조를 검토할 수 있습니다. 단, 튜플 안에 리스트가 들어 있다면 그 튜플도 키로 사용할 수 없습니다.

딕셔너리의 순서 유지와 집합의 순서를 혼동하지 않는다#

딕셔너리는 추가된 순서대로 항목을 순회할 수 있습니다. 집합은 저장 순서나 출력 순서에 기대어 로직을 작성하면 안 됩니다. 화면에 일정한 순서로 표시해야 한다면 정렬해서 사용합니다.

tags = {"Python", "자료구조", "입문"}

for tag in sorted(tags):
    print(tag)

직접 확인해 보기#

아래 프로그램은 상품의 태그를 여러 건 받아 중복을 없애고, 상품 ID로 가격을 찾습니다.

product_tags = ["개발", "입문", "Python", "입문"]

products = {
    "P001": {"name": "프로그래밍 입문", "price": 12_000},
    "P002": {"name": "자료구조 기초", "price": 15_000},
}

unique_tags = set(product_tags)
selected_product = products["P002"]

print(len(product_tags))
print(len(unique_tags))
print(selected_product["name"])
print(selected_product["price"])

실행 결과:

4
3
자료구조 기초
15000

왜 태그의 길이는 각각 4와 3일까요? 리스트에는 "입문"이 두 번 들어 있고, 집합은 중복 값을 하나만 보관하기 때문입니다. "P002"로 상품 정보를 찾는 것은 딕셔너리의 키 조회입니다.

이번에는 태그의 중복을 제거하되 처음 등장한 순서까지 유지하려면 어떤 방법을 사용할지 생각해 보세요. 집합으로 바꾼 뒤 그대로 출력하는 방법으로는 순서를 보장할 수 없습니다.

정리#

네 자료구조의 차이를 한 문장씩 기억해 보세요.

  • 리스트: 순서가 있는 값을 모으고 항목을 변경하는 목록
  • 튜플: 순서가 있는 값의 구성을 고정해 묶는 구조
  • 딕셔너리: 키 이름으로 값을 찾는 구조
  • 집합: 중복 없는 값을 관리하는 구조

실제 프로그램에서는 하나만 사용하지 않습니다. 주문 목록은 리스트, 주문 한 건의 정보는 딕셔너리, 처리한 주문 ID는 집합처럼 목적에 따라 조합합니다.

자료구조를 고를 때 가장 먼저 할 질문은 “어떤 문법을 배웠는가?”가 아닙니다. 값을 어떤 기준으로 찾고, 순서와 중복을 보존해야 하며, 나중에 무엇을 변경해야 하는가? 이 세 가지를 먼저 정하면 선택이 훨씬 쉬워집니다.

이 페이지의 목차