Step 52. 클래스와 객체지향 — 데이터와 동작을 한 상자에

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. 미션과 연습문제

미션 — 내 랩 자산 관리기

  1. 위 Host 클래스를 바탕으로 인스턴스 셋 이상을 만듭니다: 공유기, 내 PC, 가상머신
  2. 전원 상태와 열린 포트가 서로 다르게 설정합니다
  3. 전체 자산을 리스트에 담고, for 반복으로 info()를 찍는 "자산 현황판"을 출력합니다
  4. 켜져 있는 자산만 골라 포트 목록을 출력하는 기능을 추가합니다 (Step 43 복습)
  5. 도전: 현황판을 자산현황.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 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.