04. 클래스와 객체의 선언과 구현

송지윤·2025년 7월 30일

Python

목록 보기
11/20

클래스와 상속

파이썬은 함수형 언어의 특징과 객체지향 언어의 특징이 모두 있다.

클래스와 객체

크래스와 객체는 아주 밀접한 관계가 있다. 클래스는 설계도면에, 객체는 집에 비유해보자. 집을 짓기 전에는 먼저 설계도면을 작성한다. 예를 들어 창문이 10개 있고 현관문이 2개인 이층집을 설계했다면 이 설계도면을 바탕으로 지은 집의 구조는 설계도면과 같을 것이다.
그런데 설계도면에 그려진 집은 실제로 들어가서 살 수 있는 실체가 아니다. 설계도면에 설계한 대로 집을 지어야 실체가 존재하는 것이다. 이처럼 클래스는 객체를 만들기 위한 틀이고, 객체는 실체로 존재하는 인스턴스다.
설계도면이 하나 있으면 여러 채의 집을 지을 수 있다. 예를 들어 하나의 설계도면으로 집을 종로구에 한 채, 강남구에 한 채, 강동구에 한 채 지었다면 집의 구조는 모두 같지만 주소는 모두 다를 것이다.
마찬가지로 하나의 클래스로 여러 개의 객체를 생성할 수 있다. 같은 클래스로 만들어진 객체는 내부 구조가 서로 같고, 객체가 생성된 메모리 주소만 다르다.

클래스(class)
template for object
객체를 생성하기 위한 틀
예 : 설계도면(소스 코드, 템플릿)
구성 : 데이터 + 행위

클래스 통해서 객체를 생성

객체(object)
instance of class
클래스로부터 생성된 인스턴스
예 : 집(메모리에 할당되는 코드 블록(주소 값에 생성))
구성 : 변수 + 함수

클래스와 객체의 구성을 보면 클래스는 데이터+행위, 객체는 변수+함수로 표현했다. 데이터는 곧 변수이며 행위는 곧 함수이다. 특히 객체가 가진 함수를 메소드(method)라고 하므로 이제부터 객체에 정의된 함수는 메소드라고 하겠다.

실제 프로그램은 코드에 작성한 객체들이 메모리에 할당된 다음 주어진 명령에 따라 실행되어 결과를 낸다. 이때 메모리의 주소값으로 실행할 코드의 영역을 구분한다. 객체가 생성될 때 메모리의 주소값이 정해지고, 객체의 주소값은 변수에 할당된다. 따라서 우리는 메모리의 주소값에 대신에 변수를 통해 객체를 참조하는 것이다.
객체를 메모리의 주소값으로 참조할 수는 없을까? 파이썬뿐만 아니라 자바 등 대부분의 객체지향 언어에서는 객체의 주소값을 알아낼 수 없고, 주소값이 할당된 변수를 통해서만 객체를 사용할 수 있다.

클래스 선언과 구현
클래스를 선언하고 객체를 사용하는 방법은 아래의 3단계를 거친다.
클래스의 선언 및 정의 -> 클래스로부터 객체를 생성 -> 객체를 통해 변수나 메소드 사용
클래스를 선언할 때는 class라는 키워드(예약어)를 사용하며, 선언부와 구현부는 콜론과 들여쓰기로 구분한다. 클래스 내부에는 여러 개의 변수와 메소드를 정의할 수 있다. 또한, 클래스는 변수만 혹은 메소드만으로도 만들 수 있다.

class 클래스명 :
	변수명 =def 메소드(self, 매개변수, ...) :
    	...

메소드의 매개변수 중에 self는 객체 자기 자신을 의미한다. 클래스에 속한 메소드에서 클래스 내의 변수나 메소드를 참조할 때 self를 사용하므로 객체가 가지는 메소드는 기본적으로 self를 매개변수로 받아야 한다. 파이참에서는 메소드를 선언할 때 매개변수에 self를 자동으로 추가해준다.

class Employee :
    name = '사원'
    def prName(self):
        print('이름은 {0}입니다.'.format(self.name))

emp = Employee()
# 생성자 : 클래스 이름과 동일한 이름의 함수 (객체를 생성)
emp.prName()

내부에서 지역변수 참조할 때 self 붙여줘야함
클래스 내부에 생성자가 없을 때는 기본 생성자를 제공해준다.

출력 결과

이름은 사원입니다.

Employee 객체 10번지 주소값이 emp에 들어감
emp 객체 안에는 name 과 prName 함수가 있음
주소값은 알 수 없다.
객체를 변수에 할당한 이후에 사용해야함
변수를 통해서 참조해야함.

생성자와 소멸자

생성자

생성자는 생성자 함수라고도 하며 객체를 생성할 때 호출한다. 생성자를 호출하려면 클래스명과 동일한 이름으로 함수를 호출하면 되는데, 예를 들어 앞에서 작성한 Employee 클래스의 생성자는 Employee()인 것이다.
함수는 호출하기 이전에 선언을 먼저 해야 한다. 그런데 class_test.py에서는 Employee()라는 함수를 선언하지 않았다. 선언하지도 않은 Employee() 함수를 호출할 수 있었던 이유는 클래스에 생성자를 만들지 않으면 파이썬 프로그램에서 생성자를 자동으로 제공하기 때문이다.
그렇다면 기본으로 제공되는 생성자 이외에 어떨 때 생성자를 추가할까? 꼭 정해진 것은 아니지만 객체를 생성할 때 필요한 작업이 있다면 생성자를 추가하고 구현부에 코드를 작성한다.
대표적인 예가 생성자의 매개변수로 받은 값으로 객체의 변숫값을 초기화할 때다. 환경 설정 파일에서 설정을 읽어오거나 데이터베이스 연결, 또는 파일을 열 때가 그러하다.
생성자를 추가할 때 주의할 점은 생성자는 '클래스명()'으로 호출하지만, 생성자를 선언하고 구현할 때는 __init__()이라는 메소드를 사용한다는 점이다. 언더바는 init의 앞뒤로 두 개씩 붙으며, __init__() 메소드에는 매개변수를 원하는 대로 정의할 수 있다. 클래스 내부에 __init__() 메소드를 구현해 놓으면 객체를 생성할 때 파이썬이 이 메소드를 자동으로 호출한다.

class Manager :
    name = '이름'
    position = '직책'

    def __init__(self, name, position):
        print('두 개의 매개변수를 받는 생성자 호출')
        print('{0} 님의 직책은 {1} 입니다.'.format(self.name, self.position))
        self.name = name
        self.position = position
        print("초기화 이후")
        print('{0} 님의 직책은 {1} 입니다.'.format(self.name, self.position))

mgr = Manager('김사원', '대리')

출력 결과

두 개의 매개변수를 받는 생성자 호출
이름 님의 직책은 직책 입니다.
초기화 이후
김사원 님의 직책은 대리 입니다.
  1. Manager 클래스를 선언하고, name과 position 변수를 선언한다.
  2. 두 개의 매개변수 name 과 position을 받는 생성자 __init__()을 선언한다.
  3. 클래스가 가진 기본 변수 self.name과 self.position을 출력한다.
  4. Manager 객체를 생성할 때마다 Manager의 이름과 직책이 다를 수 있으므로 객체를 생성할 때 이름과 직책을 생성자 내부에 전달하고, 각각 self.name, self.position에 할당한다.
  5. 초기화 이후의 self.name 과 self.position을 출력한다.
  6. Manager 생성자를 호출할 대 이름과 직책을 매개변수로 전달하고, 이렇게 생성된 객체를 mgr 변수에 할당한다.

이렇게 클래스에 매개변수를 받는 생성자를 ㅏㄴ번 추가하고 나면 매개변수가 없는 형태의 기본 생성자르 더는 사용하지 못한다.

소멸자

소멸자는 소멸자 함수라고도 하며, 객체가 메모리에서 사라지기 전에 종료 작업을 수행한다. 소멸자는 생성자처럼 코드에서 직접 호출하는 것이 아니라 객체가 사라지기 전에 파이썬 프로그램 내부에서 자동으로 호출된다.

객체가 메모리에서 사라지는 시점은 아래 두 가지 경우이다.

  • 더 이상 객체를 참조하는 참조 변수가 없을 때
  • 파이썬 프로그램을 종료할 때

소멸자를 정의하고 싶다면 클래스 내부에 __del__() 메소드를 정의하면 된다.

파이썬은 객체를 어떻게 관리할까?

파이썬의 내부 동작 방식이나 구조를 알지 못해도, 파이썬으로 프로그램을 만들 수는 있다. 하지만 파이썬의 동작 방식을 알면 프로그램의 성능을 향상시키는 등 개발에 응용할 수 있는 부분이 많다. 따라서 파이썬에서 객체를 관리하는 방법을 알아두면 좋은데, 핵심 키워드는 바로 레퍼런스 카운트(reference count)와 가비지 컬렉터(GC: garbage collector)다.

객체 생성 : 객체를 메모리에 올려준다(할당) - 개발자가 생성자를 호출
객체 소멸 : 객체를 메모리에서 지운다.(해지) - 객체가 필요없을 때 파이썬에서 내부적으로 지워줌 (해지한다)

상속

상속(inheritance)은 객체지향 언어의 특징 중 하나다. 상속 관계는 클래스에서 사용하며, 부모(parent) 클래스가 가진 모든 속성(변수와 메소드)을 자식(child) 클래스에 물려주는 것을 의미한다.
여러 개의 클래스에서 쓰이는 변수나 메소드가 공통적인 특징이 있을 때 클래스마다 동일한 코드를 반복해서 작성하는 것은 비효율적이다. 공통부분을 모아서 부모 클래스에 정의하고, 나머지 클래스가 이를 상속하면 된다.
또한 상속은 클래스에 변경이 발생했을 대도 유용하다. 상속을 사용하지 않으면 클래스마다 똑같은 부분의 코드를 일일이 수정해야 한다. 하지만 상속은 부모 클래스만 변경하면 자식 클래스가 부모 클래스를 그대로 물려받으므로 변경하지 않아도 된다. 이때 자식 클래스에서는 상속한 코드의 일부를 변경하거나 새로운 코드를 추가할 수 있다.
동물원 관리 프로그램을 작성한다고 가정해보자. 각 동물 클래스는 동물의 이름, 나이, 먹이 등의 변수와 먹이주기(), 관리하기() 등의 다양한 메소드를 가진다. 동물들은 서로 공통적인 특징도 있고, 동물마다 고유한 특징도 있을 것이다.
여기서 공통적인 변수와 메소드는 Animal이라는 부모 클래스에 만들고, 각 동물 클래스가 이를 상속한다. 즉, 부모 클래스 Animal에 있는 변수와 메소드는 Wolf, Lion, Tiger 등 모든 동물에 상속되므로 따로 정의할 필요가 없다. 동물마다 고유한 변수나 메소드가 있다면 각 동물 클래스에 추가하면 된다.
이처럼 상속을 사용하면 공통 속성을 한 번에 관리할 수 있어 코드의 재사용성과 유지보수성 및 가독성을 높일 수 있다.
클래스의 상속을 선언하는 방법은 클래스 선언부의 괄호 안에 상속할 부모 클래스를 적어주면 된다.

class 클래스명(부모 클래스) :

상속의 장점을 정리하면 다음과 같다.

  • 부모 클래스에 공통된 속성과 메소드를 정의함으로써 각 클래스에서 동일한 코드가 반복되지 않는다.
  • 공통된 속성을 부모 클래스에서 관리하므로 수정, 추가, 삭제 등의 유지보수가 쉽다.
  • 자식 클래스에서는 특화된 메소드와 데이터를 추가할 수 있어 클래스마다 고유한 특징을 사용할 수 있다.
  • 부모 클래스로부터 상속한 메소드는 메소드 오버라이딩을 통해 다양성을 구현할 수 있다.

메소드 오버라이딩

메소드 오버라이딩(method overriding)이란 부모 클래스의 메소드를 자식 클래스에서 다시 정의(재정의)하는 것을 의미한다. 메소드를 다시 정의한다는 것이 무슨 뜻일까?
메소드의 선언부는 같고 구현부만 다르게 만드는 것이다. 즉, 메소드의 이름과 매개변수는 같은데 내부의 동작하는 코드가 다른 것을 말한다. 클래스를 다양한 형태로 나타내는 이러한 특성을 객체지향의 다형성(polymorphism)이라고 한다.
예를 들어 일반 사원과 관리자의 월급 계산을 생각해보자. 사원과 관리자의 월급을 계산하는 로직은 다르지만 월급을 계산한다는 기능은 같다. 따라서 선언부에 월급 계산이라는 메소드명과 필요한 매개변수는 똑같고, 구현부만 다르게 작성하면 되는 것이다.
메소드 오버라이딩은 상속과 함꼐 사용된다. 부모 클래스의 메소드를 자식 클래스에서 오버라이딩 했을 때, 어떤 메소드가 호출될까? 부모 클래스의 메소드일까? 아니면 자식 클래스의 메소드일까? 상속을 받은 자식 클래스의 객체를 생성하고 오버라이딩 메소드를 호출하면, 자식 클래스에서 오버라이딩한 메소드가 호출된다.

from ipywidgets.widgets.interaction import empty


class Emp :
    name = '사원 이름'
    salary = 300

    def __init__(self, name):
        self.name = name

    def info(self):
        print('=== 사원 정보 ===')
        print('{0} 님의 정보입니다.'.format(self.name))

    def calcSalary(self):
        print('=== 사원 월급 ===')
        print('{0} 사원의 월급은 {1} 입니다.'.format(self.name, self.salary))

class Manager(Emp) :
    title = '직책'

    def __init__(self, name, title):
        self.name = name
        self.title = title

    def calcSalary(self):
        print('=== 관리자 월급 ===')
        if self.title == '대리' :
            self.salary += 50
        elif self.title == '과장' :
            self.salary += 100
        elif self.title == '차장' :
            self.salary += 150
        elif self.title == '부장' :
            self.salary += 200
        else :
            self.salary += 300

        print('{0}의 월급은 {1} 입니다.'.format(self.title, self.salary))


emp = Emp('사원')
emp.info()
emp.calcSalary()

mgr = Manager('홍길동', '대리')
mgr.info()
mgr.calcSalary()

mgr2 = Manager('이순신', '부장')
mgr2.info()
mgr2.calcSalary()

출력 결과

=== 사원 정보 ===
사원 님의 정보입니다.
=== 사원 월급 ===
사원 사원의 월급은 300 입니다.
=== 사원 정보 ===
홍길동 님의 정보입니다.
=== 관리자 월급 ===
대리의 월급은 350 입니다.
=== 사원 정보 ===
이순신 님의 정보입니다.
=== 관리자 월급 ===
부장의 월급은 500 입니다.

Manager 에 선언한 calcSalary()는 Emp 클래스 calcSalary() 오버라이딩 된 거

다중 상속

두 개 이상의 클래스를 상속하는 것을 다중 상속이라고 한다. 다중 상속으로 여러 부모 클래스에 있는 모든 속성(변수와 메소드)을 그대로 상속하고, 메소드 오버라이딩도 가능하다. 상속을 선언할 대 클래스 선언부의 괄호 안에 부모 클래스를 입력했던 것처럼 다중 상속은 콤마로 여러 클래스를 지정하면 된다.

class 클래스명(부모 클래스, 부모 클래스, ...) :

만약 상속한 여러 부모 클래스에 동일한 메소드가 있다면 어떤 메소드가 호출될까? 다중 상속에서 메소드는 메소드 결정 순서(method resolution order)에 의해 호출된다. 메소드 결정 순서는 클래스의 속성인 __mro__ 에서 정의한다.
참고로 파이썬 3부터 모든 클래스는 선언부에 따로 명시하지 않아도 object 클래스를 묵시적으로 상속한다. 즉, object는 모든 클래스의 부모 클래스이다. 앞에서 설명한 모든 객체의 글미에서도 객체 맨 위에 object 객체가 만들어진다. 다만, 앞에서는 Emp 객체나 Manager 객체의 설명에 집중하기 위해 object 객체를 생략했다. 이후로는 파이썬의 모든 객체가 만들어질 대 object 객체가 기본적으로 만들어진다고 보면 된다.

class Employee :
    def info(self):
        print('일반직 사원 정보')
    def calcSalary(self):
        print('일반직 사원 월급')

class Sales :
    def sale(self):
        print('영업직 사원 정보')
    def calcSalary(self):
        print('영업직 사원 월급')

class SalesEmp(Sales, Employee) :
    def manage(self):
        print('영업 관리 정보')

s = SalesEmp()
s.info()
s.sale()
s.calcSalary()
s.manage()
print(SalesEmp.__mro__)        

출력 결과

일반직 사원 정보
영업직 사원 정보
영업직 사원 월급
영업 관리 정보
(<class '__main__.SalesEmp'>, <class '__main__.Sales'>, <class '__main__.Employee'>, <class 'object'>)

Sales 에서 calcSalary() 오버라이딩함

class SalesEmp(Employee, Sales) 이렇게 작성하면

(<class '__main__.SalesEmp'>, <class '__main__.Employee'>, <class '__main__.Sales'>, <class 'object'>)

이렇게 바뀌고
출력도

일반직 사원 정보
영업직 사원 정보
일반직 사원 월급
영업 관리 정보

이렇게 바뀜

0개의 댓글