Step 42. 자료형 심화 — 값을 담는 네 가지 바구니

Step 42. 자료형 심화 — 값을 담는 네 가지 바구니

Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★☆☆☆ | 예상 소요 시간 3시간

전제: Step 41을 마쳤다. 파이썬 설치, VSCode 실행, 변수와 input()/print()를 안다.

  • 준비물: Step 41에서 만든 작업대(파이썬 + VSCode) 그대로. 새 폴더 py_practice를 계속 씁니다.
  • 주의: 오늘 실습은 100% 안전합니다. 내 컴퓨터에서 코드 파일을 만들고 실행하는 일뿐입니다.

지난 시간에는 값 하나에 이름표를 붙이는 법을 배웠습니다. name = "민수". 그런데 현실의 문제는 늘 "하나"가 아닙니다. 접속 로그에는 IP가 수천 개, 학생 명단에는 이름이 수십 개, 서버 설정에는 항목이 수십 개입니다. 이걸 변수 하나씩으로 담으면 ip1, ip2, ip3… 천 번째쯤에서 관리가 무너집니다. 그래서 프로그래밍에는 값 여러 개를 한 변수에 담는 자료구조(data structure)가 있습니다. 오늘은 파이썬의 대표 바구니 네 가지를 배우고, 문자열 조작을 함께 익힙니다. "어느 바구니에 담을까"의 선택은 나중에 스캔 결과 정리, 로그 집계 코드의 절반을 결정합니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • 리스트를 만들고 인덱스로 꺼내고 append로 추가한다
  • 슬라이싱 [시작:끝]의 "끝 미포함" 규칙을 설명한다
  • 문자열을 split·replace·in으로 쪼개고 찾는다
  • 딕셔너리에 항목을 추가·수정·조회하고 .get()의 안전성을 안다
  • 셋으로 중복을 제거하고, 가변/불변의 구분을 안다
  • 상황을 보고 네 바구니 중 적절한 것을 이유와 함께 고른다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 파이썬 3.12 이상, VSCode (Step 41의 작업대)
오늘의 명령어·문법 리스트 []·append·len·슬라이싱, 튜플 (), 딕셔너리 {}·.get(), 셋 set(), 문자열 split·replace·join·in
필요한 개념 인덱스(0부터 세기), 가변(mutable)과 불변(immutable), 중첩 자료구조

2-1. 리스트 — 순서 있는 책꽂이

리스트(list)는 순서가 있는 바구니입니다. 대괄호 []로 만듭니다.

ips = ["1.1.1.1", "8.8.8.8", "9.9.9.9"]

칸마다 번호가 있는데, 컴퓨터는 0부터 셉니다. 첫 번째 값은 ips[0], 두 번째는 ips[1]입니다. 이 번호를 인덱스(index)라고 부릅니다. 처음엔 어색하지만 곧 몸이 기억합니다.

2-2. 튜플 — 봉인된 리스트

튜플(tuple)은 소괄호 ()로 만드는, 한 번 만들면 바꿀 수 없는 리스트입니다. "이 값들은 변하면 안 된다"는 약속이 필요할 때 씁니다. 오늘은 존재와 모양만 알고 넘어갑니다.

2-3. 딕셔너리 — 이름표 사전

딕셔너리(dictionary)는 중괄호 {}로 만드는 "이름표: 값" 쌍의 모음입니다.

host = {"ip": "192.168.0.23", "os": "ubuntu", "port": 22}

리스트가 "몇 번째 칸?"으로 찾는다면, 딕셔너리는 "무슨 이름표?"로 찾습니다. host["os"]라고 물으면 "ubuntu"가 나옵니다. 순서가 아니라 이름으로 찾으니 항목이 많아도 헷갈리지 않고, 서버 정보·설정값을 담는 데 가장 많이 쓰입니다. 이때 이름표를 키(key), 값을 값(value)이라 부릅니다.

2-4. 셋 — 중복 금지 명단

셋(set)은 중복을 허용하지 않는 바구니입니다. 같은 값을 두 번 넣어도 하나만 남습니다. 로그에서 "서로 다른 IP만 뽑을 때" 쓰는 도구입니다.

2-5. 가변과 불변 — 바꿀 수 있는가

네 바구니를 관통하는 개념이 하나 있습니다. 만들고 나서 내용을 바꿀 수 있는가?

  • 가변(mutable): 리스트, 딕셔너리, 셋 — 바꿀 수 있습니다.
  • 불변(immutable): 튜플, 그리고 문자열 — 바꿀 수 없습니다.

"문자열이 불변"이라는 말은 "abc"의 두 번째 글자를 직접 고칠 수 없다는 뜻입니다. 대신 새 문자열을 만들어 바꿔치기합니다. 이 구분은 나중에 "왜 이건 되고 저건 안 되지?"의 답이 됩니다.


3. 따라 하기

3-1. 리스트 만들고 다루기

basket.py 파일을 만들어 따라 씁니다.

ips = ["1.1.1.1", "8.8.8.8"]
print(ips)
print(ips[0])    # 첫 번째 칸
print(ips[-1])   # 마지막 칸 (뒤에서 세기)
ips.append("9.9.9.9")  # 끝에 추가
print(ips)
print(len(ips))  # 칸 수
['1.1.1.1', '8.8.8.8']
1.1.1.1
8.8.8.8
['1.1.1.1', '8.8.8.8', '9.9.9.9']
3

(2026-09-09 실측.)

읽는 법: ips[0]은 첫 칸, ips[-1]은 "뒤에서 첫 칸"이라는 편리한 표현입니다. .append()는 끝에 하나를 얹고, len()은 칸 수를 셉니다. 점(.)은 "~의"라고 읽으면 됩니다. ips.append는 "ips의 추가 기능"입니다. 이 네 동작(꺼내기·추가·세기)이 리스트 사용의 대부분입니다.

3-2. 슬라이싱 — 일부만 떼어 내기

ips = ["1.1.1.1", "8.8.8.8", "9.9.9.9", "114.114.114.114"]
print(ips[0:2])
print(ips[2:])
print(ips[:2])
print(ips[1:3])
print(ips[-2:])
['1.1.1.1', '8.8.8.8']
['9.9.9.9', '114.114.114.114']
['1.1.1.1', '8.8.8.8']
['8.8.8.8', '9.9.9.9']
['9.9.9.9', '114.114.114.114']

(2026-09-09 실측.)

읽는 법: ips[0:2]는 "0번부터 2번 앞까지"입니다. 끝 번호는 포함하지 않습니다. 0:2인데 0번과 1번만 나옵니다 — 오늘의 첫 함정입니다. 콜론 한쪽을 비우면 "처음부터" 또는 "끝까지"입니다. 슬라이싱(slicing)은 로그의 "최근 10줄만" 같은 조각내기의 기본기입니다.

3-3. 문자열 조작 — 글자도 리스트처럼

문자열은 글자의 나열이라 리스트와 비슷한 조작이 됩니다.

log = "192.168.0.23 - - GET /admin HTTP/1.1"
print(log[0:13])                   # 앞부분 자르기
print(log.split(" "))              # 공백으로 쪼개 리스트로
print(log.replace("GET", "POST"))  # 바꿔치기한 새 문자열
print("admin" in log)              # 포함 여부 검사
192.168.0.23 
['192.168.0.23', '-', '-', 'GET', '/admin', 'HTTP/1.1']
192.168.0.23 - - POST /admin HTTP/1.1
True

(2026-09-09 실측. 첫 줄 끝에 공백 하나가 포함된 것까지 원래 출력 그대로입니다.)

읽는 법: split(" ")은 공백을 칼 삼아 쪼개 리스트로 만듭니다. replace는 원본을 고치는 게 아니라 바꿔치기한 새 문자열을 돌려줍니다(문자열은 불변이라서). in은 "포함되어 있나?"라는 참/거짓 질문입니다. 보안 공부에서 다루는 데이터의 상당수가 텍스트 로그이고, 쪼개고·찾고·바꾸는 이 세 기술이 로그 분석의 출발점입니다.

3-4. 딕셔너리 — 이름표로 관리하기

host = {"ip": "192.168.0.23", "os": "ubuntu", "port": 22}
print(host["os"])
host["user"] = "lee"     # 새 항목 추가
host["port"] = 2222      # 기존 항목 수정
print(host)
print(host.get("location"))        # 없는 키를 안전하게 묻기
print(host.get("location", "모름"))  # 없으면 대신 돌려줄 값 지정
ubuntu
{'ip': '192.168.0.23', 'os': 'ubuntu', 'port': 2222, 'user': 'lee'}
None
모름

(2026-09-09 실측.)

읽는 법: 추가와 수정이 같은 모양(host["키"] = 값)이라는 것이 포인트입니다. 그리고 없는 키를 host["location"]처럼 직접 물으면 KeyError 오류가 나지만, .get()으로 물으면 오류 대신 None(없음이라는 특별한 값)이 돌아옵니다. .get(키, 기본값)의 두 번째 칸은 "없으면 이걸로 대신"입니다. 실전 코드에서는 .get()이 안전벨트입니다.

3-5. 셋으로 중복 제거하기

visitors = ["1.1.1.1", "8.8.8.8", "1.1.1.1", "9.9.9.9", "8.8.8.8"]
unique = set(visitors)
print(unique)
print(len(unique))
{'8.8.8.8', '9.9.9.9', '1.1.1.1'}
3

(2026-09-09 실측.)

읽는 법: 다섯 번의 방문이 실제로는 세 명의 방문자였습니다. set()으로 감싸는 순간 중복이 증발합니다. 실측 출력에서 순서가 넣은 순서와 다르게 보이는데, 셋은 원래 순서를 보장하지 않는 바구니입니다. "중복 없음"만 약속합니다.

3-6. 바구니 고르기 연습

상황을 읽고 어느 바구니가 맞는지 골라 보세요. 답과 이유를 노트에 적습니다.

  1. 오늘 방문한 IP를 순서대로 기록하고 싶다.
  2. 서버의 IP·운영체제·열린 포트를 함께 저장하고 싶다.
  3. 중복 제거된 방문자 수를 세고 싶다.
  4. 회사의 위도·경도처럼 절대 변하면 안 되는 값 쌍을 저장하고 싶다.

정답: 1번 리스트(순서가 의미 있음), 2번 딕셔너리(이름표로 찾음), 3번 셋(중복 제거), 4번 튜플(봉인). 정답을 맞히는 것보다 이유를 말하는 것이 중요합니다. 이유가 곧 실전의 판단 근거입니다.

3-7. 중첩 바구니 — 바구니 안의 바구니

실전 데이터는 겹겹입니다. "서버 목록"이면서 각 서버가 "정보 묶음"이라면 이렇게 담습니다.

servers = [
    {"name": "web", "ip": "192.168.0.10", "ports": [80, 443]},
    {"name": "db",  "ip": "192.168.0.20", "ports": [3306]},
]
print(servers[0]["ip"])
print(servers[1]["ports"][0])
print(len(servers[0]["ports"]))
192.168.0.10
3306
2

(2026-09-09 실측.)

읽는 법: 리스트 안에 딕셔너리, 그 안에 또 리스트입니다. 읽는 순서는 왼쪽부터. servers[1]은 두 번째 서버(딕셔너리), ["ports"]는 그 서버의 포트 리스트, [0]은 그 리스트의 첫 값입니다. 껍질을 하나씩 벗기는 것과 같습니다. 나중에 만날 API 응답, 설정 파일, 스캔 결과는 전부 이런 중첩(nested) 구조입니다. "왼쪽부터 한 껍질씩" 원칙만 기억하면 깊은 구조도 두렵지 않습니다.

직접 해 보기: servers[1]["ports"]에 5432를 추가하는 코드를 써 보세요. 힌트: 꺼내는 것과 같은 경로로 접근해 append를 붙입니다. 정답은 5번 섹션의 체크리스트 해설이 아니라 바로 여기에 — servers[1]["ports"].append(5432)입니다.


4. 미션과 연습문제

미션 — 학생 점수 관리 프로그램

scores.py 파일을 만들고 다음을 스스로 조립하세요.

  1. 딕셔너리에 학생 네 명의 이름과 점수를 저장한다. 예: {"민수": 85, "지영": 92, "철수": 78, "하늘": 95}
  2. 새 학생 한 명을 입력받아 추가한다 (이름과 점수, 점수는 int() 변환).
  3. 전체 학생의 이름과 점수를 한 줄씩 출력한다. (힌트: 딕셔너리.items()를 for로 돌리면 이름표와 값이 함께 나옵니다. for가 아직 어색하면 딕셔너리를 통째로 print해도 인정)
  4. 평균 점수를 계산해 출력한다. (힌트: 점수 리스트는 list(딕셔너리.values()), 합계는 sum(), 개수는 len())
  5. 최고 점수를 출력한다. (힌트: max(점수리스트))

완성 후 스스로 채점합니다. 평균이 소수점까지 정확한지, 새 학생이 평균에 포함되었는지 확인하세요.

연습문제

문제 1. ips = ["1.1.1.1", "8.8.8.8", "9.9.9.9"]에서 ips[3]을 출력하면 어떻게 되며, 왜 그런가요?

문제 2. ips[1:3]ips[1:2]의 결과 차이를 "끝 번호 미포함" 규칙으로 설명해 보세요.

문제 3. 딕셔너리에서 없을 수도 있는 키를 조회할 때 host["location"] 대신 host.get("location")을 권하는 이유는 무엇인가요? 각각 없는 키를 물었을 때 무슨 일이 일어나나요?

문제 4. 로그에서 "오늘 접속한 서로 다른 IP의 개수"를 구하려 합니다. 어느 바구니를 써야 하며, 그 이유는 무엇인가요? 셋을 쓸 때 포기해야 하는 것 하나는 무엇인가요?


5. 모범 답안과 완료 기준

미션 모범 답안

scores = {"민수": 85, "지영": 92, "철수": 78, "하늘": 95}

new_name = input("새 학생 이름: ")
new_score = int(input("점수: "))   # input은 글자이므로 변환
scores[new_name] = new_score

print("=== 전체 점수 ===")
for name, score in scores.items():
    print(f"{name}: {score}점")

values = list(scores.values())
print(f"평균: {sum(values) / len(values)}점")
print(f"최고 점수: {max(values)}점")

실행 예시 (새 학생 영희, 88 입력 시):

=== 전체 점수 ===
민수: 85점
지영: 92점
철수: 78점
하늘: 95점
영희: 88점
평균: 87.6점
최고 점수: 95점

검증하는 법: ① 새 학생이 목록과 평균에 포함되었는지 확인합니다. ② 평균을 손으로 검산합니다 — (85+92+78+95+88) ÷ 5 = 87.6. ③ 점수 입력에서 int()를 빠뜨리면 sum() 단계에서 TypeError가 나므로, 그 오류가 곧 검증 신호입니다.

연습문제 해답

문제 1 해답. IndexError: list index out of range 오류가 납니다 (2026-09-09 실측 메시지). 인덱스는 0부터 세므로 세 칸짜리 리스트의 마지막은 ips[2]이고, ips[3]은 존재하지 않는 칸이기 때문입니다.

문제 2 해답. 둘 다 1번부터 시작하지만 끝 번호는 포함하지 않으므로, ips[1:3]은 1번과 2번 두 개, ips[1:2]는 1번 하나만 나옵니다. 끝 - 시작이 곧 조각의 개수라고 기억하면 편합니다.

문제 3 해답. host["location"]은 키가 없으면 KeyError로 프로그램이 멈추지만, .get()은 오류 대신 None(또는 지정한 기본값)을 돌려주어 프로그램이 계속 진행됩니다 (2026-09-09 실측: host.get("location")None, host.get("location", "모름")모름). 없을 수 있는 값을 다룰 때는 .get()이 안전합니다.

문제 4 해답. 셋을 씁니다. 같은 값을 두 번 넣어도 하나만 남는 "중복 불가" 성질이 그대로 정답이기 때문입니다. len(set(visitors)) 한 줄로 끝납니다 (2026-09-09 실측: 다섯 번 방문 → 3개). 포기하는 것은 순서입니다 — 셋은 넣은 순서를 보장하지 않습니다.

완료 기준 체크리스트

  • [ ] 리스트의 인덱스(0부터)와 ips[-1](마지막 칸)을 설명할 수 있다
  • [ ] 슬라이싱의 "끝 번호 미포함" 규칙을 예시와 함께 설명할 수 있다
  • [ ] append, len, split, replace, join(" ".join(리스트))을 쓸 수 있다
  • [ ] 딕셔너리에 항목을 추가·수정·조회할 수 있고 .get()의 안전성을 안다
  • [ ] set()으로 중복을 제거하고, 순서가 보장되지 않음을 안다
  • [ ] 중첩 구조에서 "왼쪽부터 한 껍질씩" 규칙으로 값을 꺼낼 수 있다
  • [ ] 미션(학생 점수 관리)을 완성하고 평균을 손으로 검산했다

6. 흔한 실수와 해결

벽 1. IndexError: list index out of range

증상 (2026-09-09 실측):

Traceback (most recent call last):
  File "...\s42_indexerror.py", line 2, in <module>
    print(ips[5])
          ~~~^^^
IndexError: list index out of range

원인: 칸이 두 개뿐인 리스트에서 5번 칸을 꺼낸 것입니다. 0부터 세니 두 칸의 마지막은 ips[1]입니다.
해결: 꺼내기 전에 len(ips)로 칸 수를 확인하는 습관을 들이고, "마지막"이 필요하면 번호를 세지 말고 ips[-1]을 씁니다.

벽 2. KeyError가 난다

증상: host["lacation"]에서 KeyError.
원인: 두 가지 경우입니다. 키가 진짜 없거나, 오타(위 예시는 location의 오타). 딕셔너리는 키 철자에 엄격합니다.
해결: 먼저 print(host)로 실제 키 목록을 눈으로 확인합니다. 없을 수 있는 키는 처음부터 .get()으로 묻는 습관을 들입니다.

벽 3. 리스트를 출력하면 대괄호와 따옴표가 보인다

증상: print(ips)['1.1.1.1', '8.8.8.8']로 나옵니다. 깔끔하게 보고 싶습니다.
원인: 리스트를 통째로 출력하면 파이썬이 바구니째 보여 줍니다.
해결: " ".join(ips)으로 "공백으로 이어 붙인 하나의 문자열"을 만들어 출력하세요. joinsplit의 거꾸로입니다. 더 정교한 한 줄씩 출력은 다음 단계(반복문)에서 배웁니다.

벽 4. set으로 바꿨더니 순서가 뒤죽박죽이다

증상 (2026-09-09 실측): {'1.1.1.1', '8.8.8.8', ...}로 넣었는데 {'8.8.8.8', '9.9.9.9', '1.1.1.1'}처럼 순서가 다르게 나옵니다.
원인: 셋은 순서를 보장하지 않습니다. "중복 없음"만 약속하는 바구니입니다.
해결: 순서가 필요하면 리스트를 쓰고, 둘 다 필요하면 sorted(set(visitors))로 정렬된 리스트를 얻습니다. 도구의 약속을 아는 것이 실력입니다.


7. 정리

오늘의 개념

개념 한 줄 설명
리스트 [] 순서 있는 바구니 — 인덱스는 0부터
튜플 () 봉인된 리스트 — 만들면 바꿀 수 없음
딕셔너리 {} "키: 값" 쌍의 모음 — 이름표로 찾는다
set() 중복 불가 바구니 — 순서는 보장 안 됨
가변/불변 만들고 나서 바꿀 수 있는가의 구분
인덱스·슬라이싱 칸 번호(0부터)와 [시작:끝] 조각내기(끝 미포함)
중첩 구조 바구니 안의 바구니 — 왼쪽부터 한 껍질씩

오늘의 문법

문법 하는 일
리스트[i] / 리스트[-1] i번째 값 꺼내기 / 마지막 값
리스트[a:b] a부터 b 앞까지 조각
리스트.append(값) / len(리스트) 끝에 추가 / 칸 수
문자열.split(" ") / " ".join(리스트) 쪼개 리스트로 / 이어 붙여 문자열로
문자열.replace(구, 신) / x in 문자열 바꿔치기한 새 문자열 / 포함 여부
딕셔너리[키] = 값 추가와 수정 (같은 모양)
딕셔너리.get(키, 기본값) 없어도 오류 없이 조회
set(리스트) 중복 제거

문법보다 중요한 감각

같은 문제도 바구니를 잘못 고르면 코드가 세 배로 길어집니다. 중복을 수동으로 검사하는 리스트 코드 백 줄이, 셋 하나면 한 줄로 끝납니다. 코드를 다 쓰고 나서 한 번만 물어보세요. "이 바구니가 맞나?" 이 회고가 자료구조 감각을 가장 빨리 키웁니다.

그리고 오늘의 네 바구니는 로그 분석의 재료입니다. 로그 한 줄을 쪼개고(split), IP별로 집계하고(딕셔너리), 유일 방문자를 세는(셋) 일이 전부 오늘 배운 도구의 일입니다. 이 분석의 대상은 언제나 내 시스템의 로그 또는 합법적으로 제공된 데이터입니다. 모든 공격 실습은 내 랩·합법 플랫폼에서만. 실서비스 무단 공격은 범죄입니다.


전부 체크되면 Step 42 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.