코드를 작성할 때 프로그램의 개발과 보수를 용이하게 하거나, 직관적인 코드를 제공하는 등의 여러 요인으로 좋은 코드를 정의하게 된다. 좋은 코드를 정의할 때 언급되는 대표적인 방식인 객체 지향 프로그래밍(Object-Oriented Paradigm)에 대해서 알아보고, 이를 Python에서 적용한 예시를 살펴보자. (사진 출저: GeeksforGeeks, 내용 참고: realPython, programiz)
1. Object-Oriented Paradigm in Python 이 무엇이며, 어떤 면에서 유리한가?
객체 지향 프로그래밍(Object-Oriented Programming, OOP)는 컴퓨터 패러다임 중 하나로, 관련된 속성이나 동작을 개별 객체(object)로 묶음으로써 구조화된 프로그램을 구성하는 방법이다. 예를 들어, 객체는 이름, 나이, 주소와 같은 속성(properties)와 걷기,말하기, 호흡하기, 달리기와 같은 행동(behaviors)을 가진 사람을 나타낼 수 있다. 즉, 객체 지향 프로그래밍은 자동차와 같은 구체적이고 실제적인 사물 뿐만 아니라 회사와 직원, 학생과 교사 등과 같은 사물 간의 관계를 모델링하는 접근 방식이다.
OOP 방식을 통해 프로그램을 유연하고 변경이 용이하게 함은 물론, 프로그램의 개발과 보수를 간편하게 만들며, 직관적인 코드 분석을 가능케 한다. 하나의 문제 해결을 위해 데이터를 모아 놓은 (클래스의) 객체를 활용한 프로그래밍을 지향하여 응집력을 강화하고, 클래스 간에 독립적으로 디자인하여 결합력을 약하게 한다. Python에서 이를 적용한 예시를 살펴보도록 하자.
2. 클래스(Class) 정의하기
클래스(Class)는 사용자가 정의한 데이터 구조를 만드는데 사용된다. 클래스는 해당 클래스에서 생성된 객체가 가질 수 있는 속성(Attribute) 와 수행할 수 있는 동작과 작업을 식별하는 메서드(Method)라는 함수를 정의한다.
다음과 같이 개별 개 객체(Dog Object)가 가질 수 있는 특성 및 행동에 대한 일부 정보를 저장하는 Dog class를 정의할 수 있다.
class Dog:
#Class Variable
species = "Canis familiaris"
def __init__(self, name, age):
#Instance Variable
self.name = name
self.age = age
self.leg = 4
# Instance method
def description(self):
return f"{self.name} is {self.age} years old"
# Another instance method
def speak(self, sound):
return f"{self.name} says {sound}"
코드를 보면 개 객체가 가져야 하는 속성은 init()메서드와 클래스 내에 정의할 수 있다. 새 객체가 생성될 때마다 init() 함수를 통한 초기 상태를 설정하여 클래스의 각 새 instance를 초기화한다. 해당 코드에서는 객체 생성시 name과 age는 설정 가능하며, leg attribute는 4로 고정된다. 모든 개를 정의하는 인스턴스 속성과 반대로 모든 개 객체가 가지는 클래스 속성은 위에 정의되어 있는 바와 같이 species로, “Canis familiaris” 속성을 가짐을 알 수 있다.
클래스 내부에 정의하는 함수인 인스턴스 메서드(Instance method)도 존재합니다. 인스턴스 메서드는 해당 클래스의 인스턴스에서 호출이 가능한 함수이다. 해당 Dog 클래스에는 두가지 인스턴스 메서드인 .description()과 .speak()이 존재한다.
3. 객체(Object) 생성하기
위에서 정의한 Class 정보를 바탕으로 개 객체를 생성하면 다음과 같다.
buddy = Dog(“Buddy”, 9)
miles = Dog(“Miles”, 4)
이렇게 하면 Buddy라는 9살 된 개와 Miles라는 4살 된 두 개의 새로운 개 인스턴스가 생성됩니다. 두 개의 species는“Canis familiaris” 로 동일함을 미리 알 수 있다.
클래스를 사용하여 데이터를 구성할 때의 가장 큰 장점 중 하나가 인스턴스는 (클래스에서 정의된) 예상한 속성을 가질 수 있다는 점이다. 모든 개 인스턴스에는 .species, .name, .age, .leg 속성이 있고 이러한 속성은 항상 값을 반환하므로 안심하고 사용 가능하다. 이와 더불어 객체의 속성은 기본적으로 변경 가능하다. 다음과 같이 코드를 통해 확인해보자.
>>> buddy.age = 10
>>> buddy.age
10
>>> miles.description()
‘Miles is 4 years old’
>>> miles.speak(“Woof Woof”)
‘Miles says Woof Woof’
>>> miles.speak(“Bow Wow”)
‘Miles says Bow Wow’
다음과 같이 생성된 miles 객체에 대해서 메소드도 정의대로 잘 실행됨을 알 수 있다.
4. Python의 기능성(Functionality) 확장하기
OOP 패러다임의 가장 큰 특징 중 하나는 함수를 통한 기능성 확장이 가능하다는 점이다. 그 중에서도 대표적인 특징인 상속 (inheritance)와 다형성(Polymorphism)을 예시를 통해 확인해보자.
4-A. 상속(Inheritance)
상속은 이미 부모 클래스 용으로 작성된 클래스를 바탕으로 자식 클래스 코드를 작성하는 방법이다. 예를 들어, 부모 클래스인 Car 클래스의 일부 속성은 자식 클래스인 Bus, Truck 등의 클래스와 동일하다. 상속 특성을 통해 자식 클래스는 부모 클래스의 기능을 확장할 수 있다는 장점이 있다. 즉, 자식 클래스는 부모의 모든 속성과 메서드를 상속하지만 자신에게 고유한 속성과 메서드를 지정할 수 있다.
4-B. 다형성(Polymorphism)
자식 클래스와 부모 클래스의 inheritance 관계를 통해 자식 클래스는 메서드를 재정의할 수 있다. 다형성은 다양한 형태를 취할 수 있는 능력을 의미한다. 이러한 다형성은 메서드 오버라이딩 (Method Overriding)과 메서드 오버로딩(Method Overloading) 으로 존재합니다. Python은 둘 중 메서드 오버라이딩 만을 지원한다.
- 메서드 오버라이딩(Method Overriding)
위에서 서술된 개 클래스를 바탕으로, 오버라이딩된 다음 예제를 확인해보자.
class GoldenRetriever(Dog):
def speak(self, sound="Bark"):
return super().speak(sound)
개의 품종 중 하나인 GoldenRetriever 에 대한 자식 클래스이다. 개 품종마다 짖는 소리가 약간 다르기 때문에 부모 클래스에 나온 .speak() 함수를 재정의해야 한다. GoldenRetriever 클래스에서는 부모 클래스인 Dog의 메서드를 상속하여 그대로 사용함을 알 수 있다. 자식 클래스에 대한 객체를 생성해보고 함수를 실행한 결과를 보자.
>>> jim = GoldenRetriever(“Jim”, 5)
>>> jim.speak()
‘Jim says Bark’
코드 실행 시 Jim라는 5살 된 골든 리트리버 품종의 개 인스턴스가 생성된다. 메서드 오버라이딩을 통해, 이전에 Dog 객체에서 speak 함수를 부를 때 sound 파라미터를 넣을 필요없이 품종에 따라 바로 함수가 작동됨을 알 수 있다.