5 minute read

클래스 만들기

클래스는 데이터와 기능을 묶어서 처리하기 위한 개념입니다. 어떤 클래스를 정의하고 나면 그 클래스의 인스턴스를 생성할 수 있게 됩니다. 클래스에서 정의하는 바에 따라, 인스턴스는 자신에게 속한 변수와 기능들을 다른 인스턴스로부터 독립적으로 다룰 수 있습니다.

구구절절 설명하는 것보다 직접 한 번 만들어 보는 것이 낫습니다. 여러분이 포켓몬스터 게임을 만들고 싶다고 상상해봅시다. 포켓몬스터 게임을 떠올리니 피카츄가 가장 먼저 떠오르고, 피카츄가 포켓몬 세계를 돌아다니며 몸통박치기와 백만 볼트로 기라성 같은 트레이너들을 물리치는 모습을 만들고 싶어졌습니다.

지금부터 ‘피카츄’ 클래스를 정의하고, 실제로 피카츄 클래스의 인스턴스를 만들고 동작시켜 보면서 파이썬에서의 클래스 관련 용어들과 사용법을 살펴보겠습니다.

무작정 만들어 보기

1
2
3
4
5
6
class Pikachu:
    def __init__(self):
        self.level = 1
        self.hit_point = 100
        self.atk = 20
        self.type = 'electric'

피카츄를 만들기 위한 클래스의 이름은 Pikachu로 정했습니다. 이 클래스는 네 가지 속성을 갖는데, 레벨(level), HP(hit_point), 공격력(atk), 그리고 타입(type)입니다. 물론 실제의 포켓몬스터 게임에 등장하는 피카츄에게는 상대 포켓몬과의 대결에서 선공권 획득 여부를 결정하는 속도 관련 능력치 등 게임의 세부 구현과 관련하여 필요한 다른 많은 속성(attribute)들이 존재할 것이지만, 우선 이 정도 속성들만을 가지고 간단하게 시작해봅시다. 이 Pikachu 클래스는 앞으로 우리가 실제로 사용할 피카츄 객체들을 만들어내기 위한 ‘설계도’입니다.

위 코드에 대해 자세히 설명하기 전에, 우선 이 설계도를 이용하여 당장 피카츄 한 마리를 만들어 보겠습니다. 아래의 코드는 Pikachu 클래스를 이용하여 피카츄 객체 하나를 a라는 이름으로 생성합니다.

1
>>> a = Pikachu()

피카츄 객체를 만들고 나면, 앞서 정의한 레벨, HP, 공격력과 포켓몬 타입을 확인할 수 있습니다. 피카츄 객체 a에서 이 네 가지 속성 값을 확인해 봅시다. 이 네 가지 값들은 Pikachu 클래스에 정의된 객체 변수이며, 동시에 Pikachu 클래스의 인스턴스 a에 종속되는 인스턴스 변수 라고 부릅니다.

1
2
3
4
5
6
7
8
>>> print(a.level)
1
>>> print(a.hit_point)
100
>>> print(a.atk)
20
>>> print(a.type)
electric

클래스, 객체, 인스턴스

위의 Pikachu 클래스 코드를 자세히 설명하기 전에, 우선 클래스객체, 인스턴스의 구분을 살펴봅시다. 각 용어의 의미를 설명하기 전에, 앞으로 일반적으로 자연스럽게 쓰이게 될 형태의 진술들을 위의 Pikachu 클래스 코드 예시를 이용해 나열해 보겠습니다.

우리는 앞 절에서 아래와 같은 작업을 하였습니다:

  • 포켓몬스터 게임의 일부가 될 피카츄 객체를 만들고 싶다고 생각했습니다.
  • 피카츄 객체를 만들기 위해, Pikachu 클래스를 정의하였습니다.
  • Pikachu 클래스를 이용하여, 실제로 게임 내에서 활동할 인스턴스 a를 생성하였습니다.

일반적으로 쓰이는 ‘객체’와 ‘클래스’, ‘인스턴스’의 구분은 대략적으로 아래와 같습니다:

  • 객체는 소프트웨어 세계에 실체화하고자 하는 대상입니다.
  • 클래스는 객체를 구현하기 위한 설계도입니다.
  • 인스턴스는 클래스를 이용해 실제로 객체를 구체화하여 특정 메모리 영역을 점유하게 된 것입니다.

객체와 인스턴스의 구분은 종종 모호합니다. 위에서는 피카츄 = 객체, a = 인스턴스라고 썼지만 우리의 상상 속 피카츄 객체를 실체화한 a에 대해 ‘a는 객체입니다’, ‘객체 a에 대해 생각해 봅시다’와 같이 진술하는 것도 틀리지 않습니다. 객체라는 말은 우리가 실체화하고자 하는 대상 자체를 논할 때 사용할 수도 있지만, 실제 코드가 실행되는 도중에 발생하는 독립된 실체를 가리키기 위해 사용할 수도 있습니다. 후자의 경우 객체라는 단어는 사실상 인스턴스와 같은 의미이며, 따라서 인스턴스 $\subset$ 객체라고 말할 수 있습니다. 하지만 ‘피카츄는 인스턴스입니다’ 혹은 ‘피카츄 인스턴스’라는 말은 조금 이상합니다. 우리가 a라는 이름으로 Pikachu 클래스의 인스턴스를 만들어내기 전까지는 우리가 ‘인스턴스’라고 부를수 있는, 피카츄 객체가 실제로 구체화된 무언가가 만들어지지 않았기 때문입니다.

또한 우리가 상상한 피카츄라는 객체를 구현하기 위해 Pikachu라는 클래스를 만들었고, 적어도 우리끼리는 피카츄 객체의 구현을 위해 Pikachu 클래스 외에는 다른 코드를 작성하지 않았다는 것을 알고 있으니, ‘피카츄 클래스’라고 말하는 것도 이 포스팅 안에서의 소통에는 문제가 없습니다. 그러나 만일 여러분이 윈도우즈용 포켓몬스터 게임과 게임보이용 포켓몬스터 게임을 동시에 개발하고 있는 팀의 일원이고, 여러분의 프로젝트에는 윈도우즈용 포켓몬스터 게임에 피카츄 객체를 실체화하기 위한 PikachuWindows 클래스와 게임보이용 포켓몬스터 게임에 피카츄 객체를 실체화하기 위한 PikachuGameboy가 구분되어 존재한다고 생각해 봅시다.

여러분은 윈도우즈용 포켓몬스터 게임 개발 팀의 일원입니다. 어제 점심 시간에 게임보이용 포켓몬스터 개발 팀원과 대화 중에 ‘내일까지 피카츄 클래스에서 피카츄를 풀 속성으로 바꾸기로 했다는데’라는 말을 들었는데, 최근 너무 바빴던 나머지 크로스체크 없이 여러분 팀의 코드에서 PikachuWindows 클래스의 type 속성을 전기(electric)에서 풀(grass)로 바꾸었습니다. 하지만 사실 피카츄를 풀 속성으로 바꾸는 것은 게임보이용 포켓몬스터 개발팀에서 만우절 이벤트로 준비한 것이었고, 윈도우즈용 포켓몬스터 게임에는 적용될 예정이 아닌 변경 사항이었습니다…

상황 설정이 코미디에 가깝기는 하지만, 이러한 관점에서 객체와 클래스 또한 구분됩니다. 우리는 피카츄 객체를 만들어내기 위해 Pikachu라는 클래스를 만들었지만, 다양한 이유로 이 구현은 Pikachu라는 한 종류의 클래스에 국한되지 않을 수 있습니다. 위의 예시에서는 PikachuWindows와 PikachuGameboy라는 두 가지 다른 구현이 존재하게 되었고, ‘피카츄 클래스’라는 말만으로는 이 둘 중 어느 것을 지칭하는지가 명확하지 않은 상황이 발생하게 되었습니다.

이런 관점에서 이 포스트와 앞으로의 포스트들에서는 가능하면 클래스의 실제 이름을 명시하지 않은 채 그 클래스로 구현하고자 하는 소프트웨어적 실체를 클래스라고 지칭하는 것 또한 최대한 피하도록 하겠습니다. ‘Pikachu 클래스’라는 표현은 사용하겠지만, ‘피카츄 클래스’라고 말하는 것은 지양할 것입니다. 사실 ‘피카츄 클래스’와 같은 표현도 심심찮게 사용되고, 위의 예시처럼 억지로 상황을 만들어내지 않는 한 문제가 있는 표현은 아니지만요. 개념들에 익숙해지고 실제로 다양한 구현을 접하다 보면 용어의 정의와 구분을 굳이 명확히 하려고 하지 않아도 이 개념들을 이용하는 진술을 자연스럽게 구사할 수 있게 될 것입니다.

클래스에 기능 부여하기: 메서드

이제 다시 우리의 Pikachu 클래스 구현으로 돌아가 봅시다.

1
2
3
4
5
6
class Pikachu:
    def __init__(self):
        self.level = 1
        self.hit_point = 100
        self.atk = 20
        self.type = 'electric'

우리는 함수를 작성하기 위해 def <함수명> (<인수>): 구문을 사용할 수 있다는 것을 이미 알고 있기 때문에, Pikachu 클래스에 __init__이라는 함수가 정의되어 있다는 것을 위 코드에서 알 수 있습니다. 위와 같이 클래스에 종속되어 구현되는 함수들을 메서드라고 합니다. 따라서 Pikachu 클래스에는 __init__ 이라는 메서드 하나가 정의되어 있는 상태입니다.

생성자 __init__과 self

__init__ 메서드에는 생성자라는 특별한 이름이 붙어 있습니다. 이 메서드는 클래스의 인스턴스를 만들 때 가장 처음 호출되는 메서드입니다. 이 메서드는 앞 절에서 우리가 Pikachu 클래스의 인스턴스 a를 만들기 위해 a = Pikachu()를 실행했을 때, 우리가 일부러 호출하지 않았지만 함께 실행되었습니다.

생성자의 인수인 self는 아직 실체화되지 않은 임의의 Pikachu 인스턴스들을 가리키는 ‘지시대명사’입니다. self가 사용되는 이유는 클래스 안에서는 클래스 자신으로부터 유래된 인스턴스를 구체적으로 지칭할 방법이 없기 때문이며, 클래스를 이용해 인스턴스가 만들어지고 나면 만들어진 인스턴스 변수명을 이용하여 self들을 모두 대신할 수 있습니다.

이제 a = Pikachu()코드가 실행될 때 일어나는 일을 살펴봅시다. 생성자가 가장 먼저 실행되므로, __init__(self)가 실행되어야 합니다. self는 지시대명사일 뿐이므로 인스턴스명으로 치환해 주어야 하는데, 이 경우 인스턴스명은 a이므로 __init__(a)를 실행하는 것이 됩니다. 하지만 파이썬에서 어떤 메서드를 호출할 때, 해당 메서드를 호출하는 인스턴스명은 괄호 밖으로 꺼내 주고 온점(.)을 붙인 다음 메서드명을 이어 쓰는 것이 규칙입니다. 따라서 a = Pikachu()를 실행했을 때 실제로 실행되는 구문은 a.__init__()이라고 볼 수 있습니다.

메서드 만들고 사용하기 (1)

1
2
3
4
5
6
7
8
class Pikachu:
    def __init__(self):
        self.level = 1
        self.hit_point = 100
        self.atk = 20
        self.type = 'electric'
    def get_damaged(self, damage):
        self.hit_point -= damage

이제 생성자가 아닌 다른 메서드를 작성해 봅시다. get_damaged이라는 메서드를 작성했습니다. 이 메서드는 self 인수와 함께 damage라는 새로운 인수를 전달받아, 해당 수치만큼 hit_point변수 값을 낮추도록 구현하였습니다.

Pikachu 클래스의 인스턴스 a를 만들어 get_damaged 메서드를 호출해 보겠습니다. 피카츄 a가 10의 데미지를 입는다고 생각해 보지요, 이 상황을 표현하려면 a가 get_damaged 메서드를 호출하면서, damage 인수로 10을 넘겨 주어야 할 것입니다. self는 a로 치환한 다음 메서드명 앞에 온점과 함께 써 주고, damage인수는 그대로 괄호 안에 넣어 줍니다. 최종적으로 아래와 같이 됩니다:

1
2
3
4
5
6
>>> a = Pikachu()
>>> print(a.hit_point) # a 피카츄의 HP를 확인
100
>>> a.get_damaged(10) # a의 HP에 10의 데미지를 줍니다.
>>> print(a.hit_point) # 정말로 10의 데미지를 입었는지 확인해 봅니다.
90

self를 인수로 갖지 않는 메서드: 클래스 내부에 정의되어 있으면서도 self 인수를 갖지 않는 메서드를 만들 수 있습니다. 우선은 self를 인수로 받는 경우만 다루기로 하고, 이후에 ‘정적 메서드 (static method)’와 ‘클래스 메서드 (class method)’에서 이러한 경우들을 다루도록 하겠습니다.

메서드 만들고 사용하기 (2)

억울한 피카츄를 위해, 다른 피카츄를 공격할 수 있는 attack 메서드를 만들어 줍시다.

1
2
3
4
5
6
7
8
9
10
class Pikachu:
    def __init__(self):
        self.level = 1
        self.hit_point = 100
        self.atk = 20
        self.type = 'electric'
    def get_damaged(self, damage):
        self.hit_point -= damage
    def attack(self, other_pikachu):
        other_pikachu.get_damaged(self.atk)

attack 메서드는 Pikachu 클래스의 다른 인스턴스 (other_pikachu)를 인수로 받도록 작성되었습니다. other_pikachu 인수로 반드시 Pikachu 클래스의 인스턴스만을 넘겨 주기로 약속하면, 우리는 other_pikachu 객체가 hit_point 인스턴스 변수를 가질 뿐만 아니라 get_damaged 메서드를 호출할 수도 있다는 것을 보장할 수 있을 것입니다.

따라서 공격당할 Pikachu 클래스의 인스턴스 (other_pikachu)를 인수로 받아, other_pikachu 인스턴스에 종속된 get_damaged를 호출함으로써 other_pikachu 인스턴스의 hit_point 변수 값을 낮추는 방식으로 attack 메서드를 구현하였습니다. other_pikachu가 입을 피해량은 attack을 호출한 Pikachu 클래스의 인스턴스가 갖는 공격력(self.atk) 수치 만큼이 되도록 하였습니다.

이제 피카츄 두 마리를 만들고, 한 녀석이 다른 녀석을 공격하도록 해 봅시다. 몸통박치기!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
>>> a = Pikachu() # Pikachu 클래스의 인스턴스 a를 만들었습니다.
>>> b = Pikachu() # 또 다른 Pikachu 객체 b를 만들었습니다.

# 체력을 확인해 봅시다.
>>> print(a.hit_point)
100
>>> print(b.hit_point)
100

# a 피카츄의 몸통박치기!
>>> a.attack(b)

# HP를 확인해 봅시다.
>>> print(a.hit_point)
100
>>> print(b.hit_point)
80

# attack 메서드에 자신을 호출하는 인스턴스를 인수로 전달하는 것을 금지하지 않았기 때문에,
# 아래와 같은 호출도 가능합니다.
>>> a.attack(a) # a 피카츄는 혼란에 빠져 스스로를 공격했다...

# 막상막하의 대결이 되었습니다.
>>> print(a.hit_point)
80
>>> print(b.hit_point)
80

메서드 만들고 사용하기 (2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Pikachu:
    def __init__(self):
        self.level = 1
        self.hit_point = 100
        self.atk = 20
        self.type = 'electric'
        self.is_in_monsterball = false
    def get_damaged(self, damage):
        self.hit_point -= damage
    def attack(self, other_pikachu):
        other_pikachu.get_damaged(self.atk)
    def level_up(self):
        self.level += 1
        self.hit_point += 10
        self.atk += 1
        print(f"피카츄는 레벨 {self.level}이 되었다!")
        print(f"피카츄의 HP: {self.hit_point}")
        print(f"피카츄의 공격력: {self.atk}")
    def retrieve(self):
        print("피카츄, 돌아와!")
        self.is_in_monsterball = true
        print("피카츄는 몬스터볼로 돌아왔다!")

Pikachu 클래스를 조금 더 가지고 이것저것 해 보았습니다. 레벨을 올려 주는 level_up 메서드를 추가하였고, 새 인스턴스 변수 is_in_monsterball 을 추가하여 피카츄가 몬스터볼 안에 들어 있으면 true라고 두기로 하되 기본값은 false로 둡니다.

피카츄의 설계도가 풍성해졌습니다. ‘공격한다(attack 메서드)’ 외에 아무 기술도 없고, HP보다 큰 데미지를 입어도 아무 반응도 하지 않지만, 클래스 명세는 원하는 기능들이 원하는 형태로 완성될 때까지 계속 수정하면 됩니다.

이제 마지막으로 retrieve 메서드를 만들어 수고한 피카츄를 몬스터볼로 회수해 주고, 다음 장으로 넘어가 보도록 합시다.

1
2
3
4
5
6
7
8
>>> my_pikachu = Pikachu()
>>> my_pikachu.level_up()
피카츄는 레벨 2 되었다!
피카츄의 HP: 120
피카츄의 공격력: 21
>>> my_pikachu.retrieve()
피카츄, 돌아와!
피카츄는 몬스터볼로 돌아왔다!