06. 클래스(Class)와 객체지향 다루기

안녕하세요 이전 포스팅 애서는 Pyhton 코드를 작성할 때 반복적인 작업, 기능 등을 수행하는 단위를 함수로 정의해서 다루는 것에 대해서 얘기를 나눠봤는데요.

이번 포스팅에서는 정말 중요한 객체 지향 프로그래밍(Object-Oriented Programming), 클래스(Class)와 객체(Object) 의 개념 그리고 실제 이것을 구현하는 방법에 대해서 다뤄보려고 합니다.

  • 객체 지향 프로그래밍
  • 클래스와 인스턴스

객체 지향 프로그래밍

늘 강조하는 것이지만, 객체 지향 프로그래밍이 왜(Why) 중요한지 그리고 무엇(What) 을 객체 지향 프로그래밍이라고 하는지에 대해서 잠시 짚고 넘어가도록 하겠습니다.

정의

컴퓨터 프로그래밍의 패러다임 중 하나로, 필요한 데이터를 추상화 시켜 속성(Attribute)행위(Behavior) 를 가진 객체최소한의 단위 로 만들고, 이러한 객체들 간의 유기적인 상호작용 을 할 수 있도록 로직 을 구성하는 프로그래밍 방법

여기에서 추상화최소한의 단위 표현에 대해서 애매하게 다가오실 수 있을 것 같은데요. 좀 더 설명 드리자면,

  • 추상화 란, 공통의 속성이나 기능을 묶어 이름을 붙이는 것 을 뜻하는데요. 예를 들면, ‘사자’, ‘호랑이’, ‘표범’ 들을 ‘육식동물’ 이라고 묶어서 이름을 붙이는 것이죠. 프로그래밍 관점에서는 ‘클래스를 만들고 설계하는 것 자체’를 뜻합니다.

  • 최소한의 단위 라고 표현한 이유는 이전 클린코드 작성법 에서 함수를 설계할 때 최소한의 기능 단위로 정의를 하는 것이 좋다고 설명드렸습니다. 이러한 방식은 우리가 클래스(Class)를 정의하는 것에도 비슷하게 적용됩니다.

    우선 결합도응집도 에 대한 개념을 잠시 짚어 보겠습니다.

    • 결합도 란, 모듈(클래스, 함수) 간의 상호 의존 정도를 나타내는 지표를 뜻합니다 => 결합도가 낮으면 모듈간의 상호 의존성이 줄어들어 객체의 재사용 및 유지보수가 용이해집니다.
    • 응집도 란, 하나의 모듈 내부에 존재하는 구성 요소들의 기능적 관련성을 뜻합니다 => 응집도가 높은 모듈은 하나의 책임(기능 수행)에 집중하고 독립성이 높아져, 재사용 및 유지보수가 용이해집니다.

    즉 좋은 객체 지향 설계는 결합도는 낮추고 응집도는 높이는 것 입니다. 하지만, 이렇게 객체 지향적인 코딩을 하다보면 이 둘은 트레이드오프(trade-off)관계라는 것을 알 수 있을 텐데요. 그래서 저는 이 둘의 트레이드오프를 잘 조절한 것을 최소한의 단위 로 표현한 것입니다.

    지금 당장은 와닿지 않는 표현들이라 생각됩니다. 지금 당장은 아니더라도 앞으로 코딩을 하면서 차차 알아가실 내용이기에 이쯤에서 정리하도록 하죠.

장,단점

객체 지향 프로그래밍 설계의 장,단점 당연한 얘기겠지만 빠르게 정리하고 넘어가죠

  • 장점
    • 코드 재사용이 용이
    • 유지보수가 쉬움
    • 복잡한, 대형 프로젝트에 필수적
  • 단점
    • 설계시 많은 시간과 노력이 필요

사실 단점을 적기에도 애매합니다. 객체 지향 설계는 이제는 필수에 가까우며, 당연히 알아야할 개념이자, 앞으로 우리가 코딩을 할 때 지향해야할 것이라고 감히 말씀드리고 싶습니다.

정리해보면...

우리가 어떤 어플리케이션을 개발할 때 모듈, 단위, 기능에 맞춰서 여러 클래스(Class)들을 정의하며(객체를 생성하며) 여기서 객체들은 각각이 수행하는 역할이 있을 것이고(응집도), 객체간 상호작용을 히먀 의존성을 가지게 되는데(결합도) 이러한 응집도와 결합도 라는 것은 트레이드오프 관계에 있어 결합도는 최소화하고 응집도는 최대화 하는 단위를 적절히 배분하여 설계하는 것이 좋은 객체 지향 프로그래밍이라 할 수 있는 것입니다.

클래스(Class) 와 인스턴스(Instance)

클래스와 인스턴스

그러면 클래스와 인스턴스 차이를 알아보겠습니다. 코드를 보면 바로 이해가 되실 것 같습니다.

클래스와 인스턴스 예시
1
2
3
4
5
6
7
8
9
class User:
def __init__(self, name):
self.name = name

def say_hello(self):
print(f"안녕하세요. {self.name} 입니다!")

user1 = User("대환")
user2 = User("우현")

간단합니다. 우리가 함수를 정의할 때 앞에 def를 사용해서 정의했듯이, 클래스를 정의할 때 앞에 class를 사용해 정의합니다. 그래서 User는 클래스가 되는 것이구요. user1, user2는 인스턴스 입니다. 글로 정리해보면,

  • 클래스(Class) : 속성(Attribute)행위(Behavior)변수(Variable)함수(Function)로 정의한 것
  • 인스턴스(Instance): 클래스에서 정의한 것을 토대로 실제 메모리에 할당 된 것

이렇게 정리할 수 있겠네요.

Tip! 파이썬에서 클래스(Class)를 네이밍 할 때 PascalCase 로 작성합니다. ex) ClassName, BertTokenizer

자, 위 예시에서 __init__, self 에 대해서 궁금하실 것 같은데요.

__init__

__init__ 메서드는 클래스 생성시 자동으로 호출되는데, 우선 __init__ 이 없을 때를 보면,

__init__ 메서드가 없을 때 객체 속성을 지정하는 방법
1
2
3
4
5
6
7
class User:
def initialize(self, name):
self.name = name

user1 = User()
user1.initialize("대환")
print(user1.name)
대환
__init__ 메서드가 있다면
1
2
3
4
5
6
class User:
def __init__(self, name):
self.name = name

user1 = User("대환")
print(user1.name)
대환

__init__ 메서드가 있으면 객체를 생성할 때 넘겨주면 되므로 훨씬 간단해지죠. 이러한 장점 때문에 보통 클래스를 만들 땐 항상 __init__ 메서드를 같이 정의해줍니다.

__str__

__init__ 에 대해서 알아본 김에 __str__ 도 빠르게 짚고 넘어가죠. 인스턴스를 print()했을 때 원하는 값이 나오게 하려면 str 메서드를 사용할 수 있다.

__str__ 메서드가 있다면
1
2
3
4
5
6
7
8
9
class User:
def __init__(self, name):
self.name = name

def __str__(self):
return f"안녕하세요 저의 이름은 {self.name} 입니다."

user1 = User("대환")
print(user1)
안녕하세요 저의 이름은 대환 입니다.

Tip! 클래스(Class) 내부의 함수는 메서드라는 표현을 사용한다. 위 예시에서 say_hello, initialize 를 메서드라 부른다.

self

인스턴스가 메서드를 호출할 때 자기 자신이 항상 첫번째 인자로 들어가는데 예시와 그림을 보겠습니다.

self의 의미
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class User:
def __init__(self, name, email):
self.name = name
self.email = email

def __str__(self):
return f"이름은 {self.name} 이고, 이메일은 {self.email} 입니다."

def change_info(self, new_name, new_email):
self.name = new_name
self.email = new_email

user1 = User('대환', 'daehwan@example.com')
print(user1)
user1.change_info('대퐝', 'daefwang@example.com')
print(user1)
이름은 대환 이고, 이메일은 daehwan@example.com 입니다.
이름은 대퐝 이고, 이메일은 daefwang@example.com 입니다.

이렇게 인스턴스가 메서드를 호출할 땐 자기 자신이 첫번째 인자로 넘어가기 때문에 메서드를 정의할 때 첫 파라미터로 self 를 넣어주는 것이죠. 꼭 self 가 아니라 다른 것을 사용해도 되지만 파이썬 개발자 간의 관례, 규칙이므로 self 라고 사용해주는 것이 좋습니다.

상속(Inheritance)과 오버라이딩(Overriding)

상속(Inheritance)

상속은 우리가 일상생활에서 사용하는 상속이라는 의미로 생각하시면 좋을 것 같습니다. 클래스로 비유하면, 다른 클래스의 기능, 변수 등을 새로운 클래스에 상속, 물려받도록 하는 것이죠. 코드 보겠습니다.

CalculatorBasic 클래스 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
class CalculatorBasic:
def __init__(self):
pass

def sum(self, num1, num2):
return num1 + num2

def sub(self, num1, num2):
return num1 - num2

cal_basic = CalculatorBasic()
print(cal_basic.sum(10, 2))
print(cal_basic.sub(10, 2))
12
8
CalculatorBasic을 상속받은 CalculatorHard 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
class CalculatorHard(CalculatorBasic):

def mul(self, num1, num2):
return num1 * num2

def div(self, num1, num2):
return num1 / num2

cal_hard = CalculatorHard()
print(cal_hard.sum(10, 2))
print(cal_hard.sub(10, 2))
print(cal_hard.mul(10, 2))
print(cal_hard.div(10, 2))
12
8
20
5.0

위 예시를 보면 상속 의 의미를 확실히 이해하셨을 것 같습니다. CalculatorHard 를 정의할 때 sum, sub 메서드를 정의하지 않았음에도 사용할 수 있는 이유는 CalculatorBasic 클래스의 기능을 상속 받았기 때문이죠.

여기서 CalculatorBasic 은 부모클래스(Super Class), CalculatorHard 는 자식클래스(Sub Class)가 되는 것 입니다.

오버라이딩 (Overriding)

그러면 오버라이딩에 대해서도 알아보죠. 코드 보겠습니다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CalculatorHard(CalculatorBasic):

def sum(self, num1, num2):
print(super().sum(num1, num2))
return f'오버라이딩 된 sum: {num1+num2}'

def mul(self, num1, num2):
return num1 * num2

def div(self, num1, num2):
return num1 / num2

cal_hard = CalculatorHard()
print(cal_hard.sum(10, 2))
print(cal_hard.sub(10, 2))
print(cal_hard.mul(10, 2))
print(cal_hard.div(10, 2))
12
오버라이딩 된 sum: 12
8
20
5.0

보시는 것처럼 CalculatorHard에서 sum 이라는 함수를 다시 정의했죠. 그리고 기능을 물려준 클래스를 SuperClass 라고 얘기했는데요. super().sum() 을 하게되면 CalculatorBasicsum()을 사용할 수 있습니다. 추가로 알아둡시다!

결론

이번 포스팅에서는 객체 지향 프로그래밍이 무엇 인지, 그리고 어떻게 설계하는 것이 좋은 것인지에 대해서도 얘기를 나눠봤습니다. 그리고 클래스를 정의하는 방법과 그리고 ___init___, self 에 대한 개념을 잠시 짚고 넘어갔구요. 마지막으로 클래스 상속오버라이딩에 대해서 알아봤습니다. 다음 포스팅에서는 캡슐화에 대한 얘기, 그리고 (_, __) 가 파이썬에서 어떤 의미를 가지는지에 대해서 얘기를 나눠보겠습니다.

이상으로 이번 포스팅은 마치겠습니다. 추가 의견이나 수정이 필요한 부분이 있다면 언제든지 거침없는 피드백 부탁드립니다! 부족한 글 읽어주셔서 감사합니다!

Reference

댓글