Step 52. 클래스와 객체지향 — 데이터와 동작을 한 상자에
Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★☆☆☆ | 예상 소요 시간 3시간
전제: Step 41~51 완료. 함수(Step 44)와 딕셔너리(Step 42)를 압니다.
- 준비물: 파이썬이 설치된 PC, 텍스트 에디터, 터미널.
- 주의: 오늘 실습은 100% 안전합니다. 네트워크에 접속하는 것도, 시스템을 건드리는 것도 없이, 전부 내 컴퓨터 안의 파이썬 코드 연습입니다.
Step 33에서 네트워크의 컴퓨터들을 딕셔너리로 옮긴 적이 있습니다:
pc1 = {"name": "데스크탑", "ip": "192.168.0.10", "on": True}
pc2 = {"name": "노트북", "ip": "192.168.0.20", "on": False}
이 방식은 컴퓨터가 스무 대쯤 되면 무너집니다. pc1["name"]을 치다가 철자를 틀려도 아무도 알려 주지 않고, "전원을 켠다" 같은 동작을 붙일 자리도 없습니다. 데이터만 있고 행동이 없는 것입니다.
오늘의 주제 객체지향(object-oriented programming)은 이 문제의 해답입니다. 관련된 데이터와 동작을 한 상자에 묶는 사고방식으로, 상자의 설계도가 클래스(class), 설계도로 찍어 낸 실제 물건이 인스턴스(instance)입니다. 처음엔 어색합니다. 정상입니다. 하지만 이 사고방식은 대규모 프로그램과 보안 도구들의 공용어입니다 — 스캐너도, 분석 도구도, 전부 클래스로 되어 있습니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 클래스(설계도)와 인스턴스(물건)의 관계를 설명한다
- 속성(데이터)과 메서드(동작)를 구분해 만든다
__init__과self가 왜 필요한지 설명한다- 클래스 하나로 인스턴스 여러 개를 만들어 각자 다른 상태를 확인한다
- self 관련 오류(TypeError, NameError, AttributeError)를 보고 스스로 고친다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 파이썬 3.12. 표준 문법만 사용 — 새로 설치하거나 import하는 것 없음 |
| 오늘의 문법 | class 이름:, def __init__(self, ...), self.속성, def 메서드(self):, 이름(...)로 인스턴스 생성 |
| 필요한 개념 | 클래스와 인스턴스, 속성과 메서드, self, 특별 메서드(init) |
2-1. 설계도와 물건 — 클래스와 인스턴스
붕어빵 틀과 붕어빵으로 생각하면 쉽습니다. 클래스는 틀입니다. 하나뿐입니다. 인스턴스는 그 틀로 찍어 낸 붕어빵으로, 원하는 만큼 만들 수 있고 저마다 속(속성 값)이 다릅니다. Host 클래스 하나로 공유기 인스턴스, 내 PC 인스턴스, 가상머신 인스턴스를 찍어 내는 식입니다.
2-2. 속성과 메서드 — 데이터와 동작
속성(attribute)은 그 물건이 가진 데이터입니다. Host의 이름, IP, 전원 상태. 메서드(method)는 그 물건이 할 수 있는 동작입니다 — "전원 켜기", "정보 출력하기". 함수와 같은데 클래스 안에 속해 있다는 점이 다르고, 점(.)으로 부릅니다. pc1.boot()처럼요.
2-3. self — "나 자신"이라는 대명사
메서드 안에서 "이 물건 자신"을 가리키는 말이 self입니다. 붕어빵마다 팥이 다르듯 인스턴스마다 속성 값이 다른데, 메서드가 "내 팥"을 가리키려면 대명사가 필요하고 그게 self입니다. 파이썬은 메서드의 첫 번째 인자로 self를 적는 규칙이 있습니다.
2-4. init — 태어날 때 하는 일
인스턴스가 만들어지는 순간 자동으로 실행되는 특별한 메서드입니다. "초기화(initialize)"의 준말로, 첫 속성 값들을 받아 심는 일을 여기서 합니다. 앞뒤 밑줄 두 개씩(__)은 파이썬의 특별 메서드 표식입니다.
3. 따라 하기
3-1. 첫 클래스 — 설계도 그리기
host.py를 만듭니다:
class Host:
def __init__(self, name, ip):
self.name = name
self.ip = ip
self.on = False
def boot(self):
self.on = True
print(self.name, "전원을 켰습니다.")
def info(self):
상태 = "켜짐" if self.on else "꺼짐"
print(self.name, "|", self.ip, "|", 상태)
코드 읽는 법: class Host:가 설계도의 시작이고, 들여쓰기된 것들이 설계도의 내용입니다. __init__은 만들어질 때 name과 ip를 받아 self.name, self.ip에 심습니다. self.on = False는 "전원은 기본적으로 꺼져 있게"라는 초기 설정입니다.
왜 하는가: 데이터(name, ip, on)와 동작(boot, info)이 한 상자에 들어간 모습을 먼저 눈으로 보세요.
3-2. 인스턴스 찍어 내기
같은 파일에 이어서 적습니다:
pc1 = Host("데스크탑", "192.168.0.10")
pc2 = Host("노트북", "192.168.0.20")
pc1.info()
pc1.boot()
pc2.info()
pc1.info()
데스크탑 | 192.168.0.10 | 꺼짐
데스크탑 전원을 켰습니다.
노트북 | 192.168.0.20 | 꺼짐
데스크탑 | 192.168.0.10 | 켜짐
(2026-09-09 실측.)
출력 읽는 법: Host("데스크탑", "192.168.0.10") 한 줄이 "틀에 값을 넣고 찍는" 순간입니다. 이때 __init__이 자동 실행됩니다. pc1.boot()는 pc1만 켭니다 — pc2는 여전히 꺼져 있습니다. 같은 틀에서 나왔지만 상태는 각자입니다.
예측해 보기:
pc2.boot()없이pc2.info()를 다시 호출하면 상태가 뭐라고 나올까요? 예측하고 확인해 보세요.
왜 하는가: "인스턴스는 저마다 독립된 상태를 가진다"가 객체지향의 절반입니다.
3-3. self의 정체 확인
self_test.py를 만듭니다:
class Cat:
def whoami(self):
print("나는", self, "입니다.")
a = Cat()
b = Cat()
a.whoami()
b.whoami()
나는 <__main__.Cat object at 0x0000028E41F3F620> 입니다.
나는 <__main__.Cat object at 0x0000028E41F3CCB0> 입니다.
(2026-09-09 실측. 0x… 번지는 실행할 때마다 달라집니다.)
출력 읽는 법: a와 b의 self가 서로 다른 주소(0x… 번지)를 가리킵니다. self는 "호출한 그 인스턴스 자신"이라는 증거입니다. a.whoami()를 파이썬은 내부에서 "Cat.whoami(a)"처럼 a를 self에 넣어 실행합니다. 메서드에 self를 적는 이유가 바로 이 배달 때문입니다.
3-4. 딕셔너리 방식과 비교 — 왜 클래스인가
# 딕셔너리 방식: 데이터만 있다
pc_old = {"name": "데스크탑", "ip": "192.168.0.10", "on": False}
pc_old["on"] = True # 전원 켜기를 밖에서 일일이 대신한다
# 클래스 방식: 데이터와 동작이 같이 있다
pc_new = Host("데스크탑", "192.168.0.10")
pc_new.boot() # 물건에게 시킨다
읽는 법: 딕셔너리는 데이터의 묶음일 뿐이라 행동은 전부 바깥 코드가 대신합니다. 클래스는 물건 스스로 행동합니다. 대상이 늘어날수록 물건에게 시키는 쪽이 관리하기 쉽습니다. 오타도 클래스 쪽이 잘 잡아 줍니다 — 없는 메서드를 부르면 파이썬이 즉시 AttributeError로 알려 주지만, 딕셔너리 키 오타는 조용히 지나가는 경우가 많습니다.
3-5. 메서드가 데이터를 바꾸는 연습 — 포트 스캔 기록
Host 클래스를 확장합니다 (host2.py):
class Host:
def __init__(self, name, ip):
self.name = name
self.ip = ip
self.on = False
self.open_ports = []
def boot(self):
self.on = True
print(self.name, "전원을 켰습니다.")
def add_port(self, port):
if port not in self.open_ports:
self.open_ports.append(port)
print(self.name, "열린 포트:", self.open_ports)
def info(self):
상태 = "켜짐" if self.on else "꺼짐"
print(self.name, "|", self.ip, "|", 상태, "| 열린 포트:", self.open_ports)
실행해 봅니다:
gw = Host("공유기", "192.168.0.1")
gw.boot()
gw.add_port(80)
gw.add_port(443)
gw.add_port(80)
gw.info()
공유기 전원을 켰습니다.
공유기 열린 포트: [80]
공유기 열린 포트: [80, 443]
공유기 열린 포트: [80, 443]
공유기 | 192.168.0.1 | 켜짐 | 열린 포트: [80, 443]
(2026-09-09 실측.)
출력 읽는 법: self.open_ports라는 빈 리스트를 __init__에서 준비하고, add_port가 중복을 검사하며 채웁니다. 80을 두 번 넣어도 한 번만 들어간 것을 보세요. 메서드가 자기 데이터를 스스로 관리하는 모습 — 스캐너의 원형입니다.
3-6. 메서드끼리 부르기 — self의 또 다른 일
self는 속성뿐 아니라 메서드도 가리킵니다. Host 클래스 안에 메서드를 하나 더 추가합니다 (다른 메서드들과 같은 깊이로 들여쓰기):
def check(self):
if not self.on:
self.boot()
self.info()
실행합니다:
pc = Host("테스트기", "192.168.0.99")
pc.check()
pc.check()
테스트기 전원을 켰습니다.
테스트기 | 192.168.0.99 | 켜짐 | 열린 포트: []
테스트기 | 192.168.0.99 | 켜짐 | 열린 포트: []
(2026-09-09 실측.)
출력 읽는 법: check는 "꺼져 있으면 켜고(boot), 그다음 정보를 찍는다(info)"는 동작입니다. self.boot()는 "나의 boot를 불러라"입니다. 두 번째 pc.check()에서는 이미 켜져 있으니 "전원을 켰습니다"가 다시 나오지 않습니다 — 조건문이 길목을 지키고 있기 때문입니다.
왜 하는가: 메서드가 메서드를 부르면 큰 동작을 작은 동작들의 조립으로 만들 수 있습니다. 함수끼리 부르던 것(Step 44)의 클래스판입니다.
4. 미션과 연습문제
미션 — 내 랩 자산 관리기
- 위 Host 클래스를 바탕으로 인스턴스 셋 이상을 만듭니다: 공유기, 내 PC, 가상머신
- 전원 상태와 열린 포트가 서로 다르게 설정합니다
- 전체 자산을 리스트에 담고, for 반복으로
info()를 찍는 "자산 현황판"을 출력합니다 - 켜져 있는 자산만 골라 포트 목록을 출력하는 기능을 추가합니다 (Step 43 복습)
- 도전: 현황판을
자산현황.txt로 저장하게 합니다 (Step 45 복습)
마지막으로, 딕셔너리 나열보다 이 방식이 편한 이유를 한 줄로 노트에 적어 보세요. 그 한 줄이 객체지향을 이해했다는 증거입니다.
연습문제
문제 1. 클래스와 인스턴스의 차이를 설계도/물건 비유로 설명하고, "인스턴스마다 다르게 가지는 것"이 무엇인지 말해 보세요.
문제 2. def info():처럼 self를 빼고 메서드를 정의한 뒤 pc.info()를 호출하면 어떤 일이 일어나나요? 파이썬이 self를 어떻게 전달하기 때문인가요?
문제 3. __init__을 _init_으로 잘못 적으면 어떤 일이 일어나며, 그 뒤 어떤 오류가 나타나나요?
문제 4. 어떤 상황에서는 클래스를 쓰지 않는 것이 낫나요? "클래스를 꺼낼 때"의 기준 두 가지를 말해 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
class Host:
def __init__(self, name, ip):
self.name = name
self.ip = ip
self.on = False
self.open_ports = []
def boot(self):
self.on = True
def add_port(self, port):
if port not in self.open_ports:
self.open_ports.append(port)
def info(self):
상태 = "켜짐" if self.on else "꺼짐"
return f"{self.name} | {self.ip} | {상태} | 열린 포트: {self.open_ports}"
assets = [
Host("공유기", "192.168.0.1"),
Host("내PC", "192.168.0.10"),
Host("가상머신", "192.168.0.50"),
]
assets[0].boot()
assets[0].add_port(80)
assets[0].add_port(443)
assets[1].boot()
lines = ["=== 랩 자산 현황 ==="]
for h in assets:
lines.append(h.info())
lines.append("--- 켜져 있는 자산의 포트 ---")
for h in assets:
if h.on:
lines.append(f"{h.name}: {h.open_ports}")
report = "\n".join(lines)
print(report)
with open("자산현황.txt", "w", encoding="utf-8") as f:
f.write(report)
검증하는 법: ① 현황판에 세 자산이 각자 다른 상태로 출력되는가. ② "켜져 있는 자산" 목록에 꺼진 가상머신이 빠졌는가. ③ 자산현황.txt가 생성됐는가. ④ info()가 print 대신 문자열을 돌려주도록(return) 바꾼 것을 주목하세요 — 이래야 화면 출력과 파일 저장에 같은 결과를 재사용할 수 있습니다. "출력하는 함수"와 "만드는 함수"를 나누는 감각은 앞으로 계속 씁니다.
연습문제 해답
문제 1 해답. 클래스는 설계도(틀)로 하나뿐이고, 인스턴스는 그 틀로 찍어 낸 실제 물건으로 여러 개 만들 수 있습니다. 인스턴스마다 다르게 가지는 것은 속성의 값(이름, IP, 전원 상태 등)입니다. 메서드의 정의는 공유하지만, self를 통해 접근하는 상태는 각자의 것입니다.
문제 2 해답. 오류가 납니다. 파이썬은 pc.info()를 호출할 때 인스턴스 pc를 몰래 첫 인자로 넣어 주는데, 정의에 self가 없으면 그 인자가 설 자리를 잃습니다. 2026-09-09 실측 오류: TypeError: Host.info() takes 0 positional arguments but 1 was given. 클래스 안의 메서드는 첫 인자가 언제나 self입니다.
문제 3 해답. 파이썬은 _init_을 특별 메서드로 인식하지 못해 자동 실행하지 않습니다. 그저 평범한 (호출되지 않는) 메서드가 됩니다. 따라서 __init__에서 준비했어야 할 속성이 없는 상태가 되고, 그 속성에 접근하는 순간 AttributeError: 'Host' object has no attribute 'on' 같은 오류가 납니다 (2026-09-09 실측). 앞뒤 밑줄 두 개씩 정확히 적으세요.
문제 4 해답. 열 줄짜리 일회성 스크립트에 설계도는 과합니다. 클래스를 꺼낼 때의 기준은 ① 데이터와 동작이 함께 다닐 때, ② 같은 종류의 것이 여러 개 필요할 때입니다. 지금은 문법에 익숙해지는 것이 목표이고, 언제 쓸지의 감각은 코드를 더 써 보며 생깁니다.
완료 기준 체크리스트
- [ ] 클래스(설계도)와 인스턴스(물건)의 차이를 설명할 수 있다
- [ ] 속성과 메서드를 구분해 만들 수 있다
- [ ]
__init__이 언제, 왜 자동 실행되는지 설명할 수 있다 - [ ] self가 "호출한 인스턴스 자신"임을 설명할 수 있다
- [ ] self 빠뜨림(TypeError), self 누락(NameError),
__init__오타(AttributeError)를 구분해 고칠 수 있다 - [ ] 메서드가 같은 클래스의 다른 메서드를 self로 부를 수 있다
- [ ] 미션: 랩 자산 관리기를 완성하고 자산현황.txt를 저장했다
6. 흔한 실수와 해결
벽 1. TypeError: takes 0 positional arguments but 1 was given
증상 (2026-09-09 실측):
TypeError: Host.info() takes 0 positional arguments but 1 was given
원인: 메서드 정의에 self를 빠뜨렸습니다. def info():라고 적으면 파이썬이 몰래 넣어 주는 self(그 "1")가 설 자리를 잃습니다.
해결: 클래스 안의 메서드는 첫 인자가 언제나 self입니다 — def info(self):. 이 오류는 모든 초보가 한 번은 겪는 통과의례입니다.
벽 2. NameError: name ‘name’ is not defined
증상 (2026-09-09 실측):
NameError: name 'name' is not defined. Did you mean: 'self.name'?
원인: 메서드 안에서 self.name을 그냥 name이라고 적었습니다. 내 속성은 반드시 self를 통해 가리켜야 합니다.
해결: "내 것이면 self."라는 주문을 외우세요. 요즘 파이썬은 실측 메시지처럼 정답까지 힌트로 보여 줍니다 — 오류 메시지의 마지막 줄을 읽는 습관이 곧 답입니다.
벽 3. 괄호 없이 클래스를 쓴다
증상: pc = Host라고만 적고 pc.boot()를 부르면 이상한 오류가 납니다.
원인: Host는 설계도 자체입니다. 괄호 Host(...)를 붙여야 인스턴스가 찍혀 나옵니다.
해결: 설계도에게 일을 시킬 수는 없습니다. 괄호가 "찍어 내!"라는 명령입니다 — pc = Host("이름", "IP").
벽 4. init 이름을 틀린다
증상: 인스턴스를 만들었는데 속성이 없다는 오류 (2026-09-09 실측):
AttributeError: 'Host' object has no attribute 'on'
원인: _init_이나 init처럼 밑줄을 틀리면, 파이썬은 그것을 그냥 평범한 메서드로 보아 자동 실행하지 않습니다.
해결: 앞뒤 밑줄 두 개씩, __init__. 특별 메서드의 표식을 정확히 적으세요.
벽 5. 클래스를 추가할 때 들여쓰기가 꼬인다
증상: 3-6의 check를 추가했더니 IndentationError나 "메서드가 없다"는 오류.
원인: 메서드는 클래스 안에, 메서드의 본문은 그 안에 — 들여쓰기 깊이가 소속을 결정합니다.
해결: check의 def 줄이 다른 메서드들의 def 줄과 같은 깊이(공백 4개)인지 확인하세요. 클래스 밖에 적으면 그냥 함수가 되어 버립니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 클래스 | 설계도(틀) — 하나뿐 |
| 인스턴스 | 틀로 찍어 낸 물건 — 상태가 각자 다름 |
| 속성(attribute) | 물건이 기억하는 데이터 |
| 메서드(method) | 물건이 할 수 있는 동작 |
| self | 메서드 안에서 "나 자신"을 가리키는 대명사 |
__init__ |
인스턴스가 태어날 때 자동 실행되는 초기 설정 |
오늘의 문법
| 문법 | 하는 일 |
|---|---|
class Host: |
설계도 시작 |
def __init__(self, name, ip): |
생성 시 자동 실행, 첫 값 심기 |
self.name = name |
내 속성에 값 저장 |
pc = Host("이름", "IP") |
인스턴스 찍어 내기 |
pc.boot() |
물건에게 동작 시키기 |
self.boot() |
메서드 안에서 내 다른 메서드 부르기 |
명령어보다 중요한 감각
클래스를 만들 때 코드부터 치지 마세요. 노트에 두 줄 먼저: (가) 이 물건이 기억할 것들 — 이름, IP, 전원, 포트 목록. (나) 이 물건이 할 일들 — 켜기, 포트 기록, 정보 출력. (가)가 속성의 후보이고 (나)가 메서드의 후보입니다. 이 두 줄이 클래스의 뼈대입니다.
오늘의 수확을 두 가지로 정리합니다. 첫째, 오류 메시지 판독표: TypeError면 인자 개수, NameError면 이름, AttributeError면 속성·메서드 철자. 셋 모두 오늘 실측으로 만났고, 판독이 빨라질수록 여러분은 오류의 주인이 됩니다. 둘째, 유명한 보안 도구들의 코드를 열어 보면 전부 오늘 배운 모습입니다 — Scanner 클래스, Target 클래스, Report 클래스. 클래스를 읽을 수 있게 되면 남의 도구 코드가 "쓰는 설명서"에서 "고쳐 쓰는 설계도"로 바뀝니다. 도구를 만드는 사람과 쓰는 사람의 차이가 여기서 갈립니다.
전부 체크되면 Step 52 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.