본문 바로가기
Python

[Python] 객체 지향 프로그래밍_1

by 박사과정 모닝 2023. 11. 11.
반응형

시작하기에 앞서

객체 지향 프로그래밍(OOP: object oriented programming)은 우리가 살고 있는 실제 세계를 모방해서 프로그래밍 하는 개념입니다. 이 부분은 사실 절차 지향 프로그래밍 언어(POP: protocol oriented programming)인 랩뷰로 프로그래밍을 시작한 제가 파이썬을 배우기 시작하면서 굉장히 겁을 먹었던 부분이기도 한데요, 그도 그럴 것이 랩뷰로 객체 지향을 구현하는 것의 난이도가 정말 우주 끝에 있는데 제가 그걸 도전했다가 여러번 실패한 경험이 있기 때문이에요... 그렇지만 실제로 마주쳐 본 파이썬의 객체 지향은, 절차 지향 언어에 뇌가 절여졌다고 해서 절대 정복 못 할 개념이 아니었습니다! 비전공자이자 절차 지향 언어를 먼저 배운, 절대적으로 불리한 시작점에서 객체 지향을 이해한 사람으로서 이 포스팅을 통해 독자분들에게 객체 지향을 잘 전달해 볼 수 있도록 하겠습니다!

 

객체 지향 프로그래밍 vs 절차 지향 프로그래밍

우선 절차 지향 프로그래밍을 먼저 살펴보면, 우리가 어떤 언어를 배우던 처음 코딩을 연습해 보는 단계에서 쉽게 접할 수 있습니다. 말 그대로 '논리적인 순서'를 가지고 돌아가는 코드를 짜는 것을 이야기합니다. 절차 지향 코드 구성에서 중요한 것은 해결하고자 하는 문제의 풀이 절차를 논리적으로 잘 정리하고, 적절한 변수를 생성하여 활용하는 것입니다. 가장 중요한 것은 '절차'입니다. 코드를 읽을때, 코드가 하는 일을 순서(절차)를 가지고 이해합니다. 이와 반대로 객체 지향 프로그래밍은 코드 구성에서 가장 중요한 것을 절차가 아닌 '객체'로 본 것입니다. 고유한 기능을 가지는 객체들을 '클래스'라는 것으로 생성하고, 이들을 활용해 다양한 일을 합니다. 코드는 '순서' 보다는 '어떤 객체가 무슨 일을 하는지'에 초점을 맞춰 이해해야 합니다.

 

쉽게 예를 들어 설명해 보도록 하죠. 엄밀한 비유가 아닐 수 있지만, 우리가 '집안일'이라는 프로그램을 작성한다고 가정합니다. 우리는 오늘 빨래를 하고, 바닥을 쓸고, 설거지를 해야 합니다.

 

만약 '집안일'이라는 프로그램을 절차 지향적으로 작성한다면, 중요한 것은 '절차'입니다. 세 가지 집안일 중 무엇을 먼저 할 지를 정합니다. 코드를 실행할 때 마다 랜덤으로 순서를 구성하도록 해도 좋습니다. 만약 빨래-바닥 쓸기-설거지 순서로 한다면, 우리가 짜는 코드는 [빨래를 세탁기에 넣는다 - 세탁기에 세제를 넣는다 - 세탁기를 실행한다 - (빨랫감을 불린다 - 세탁한다 - 헹군다 - 탈수시킨다) - 빨래를 꺼낸다 - 청소기 전원을 켠다 - 청소기를 가지고 방을 한 바퀴 돈다 - 청소기를 끈다 - 식기세척기에 그릇을 넣는다 - 식기세척기를 실행한다 - 식기세척기에서 그릇을 꺼낸다]와 같은 형태가 됩니다. 이 과정에서 세탁기, 청소기, 식기세척기, 빨랫감, 세제, 그릇 등의 변수가 선언될 수도 있고, 빨래와 세제를 입력받아 세탁을 하는 함수를 생성할 수도 있습니다. 여하튼, 가장 중요한 것은 '절차'이며, 어떤 순서로 처리할지를 생각하며 코드를 구성합니다.

 

만약 이 프로그램을 객체 지향적으로 작성한다면, 중요한 것은 '객체'입니다. 우리는 세탁기, 청소기, 식기세척기라는 3개의 클래스 객체를 생성합니다. 세탁기라는 클래스에는 빨랫감의 양, 빨랫감의 종류, 세제의 양 이라는 필드가 포함되어 있고, 세탁하기 라는 메서드도 포함되어 있습니다. 좀 더 자세히는 불리기, 세탁, 헹굼, 탈수 등의 다양한 메서드로 생성할 수도 있겠죠. 이렇게 세탁기, 청소기, 식기세척기라는 3개의 클래스를 생성한 다음, 우리는 처리하고자 하는 집안일의 순서에 따라 '세탁기, 청소기, 식기세척기'의 객체를 호출해서 일을 시킵니다. 절차 지향과 다르게 '객체' 자체에 초점이 맞춰진 차이점이 느껴지나요?

 

클래스(class)

위에서 자연스럽게 언급한 것 처럼, 파이썬에서는 객체지향 개념을 적용하기 위해서 클래스라는 개념을 사용합니다. 클래스를 생성하는 가장 기본적인 틀은 다음과 같습니다.

class 클래스명:
    # 클래스가 가지는 속성(필드, field)
    field 1
    field 2
    # 클래스가 가지는 기능(메서드, method)
    method 1
    method 2

 

      def 라는 단어로 시작하여 함수를 정의했던 것 처럼, class 라는 단어로 시작하면 클래스를 생성할 수 있습니다. 클래스명 뒤에 :(콜론)을 붙여 클래스 선언이 시작되었음을 알리며, 아래에 들여쓰기 되어있는 부분이 클래스를 서술하는 부분입니다. 위에서 이야기한 객체 지향의 핵심 아이디어와 같이, 클래스는 '현실 세계의 사물을 소프트웨어적으로 구현하려고 고안된 개념'입니다. 가장 많이 비유되는 것이 '자동차 설계도', '붕어빵 틀'입니다. 그 중 자동차 설계도에 빗대어 클래스를 설명해보겠습니다.

 

     우리가 어떤 자동차를 설계한다고 가정합니다. 우리가 설계할 자동차의 설계 요소들로는 [자동차의 색상], [자동차의 속도]가 있습니다. 이 두 가지는 자동차가 가지는 어떠한 속성입니다. 이러한 속성을 필드(field)라고 합니다. 그렇다면 우리가 설계하는 자동차는 어떤 기능을 가질까요? [속도 올리기], [속도 내리기], [정지] 등의 기능을 가질 수 있습니다. 이러한 기능들은 클래스 내부에서 함수와 같은 형태로 선언되는데, 클래스 안에서 정의된 함수들을 메서드(method)라고 합니다. (특히, 메서드라는 용어를 함수와 혼동하지 않도록 해야 합니다. 메서드는 어떠한 클래스에서 구현된 고유한 기능이고, 함수는 클래스와 관련 없이 구현된 어떠한 동작을 하도록 하는 것입니다. 이는 조금 뒤에 다시 한번 설명합니다!) 이러한 정의에 따라 Car 클래스를 구현해보면 아래와 같습니다.

class Car:
	color = ''
	speed = 0

	def upSpeed(self, value):
		self.speed += value
	def downSpeed(self, value):
		self.speed -= value
	def carStop(self):
		self.speed = 0

      위의 코드를 뜯어봅시다. Car 라는 클래스는 color와 speed 라는 필드 값을 가지고, 각각의 필드는 빈 문자열과 0이라는 숫자로 초기화됩니다. 또한 upSpeed, downSpeed, carStop 이라는 세 가지 메서드(기능)을 사용할 수 있습니다.

      여기서 눈여겨 볼 점은 메서드(기능)을 정의할 때, 기존에 함수를 정의하던 것과 마찬가지로 def를 사용한다는 것입니다. 그리고 클래스 바깥에서 함수를 정의할 때와 다른 차이점이 하나 있습니다. 바로 각각의 메서드가 받는 매개변수에 항상 'self'가 포함되어야 한다는 점입니다. self는 클래스 자기 자신의 주소를 가지고 있습니다. self는 필수적으로 들어가야 하는 것이므로, 그냥 묻고 따지지도 말고 당연히 입력하는 것으로 받아들입니다! 다만, upSpeed 메서드를 호출해서 쓸 때 self 매개변수를 전달받지는 않습니다. upSpeed 메서드를 살펴보면, 매개변수로 value를 받습니다.

      메서드 내부를 살펴봤을 때, 위에서 정의한 필드인 speed에다가 입력받은 value를 더하는 것 같은데 앞에 self. 라는 것이 붙습니다. 이 self.는 speed라는 변수가 이 클래스의 필드를 가리키는 것임을 명시해주는 것입니다. 클래스 내부의 필드에 접근하려면 반드시 self.를 필드명 앞에 명시해 주어야 합니다.(반대로, 메서드 안에서 필드에 접근할 일이 없으면 self는 생략해도 됩니다.) 또한 이 self라는 것은 객체를 생성해야지만 활성화됩니다. (더 읽다 보면 이해가 되겠지만, 지금 간단하게 설명해보자면 아직은 설계도만 작성해둔 상태이기 때문에 self가 활성화 되지 않았으며 나중에 이 설계도로 어떠한 '실제 차'를 만들고 나면 self가 활성화 됩니다.)

 

인스턴스(instance)

     이제 위에서 자동차 클래스를 완성했습니다. 이는 설계도 작성을 완료한 것과 같습니다. 자동차 설계도를 완성했다고 해서 실제 자동차가 생긴 것은 아닙니다. 객체 지향에서 실제로 생성된 자동차를 객체 = 인스턴스(instance)라고 합니다. 우리는 기본이 되는 자동차 설계도를 가지고 빨간색, 파란색, 노란색의 자동차를 생성할 수 있으며, 또 각 자동차의 속도를 개별적으로 제어할 수 있습니다. 먼저 자동차를 생성해보죠!

myCar1 = Car()
myCar2 = Car()
myCar3 = Car()

     위에서 만들었던 Car()라는 클래스(설계도)를 가지고, Car 객체를 3개 만들었습니다. myCar1, myCar2, myCar3라는 3대의 자동차를 만들기 위해 3명의 자동차 장인에게 똑같은 Car() 라는 설계도를 전달했다고 칩시다. 각 자동차들은 이제 각각 색깔이 정해지고, 속도도 정해집니다.

myCar1.color = 'red'
myCar2.color = 'blue'
myCar3.color = 'yellow'

myCar1.speed = 0
myCar2.speed = 0
myCar3.speed = 0

     3대 자동차의 색깔을 각각 red, blue, yellow로 설정하고 속도 또한 0으로 설정했습니다. 지금은 차가 멈춰 있으니 속도를 0으로 설정했지만 달리고 있다면 다른 속도로 설정해도 됩니다. 여기서 눈여겨 볼 것은 [인스턴스명.필드명]의 형태로 객체의 필드를 변수처럼 사용한 것입니다.  자동차 인스턴스가 하나 생성될 때 마다, 각 자동차는 독립적인 메모리 공간을 차지하기 때문에 서로 영향을 주지 않습니다. 그렇기 때문에 .speed라고 똑같이 쓰더라도 서로 다른 객체의 필드이므로 서로 다른 것입니다. 위와 비슷하게 메서드를 호출할 때에도 [인스턴스명.메서드명(매개변수)]의 형태로 사용할 수 있습니다.

myCar1.upSpeed(30)

 

생성자(constructor, __init__)

     생성자는 인스턴스를 생성하면 무조건 호출되는 메서드입니다. 따라서 필드의 값들을 초기화 하는 데에 사용할 수 있습니다. 즉, 인스턴스를 생성하면서 필드값을 초기화시키는 또 하나의 메서드입니다. 생성자는 __init__()의 형태를 가집니다. 앞/위로 언더바( _ )가 두개씩 붙습니다. 파이썬에서 언더바가 앞뒤로 두개 붙은 것은 파이썬에서 예약해 둔 것입니다. 따라서 우리는 이 이름을 쓸 수 없습니다. 아래를 보면 init에도 동일하게 self를 매개변수로 받는 것을 확인할 수 있습니다.

class myName:
	def __init__(self):
    	#초기화 코드 입력

위에서와 같이 self 외에 다른 매개변수를 입력받지 않는 것을 '기본 생성자'라고 합니다. 기본 생성자를 사용해 위의 Car 클래스를 수정하면 아래와 같습니다.

class Car:
	color = ''
	speed = 0
	def __init__(self):
		self.color = 'red'
		self.speed = 0
	def upSpeed(self, value):
		self.speed += value
	def downSpeed(self, value):
		self.speed -= value
	def carStop(self):
		self.speed = 0

파이썬 인터프리터에서는 클래스 안에 생성자가 존재하지 않으면, 암묵적으로 아무런 값으로도 설정되지 않는 기본생성자를 추가합니다. 따라서 위에서 생성자를 만들지 않았을 때에도 오류가 없었습니다. 파이썬에서 생성자를 활용할 때에 지켜야 할 규칙이 몇 가지 있습니다. 우선 생성자 이름은 __init__()으로 설정해야 합니다. 다른 이름을 사용할 수 없습니다. 그리고 다른 메서드와는 달리 return 값이 없습니다. 그리고 생성자는 단 하나만 존재해야 합니다.

 

     그렇다면 기본 생성자를 통해서 자동차의 color 값과 속도를 초기화할 수 있는 것은 알았는데, 그렇다면 매번 초기값을 가지는 인스턴스를 생성한 다음 번거롭게 색상과 속도를 따로 바꿔줘야 할까요? 아닙니다. 초기값을 매개변수로 받으면 이 문제를 해결할 수 있습니다. 다만, 매개변수를 받는 생성자를 만들어 두었다면 인스턴스를 생성할 때 반드시 매개변수를 다 받아줘야 합니다. 또 위에서 언급한 규칙과 마찬가지로 생성자는 단 하나만 존재해야 하므로 우리는 기본생성자 또는 매개변수 생성자 둘 중 하나를 '선택'해서 '하나만' 만들어야 합니다.

class Car:
	color = ''
	speed = 0
	def __init__(self, color, speed):
		self.color = color
		self.speed = speed
	def upSpeed(self, value):
		self.speed += value
	def downSpeed(self, value):
		self.speed -= value
	def carStop(self):
		self.speed = 0

 

캡슐화: getter() 메서드

     클래스에서는 '캡슐화'라는 개념이 있습니다. 클래스 바깥에서 클래스의 필드를 마음대로 접근하거나 바꾸지 못 하도록 클래스의 필드가 외부에 노출되는 것을 방지하는 것을 의미합니다. 그래도 클래스의 필드 값이 필요한 때가 많기 때문에, 안전하게 잘 감싸둔 필드를 요청할 때 마다 살짝 꺼내어 보여주는 메서드를 getter() 메서드라고 합니다. 아래와 같이 구현할 수 있습니다.

class Car:
	color = ''
	speed = 0
	def __init__(self, color, speed):
		self.color = color
		self.speed = speed
	def getColor(self):
		return self.color
	def getSpeed(self):
		return self.speed

위의 코드를 보면, getColor(), getSpeed() 메서드를 호출하면 반환값으로서 필드값인 self.color와 self.speed를 반환하는 것을 볼 수 있습니다. 

 

인스턴스 변수 vs 클래스 변수

     클래스 개념에서 변수는 크게 인스턴스 변수와 클래스 변수로 나뉩니다. 우리가 위에서 이제껏 생성하고 사용했던 변수들은 모두 인스턴스 변수입니다. 인스턴스 변수는 인스턴스를 생성해야만 실제로 메모리를 할당받고, 우리가 사용할 수 있는 변수입니다. Car 클래스 내부에 color와 speed 필드를 정의했어도, 이는 아직 'Car 클래스에는 color랑 speed라는 필드가 구성되어있어~' 라고 설계도에 적어두기만 한 것입니다. 이 두가지 필드를 Car 클래스 속성의 인스턴스를 생성할 때 실제로 생성됩니다. 여기서 인스턴스 변수는 '각 인스턴스에서 각자 가지고 있는 인스턴스 개인 소유'의 변수입니다. 아무리 같은 설계도(클래스)를 바탕으로 생성된 인스턴스라고 할지라도, 서로 다른 객체의 필드(다른 인스턴스의 인스턴스 변수)를 침범할 수 없죠! 위에 보이는 myCar1.speed와 myCar2.speed는 같은 Car() 클래스의 설계도를 통해 생성된 인스턴스 변수이지만, 각자 메모리에서 독립적인 공간을 차지하며 완전히 서로 다른 존재입니다. 

 

     그렇다면 클래스 변수는 무엇일까요? Car() 클래스 설계도를 통해 생성된 모든 인스턴스들이 함께 공유하는 '공유 변수'라고도 할 수 있습니다. 같은 Car() 클래스로부터 생성된 객체라면, 공통된 클래스 변수를 공유합니다. 만약 Car() 클래스를 통해 생성된 자동차 인스턴스의 숫자를 cnt라는 클래스 변수로 세어본다면, 인스턴스가 하나 생성될 때 마다 +1씩 증가하도록 할 수 있고 어떤 인스턴스에서든지 동일한 메모리에 접근하여 cnt 값을 호출합니다. cnt 변수는 메모리에 '단 하나' 존재합니다. 인스턴스를 생성하는 순간 인스턴스 변수가 메모리에 새로 할당되는 것과는 달리 클래스 변수는 메모리의 다른 공간에 이미 생성되어 있는 공간을 공유합니다. 클래스 변수는 [클래스명.필드명]으로 사용합니다.

class Car:
	color = ''
	speed = 0
    cnt = 0
	def __init__(self, color, speed):
		self.color = color
		self.speed = speed
        	Car.cnt +=1
	def getColor(self):
		return self.color
	def getSpeed(self):
		return self.speed

차이점이 정리 되시나요? 필드명 앞에 self를 붙이면 인스턴스 변수, 클래스 이름을 붙이면 클래스 변수입니다.

반응형