Class 와 Instance

2023. 7. 12. 20:41

프로그래밍을 공부하다 보면 Class(클래스)와 Instance(인스턴스) 라는 것으로 객체 지향 프로그래밍을 구현한다는 것을 접하기 마련입니다.처음 프로그래밍을 공부하기 시작한 사람들에게는 정말 생소한 개념으로 큰 난관으로 다가옵니다. 저도 그랬습니다.

 

이번 포스팅에서는 Class(클래스) 와 Instance(인스턴스) 에 대해 이야기해보겠습니다.


1. 붕어빵과 붕어빵틀?

클래스와 인스턴스에 대한 비유 중 한국에서는 아마 가장 유명한 비유이지 않을까 싶습니다. 쿨래스는 붕어빵 틀이고 인스턴스는 붕어빵 틀로 찍어낸 붕어빵이라고요. 그래서 하나의 붕어빵 틀로 여러개의 붕어빵을 찍어낼 수 있다고 합니다. 마찬가지로 하나의 클래스로 여러개의 인스턴스를 만들수 있다고 합니다. 

 

제가 처음 봤을 떄 이 비유를 보고 생각했던 것은 '그래서 클래스가 뭐지?' 라는 것이었습니다. 저처럼 Python 을 공부하는 누군가 중 최소 한명은 이 비유를 보고 이해했을것이라 생각하지만 저는 그렇지 않았나 봅니다.

 

클래스와 인스턴스를 이해할려면 우선 객체(object) 에 대해 짚어보고 넘어가야 합니다.


2. 객체(object)

객체에 대한 정의를 위키피디아에서 찾아보면 다음과 같이 설명을 합니다.

 

컴퓨터 과학에서 객체 또는 오브젝트(object)는 클래스에서 정의한 것을 토대로 메모리(실제 저장공간)에 할당된 것으로 프로그램에서 사용되는 데이터 또는 식별자에 의해 참조되는 공간을 의미하며, 변수, 자료 구조, 함수 또는 메소드가 될 수 있다.

 

정리하면, 메모리 공간에 변수나 자료같은 이런저런 것을 두면 객체라고 한다고 합니다.

 

어렵게 생각할 필요 없이, 간단한 비유를 들어서 메모리 공간을 택배박스라고 생각해 봅시다.

아무거나 집어넣어도 된다

택배 박스 그 자체로는 그냥 아무 의미가 없습니다. 그런데 거기에 책을 넣던가 노트북을 넣던가 무언가를 넣으면 택배 박스는 무언가를 저장하고 있는 박스가 됩니다.

 

객체도 마찬가지 입니다. 어떤 메모리 공간에 숫자나 텍스트, 아니면 함수(function) 같은걸 저장했다고 하면 우리는 그걸 객체로 생각하면 됩니다.


3. 클래스 와 인스턴스가 필요한 이유

객체가 대략 어떤 것인지 알아보았으니, Class 와 Instance 가 왜 등장했는지 알아보도록 합시다.

 

사실 객체니 클래스니 이런거 없어도 충분히 원하는 기능을 가지는 프로그램을 만들 수 있습니다. 그런데 사용하는 이유가 무엇이냐면 데이터와 함수를 묶기 때문에 코드를 작성하고 관리하기 편해서 사용합니다. 요컨대 말하는 객체 지향 프로그래밍이라는 것입니다.

 

클래스 등장 이전에는 어떤 식으로 코딩을 했는지 살펴봅시다.

 

 

3.1 클래스 없이 붕어빵 조리 코드 만들어보기

처음에 붕어빵 이야기가 나왔으니, 붕어빵을 만든다고 생각해 봅시다. 붕어빵은 하나의 데이터(Data) 라고 생각하면 됩니다. 붕어빵을 만들려면 일단 재료가 필요하겠죠? 보통 밀가루 반죽을 쓸 텐데 쌀가루 반죽도 사용한다고 해 봅시다.

 

그 외에 속에 들어갈 팥앙금이나 이거저거 넣을 수도 있을 것이고, 붕어빵틀에 넣은 뒤 가열 시간을 얼만큼 할것인지, 언제 뒤집고 언제 꺼낼지 정해놓아야 할겁니다.

 

다음과 같이 재료와 조리 방법을 데이터처럼 정리해보자면 다음과 같습니다. 파이썬 Dict 자료형을 사용했습니다.

 

붕어빵_재료 = {'반죽': '밀가루', '속': '팥앙금'}
조리_방법 = {'가열시간': 120, '뒤집기': 60}

 

재료와 조리방법이 있으니 조리시 행동을 정의해 봅시다. 데이터가 입력시 함수로 처리해서 결과를 내놓는 것과 같습니다.

 

def 틀에_반죽넣기(재료):
    붕어빵 = {'반죽': 재료['반죽'], 
              '속': 재료['속'], 
              '가열시간': 0, 
              '왼쪽가열시간': 0, 
              '오른쪽가열시간': 0,
              '가열면': '왼쪽'}
    return 붕어빵

def 한쪽가열시간(조리방법):
    return 조리방법['뒤집기'], 조리방법['가열시간'] - 조리방법['뒤집기']

def 가열(붕어빵, 시간):
    if 붕어빵['가열면'] == '왼쪽':
        붕어빵['왼쪽가열시간'] += 시간
    else:
        붕어빵['오른쪽가열시간'] += 시간
    붕어빵['가열시간'] += 시간
    return 붕어빵

def 뒤집기(붕어빵):
    if 붕어빵['가열면'] == '왼쪽':
        붕어빵['가열면'] = '오른쪽'
    else:
        붕어빵['가열면'] = '왼쪽'
    return 붕어빵

def 틀에서_꺼내기(붕어빵):
    print(붕어빵)
    return 붕어빵

 

재료와 행동을 정의했으니 붕어빵을 만들 수 있을 것 같습니다. 붕어빵을 만들어 봅시다.

 

가열시간1, 가열시간2 = 한쪽가열시간(조리_방법)
붕어빵 = 틀에_반죽넣기(붕어빵_재료)
붕어빵 = 가열(붕어빵, 가열시간1)
붕어빵 = 뒤집기(붕어빵)
붕어빵 = 가열(붕어빵, 가열시간2)
붕어빵 = 틀에서_꺼내기(붕어빵)

 

이를 실행해 보면, 다음과 같이 붕어빵 상태가 print 될 것입니다.

 

{'반죽': '밀가루', '속': '팥앙금', '가열시간': 120, '왼쪽가열시간': 60, '오른쪽가열시간': 60, '가열면': '오른쪽'}

 

 

3.2 여러개의 붕어빵과 다른 메뉴 추가시

 

그런데 붕어빵을 여러 종류를 만든다고 생각해 봅시다. 슈크림 붕어빵도 있을 것이고 피자 붕어빵도 있는 등 재료가 굉장히 달라집니다. 또 취향에 따라 누구는 탄 붕어빵을 좋아하고 누구는 살짝 설익은 붕어빵을 먹고싶어합니다.

하나하나 재료와 조리방법을 정해봅시다.

 

붕어빵_재료1 = {'반죽': '밀가루', '속': '팥앙금'}
붕어빵_재료2 = {'반죽': '밀가루', '속': '슈크림'}
붕어빵_재료3 = {'반죽': '쌀가루', '속': '고구마'}
붕어빵_재료4 = {'반죽': '쌀가루', '속': '김치'}
...

조리_방법1 = {'가열시간': 120, '뒤집기': 60}
조리_방법2 = {'가열시간': 140, '뒤집기': 70}
조리_방법3 = {'가열시간': 110, '뒤집기': 55}
...

 

일일이 정의하다 보면 헷깔리기 마련입니다. 하나로 모아서 정의하고, 가격도 붙여보겠습니다.

팥붕어빵_정보 = {'재료': {'반죽': '밀가루', '속': '팥앙금'}, '조리방법': {'가열시간': 120, '뒤집기': 60}, '가격': 1000}
슈크림붕어빵_정보 = {'재료': {'반죽': '밀가루', '속': '슈크림'}, '조리방법': {'가열시간': 120, '뒤집기': 60}, '가격': 1500}
고구마붕어빵_정보 = {'재료': {'반죽': '쌀가루', '속': '고구마'}, '조리방법': {'가열시간': 120, '뒤집기': 60}, '가격': 1500}
...

 

코딩을 하다 보면, 여러개의 변수와 그에 따른 함수도 여러개 쓰기 마련입니다. 풀빵을 추가해 보도록 합시다.

팥풀빵_정보 = {'재료': {'반죽': '밀가루', '속': '팥앙금'}, '조리방법': {'가열시간': 100}, '가격': 600}
슈크림풀빵_정보 = {'재료': {'반죽': '밀가루', '속': '슈크림'}, '조리방법': {'가열시간': 120}, '가격': 1000}
고구마풀빵_정보 = {'재료': {'반죽': '쌀가루', '속': '고구마'}, '조리방법': {'가열시간': 120}, '가격': 1000}


def 풀빵_틀에_반죽넣기(재료):
    풀빵 = {'반죽': 재료['반죽'], 
              '속': 재료['속'], 
              '가열시간': 0,}
    return 풀빵

def 풀빵_가열(풀빵, 시간):
    풀빵['가열시간'] += 시간
    return 풀빵

def 풀빵_틀에서_꺼내기(풀빵):
    print(풀빵)
    return 풀빵

 

붕어빵과 풀빵을 동시에 구워봅시다. 동시에 굽는 예시를 드는 이유는, 코드의 흐름 상 동시에 데이터를 처리할 경우가 있기 때문입니다.

팥붕어빵1 = 틀에_반죽넣기(팥붕어빵_정보['재료'])
슈크림붕어빵1 = 틀에_반죽넣기(슈크림붕어빵_정보['재료'])
팥풀빵1 = 풀빵_틀에_반죽넣기(팥풀빵_정보['재료'])

가열시간1, 가열시간2 = 한쪽가열시간(팥붕어빵_정보['조리방법'])

팥붕어빵1 = 가열(팥붕어빵1, 가열시간1)
슈크림붕어빵1 = 가열(슈크림붕어빵1, 가열시간1)
팥풀빵1 = 풀빵_가열(팥풀빵1, 팥풀빵_정보['조리방법']['가열시간'])

팥붕어빵1 = 뒤집기(팥붕어빵1)
슈크림붕어빵1 = 뒤집기(슈크림붕어빵1)

팥붕어빵1 = 가열(팥붕어빵1, 가열시간2)
슈크림붕어빵1 = 가열(슈크림붕어빵1, 가열시간2)

팥붕어빵1 = 틀에서_꺼내기(팥붕어빵1)
슈크림붕어빵1 = 틀에서_꺼내기(슈크림붕어빵1)
팥풀빵1 = 풀빵_틀에서_꺼내기(팥풀빵1)

 

그냥 봐도 헷깔리기 참 좋습니다. 코드를 작성하고 관리할 때고 그렇습니다.

 

그냥 변수이름과 함수이름을 좀 다르게 해서 보기좋게 하면 되지 않나는 생각을 할 수 있는데, 실제로 코드 작성 시 이 데이터의 의미와 함수의 역할을 잘 대표하는 이름을 지어야 코드 리딩 및 리뷰, 디버깅 등 코드 관리 시 코드를 이해하기 편해지기 떄문에 유사한 이름을 짓는 경우가 많습니다.

 

그렇다면 클래스를 이용하면 어떨지 봅시다.

 


4. 클래스로 구현한 붕어빵 조리코드

 

위에서 여러가지 변수와 함수가 섞여서 꽤 머리가 아픈 일을 겪었습니다. 이를 해결하기 위해, 변수와 함수를 한데 묶어서 관리하는 방법이 등장했습니다. 이것이 객체 지향 프로그래밍이고, 클래스와 인스턴스 문법을 통해 구현할 수 있습니다.

 

 

4.1 클래스 선언

 

클래스를 이용해서, 붕어빵 조리에 필요한 변수와 함수를 정의해 보겠습니다. 이 때 클래스에 속하는 함수를 메소드(method) 라 합니다.

  • __init__ 메소드를 통해 필요 변수를 정의하고 인스턴스 속성을 정의합니다.
  • class 내 def 로 클래스 내에서 메소드를 정의합니다.
class 붕어빵_클래스(object):
    def __init__(self, 반죽, 속, 가열시간, 뒤집기):
        self.반죽 = 반죽
        self.속 = 속
        self.가열시간 = 가열시간
        self.왼쪽가열시간 = 가열시간-뒤집기
        self.오른쪽가열시간 = 뒤집기
        self.가열면 = '왼쪽'

        self.붕어빵_상태 = None
    
    def 틀에_반죽넣기(self):
        self.붕어빵_상태 = {'반죽': self.반죽, 
                            '속': self.속, 
                            '가열시간': 0, 
                            '왼쪽가열시간': 0, 
                            '오른쪽가열시간': 0,}

    def 가열(self):
        if self.가열면 == '왼쪽':
            self.붕어빵_상태['왼쪽가열시간'] += self.왼쪽가열시간
            self.붕어빵_상태['가열시간'] += self.왼쪽가열시간
        else:
            self.붕어빵_상태['오른쪽가열시간'] += self.오른쪽가열시간
            self.붕어빵_상태['가열시간'] += self.오른쪽가열시간
    
    def 뒤집기(self):
        if self.가열면 == '왼쪽':
            self.가열면 = '오른쪽'
        else:
            self.가열면 = '왼쪽'

    def 틀에서_꺼내기(self):
        print(self.붕어빵_상태)

이렇게, 클래스를 구현할 수 있습니다. 정해진 양식대로 변수를 만들고, 메소드가 작동됩니다. 일종의 설계도라고 할 수 있습니다.

 

 

4.2 인스턴스 만들기

붕어빵을 만드는데 필요한 변수와 메소드(함수)를 정의했으니, 붕어빵을 조리해봅시다.

우선, 붕어빵 변수 이름으로 클래스를 정의하면, 인스턴스를 선언하는 것입니다. 팥붕어빵 인스턴스를 선언해 봅시다.

팥붕어빵 = 붕어빵_클래스(반죽='밀가루', 속='팥앙금', 가열시간=120, 뒤집기=60)

이러면 팥붕어빵이라는 이름을 가진 인스턴스가 만들어집니다.

 

이 때 인스턴스란, 클래스로 선언된 변수 및 데이터가 묶인 객체가 메모리에 할당될 때 해당 객체를 인스턴스라고 부릅니다.

 

4.3 인스턴스 속성(attribute)

붕어빵이 어떤 상태이고 어떤 변수를 가지고 있는지는 속성(혹은 어트리뷰트) 를 통해 알 수 있습니다. 클래스를 정의할 때 self.~ 로 정의하여 어떤 변수를 인스턴스 속성으로 만들것인지 정의할 수 있습니다.

 

인스턴스 속성은 인스턴스 내부에서 공유되는 변수입니다. 즉 인스턴스 속성은 직접 접근이 가능하기도 하고, 내부 메소드로 조작이 가능하기도 합니다.

 

우선 이 붕어빵이 어떤 반죽을 사용하는지 접근해보겠습니다. 'self.반죽 = 반죽'으로 인스턴스 속성을 정의해두었기 때문에, 인스턴스 속성을 알아볼 수 있습니다.

print(팥붕어빵.반죽)
-> 밀가루

혹은 인스턴스 속성을 바로 변경할 수도 있습니다.

팥붕어빵.반죽 = '쌀가루'
print(팥붕어빵.반죽)
-> 쌀가루

 

현재 붕어빵 상태도 정의해 놓은 상태이므로, 해당 인스턴스 속성에 접근할 수 있습니다. 현재 상태는 None 입니다.

print(팥붕어빵.붕어빵_상태)
-> None

 

4.4 메소드 사용

이제 메소드를 사용하여 붕어빵 상태를 변경시켜 보겠습니다. 우선 틀에 넣기 메소드를 실행해보겠습니다.

메소드는 간단하게 인스턴스명.메소드명() 으로 사용하면 됩니다.

팥붕어빵.틀에_반죽넣기()

그리고 다시 인스턴스 속성인 붕어빵_상태를 확인해보면, 붕어빵_상태가 변경된 것을 확인할 수 있습니다.

print(팥붕어빵.붕어빵_상태)
-> {'반죽': '밀가루', '속': '팥앙금', '가열시간': 0, '왼쪽가열시간': 0, '오른쪽가열시간': 0}

 

차례대로 붕어빵을 조리해 보겠습니다.

팥붕어빵.뒤집기()
팥붕어빵.가열()
팥붕어빵.틀에서_꺼내기()
-> {'반죽': '밀가루', '속': '팥앙금', '가열시간': 120, '왼쪽가열시간': 60, '오른쪽가열시간': 60}

 

이렇게 코드가 훨씬 간단해 졌습니다. 그리고 클래스에 정의된 메소드가 아닌 다른 함수를 사용하게 되면 에러가 발생하므로, 코드 작성 시 함수를 잘못 사용하는 등의 실수도 줄일 수 있습니다.

'Python' 카테고리의 다른 글

Python Class 와 Instance 기초문법 및 구현해보기  (0) 2023.07.20
python ~ indexing  (0) 2023.07.18

BELATED ARTICLES

more