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. 바구니 고르기 연습
상황을 읽고 어느 바구니가 맞는지 골라 보세요. 답과 이유를 노트에 적습니다.
- 오늘 방문한 IP를 순서대로 기록하고 싶다.
- 서버의 IP·운영체제·열린 포트를 함께 저장하고 싶다.
- 중복 제거된 방문자 수를 세고 싶다.
- 회사의 위도·경도처럼 절대 변하면 안 되는 값 쌍을 저장하고 싶다.
정답: 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 파일을 만들고 다음을 스스로 조립하세요.
- 딕셔너리에 학생 네 명의 이름과 점수를 저장한다. 예:
{"민수": 85, "지영": 92, "철수": 78, "하늘": 95} - 새 학생 한 명을 입력받아 추가한다 (이름과 점수, 점수는
int()변환). - 전체 학생의 이름과 점수를 한 줄씩 출력한다. (힌트:
딕셔너리.items()를 for로 돌리면 이름표와 값이 함께 나옵니다. for가 아직 어색하면 딕셔너리를 통째로print해도 인정) - 평균 점수를 계산해 출력한다. (힌트: 점수 리스트는
list(딕셔너리.values()), 합계는sum(), 개수는len()) - 최고 점수를 출력한다. (힌트:
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)으로 "공백으로 이어 붙인 하나의 문자열"을 만들어 출력하세요. join은 split의 거꾸로입니다. 더 정교한 한 줄씩 출력은 다음 단계(반복문)에서 배웁니다.
벽 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 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.