1. OOP의 특징 1) 추상화(Abstraction)
1편에서 객체 지향 프로그래밍이라는 개념을 사용했지만, 이게 뭘 뜻하는지는 언급하지 않았다. 객체 지향 프로그래밍이란 여기서 설명할 4가지의 특징을 가진 프로그래밍을 말한다. 4가지의 특징들을 알아보도록 하자. 첫번째는 추상화이다.
- 추상화란 객체들의 공통적인 특징을 뽑아 이름 붙이는 것을 말한다. 객체들의 공통적인 설계도인 클래스를 정의하고, 클래스로부터 객체들에서 사용될 속성과 메서드를 구성하는 것이 추상화라고 이해하면 된다.
- 1편에서 계산기 객체들을 만들 때 객체들마다의 구체적인 인자들과 연산 기능들을 정의하지 않았다. 가령 1편에서 다룬 계산기 객체들을 다시 보면
cal1 = Cal(1,2)
cal2 = Cal(3,4)
- cal1이라는 계산기는 1과 2라는 숫자를 가지고 계산을 수행하고, cal2라는 계산기는 3과 4라는 숫자를 가지고 계산을 수행한다. 1과 2, 3과 4는 구체적인 숫자이지만 '계산의 대상이 되는 수'라는 공통의 특징을 가진다.
- 또한 두 계산기는 각각 덧셈, 뺄셈, 곱셈, 나눗셈 연산을 수행하지만 이들은 두 계산기의 공통의 특징이다.
- 그래서 우리는 두 객체를 생성하는, 따라서 두 객체를 포괄하는 상위 개념으로서 클래스를 만들었다. 그리고 두 객체에서 공통적으로 사용될 추상적인 '계산의 대상이 되는 수'와 '계산 기능들'을 먼저 정의한 후에 구체적인 계산기들로 구현해낸 것이다.
- 이러한 특징을 추상화라고 이해할 수 있다.
2. OOP의 특징 2) 상속성(Inheritance)
- 2번째 특징인 상속성이란 부모 클래스로부터 자식 클래스의 관계를 총칭한다. 몇 가지 하위 특성으로 나누어서 이해해볼 수 있다.
2.1. 자식 클래스는 부모 클래스의 속성과 메서드를 상속한다.
- 자식이 부모로부터 부모의 여러가지 것들을 물려받듯이, 자식 클래스는 부모 클래스의 모든 속성과 메서드를 물려받는다. 1편에서 만든 계산기 클래스는 다음과 같다.
class Cal:
def __init__(self, a, b):
self.a = a
self.b = b
def add(self):
return self.a + self.b
def sub(self):
return self.a - self.b
def mul(self):
return self.a * self.b
def div(self):
return self.a / self.b
- 이 계산기 클래스를 부모 클래스로 상속하는 자식 클래스를 만들어보자.
class Cal_advanced(Cal):
pass
- Cal_advanced라는 자식 클래스는 부모 클래스인 Cal의 모든 속성과 메서드를 상속했다. 이 클래스로 만든 인스턴스들은 Cal에서 사용된 모든 속성과 메서드를 따로 정의하지 않고도 사용할 수 있다.
- cal_advanced1은 Cal이 아니라 Cal_advanced에 의해 만들어진 인스턴스이지만, Cal에서 정의된 속성 a와 메서드 div()를 모두 사용할 수 있음을 확인할 수 있다.
2.2. 자식 클래스는 부모 클래스의 메서드를 수정할 수 있다.
- 자식이 부모의 특징을 물려받지만 항상 똑같이 답습할 필요는 없듯이, 자식 클래스도 부모 클래스의 메서드를 수정해서 발전시키는 것이 가능하다.
class Cal_advanced(Cal):
def pow(self):
return self.a ** self.b
- Cal을 상속한 Cal_advanced는 Cal의 모든 사칙연산 메서드들을 사용할 수 있고, 추가적으로 제곱 연산까지 수행할 수 있게 되었다.
- 만약 부모 클래스에 추가로 메서드를 정의하는 것이 아니라, 부모 클래스의 메서드 자체를 수정해버리면 어떻게 될까? 이 경우 자식 클래스에서 수정된 바와 같이 메서드가 다시 정의된다. 이를 메서드 오버라이딩(Method Overriding)이라고 한다.
- 앞서 정의한 Cal 클래스에서는 나눗셈 연산을 할 때 분모가 0이 되어서는 안 된다는 조건을 달아주지 않았다. 따라서 자식 클래스인 Cal_advanced에서 이를 보완하여 분모가 0이 되어서는 안 된다는 조건을 추가해보자.
class Cal_advanced(Cal):
def div(self):
if self.b == 0:
raise ValueError("Denominator could not be zero")
else:
return self.a / self.b
def pow(self):
return self.a ** self.b
- Cal 클래스를 상속한 cal1 인스턴스는 5를 0으로 나누라고 했을 때 파이썬의 내장된 오류 메세지인 'division by zero'를 발생시키지만, Cal_advanced 클래스를 상속한 cal_advanced1 인스턴스는 직접 설정한 오류 메세지인 'Denominator could not be zero'를 발생시킨다.
2.3. 파이썬의 모든 클래스는 Object 클래스를 상속한다.
- 파이썬의 모든 것은 객체라고 불린다. 이 점은 단지 개념적인 것이 아니라 파이썬 내에 프로그래밍되어 있는 사실이다. 클래스에 mro 메서드를 적용하면 해당 클래스가 상속한 클래스들을 보여준다.
- Cal_advanced는 Cal 클래스를 상속했다는 것을 알 수 있다. 그런데 이때 object라는 클래스도 상속했다는 것을 함께 알려준다. 마찬가지로 유저에 의해서는 아무것도 상속하지 않은 Cal 클래스도 object 클래스는 상속하고 있다는 것을 알려준다.
3. OOP의 특징 3) 캡슐화(Encapsulation)
- 3번째 특징인 캡슐화란 객체의 속성과 행위를 하나로 묶고 구현된 일부를 외부에 감추어 은닉하는 것을 말한다. 먼저 '구현된 일부를 외부에 감추어 은닉'한다는 것이 무슨 뜻인지 알아보자.
3.1. 객체의 일부를 외부로부터 은닉하기
- 객체를 생성했을 때, 객체의 속성이 수정되지 않도록 보호해야 하는 경우가 있을 수 있다. 이때 보호해야 하는 속성을 private 속성, 그렇지 않은 경우 public 속성이라고 한다. 지금까지 다룬 속성들은 모두 public 속성이었으므로 private 속성을 만드는 방법을 알아보자.
- 계산기 클래스를 동일하게 만드는데, 계산기의 버전(version)을 속성으로 추가하고 다른 버전들과 구분하기 위하여 계산기의 버전을 숨겨야(수정 불가능하게 해야) 한다고 생각해보자. 속성을 정의할 때 __을 붙여 정의하면 된다.
class Cal:
def __init__(self, a, b, version):
self.__version = version #__을 붙여 정의하면 외부 접근이 불가능한 private 변수가 된다.
self.a = a
self.b = b
def add(self):
return self.a + self.b
def sub(self):
return self.a - self.b
def mul(self):
return self.a * self.b
def div(self):
return self.a / self.b
- 인스턴스를 생성한 후, version 속성을 호출하면, 분명 클래스에서 정의했음에도 불구하고 그런 속성은 존재하지 않는다고 나온다. 즉 은닉을 통해 private 속성을 보호하는 것이다. 아예 접근이 불가능하므로 버전을 바꿀 수도 없다.
3.2. 객체의 속성과 행위를 하나로 묶기
- 그런데 이렇게 아예 접근조차 불가능하게 하는 것은 조금 불편하다. 버전이 얼마인지는 확인하게 하되 수정만 막아놓는 것이 더 합리적으로 보인다. 이를 가능하게 하기 위해 파이썬에서는 '속성과 행위'를 하나로 묶는 방법을 택하고 있다. 이게 무슨 뜻인지 다음 코드를 통해서 이해해보자.
class Cal:
def __init__(self, a, b, version):
self.__version = version #__을 붙여 정의하면 외부 접근이 불가능한 private 변수가 된다.
self.a = a
self.b = b
@property
def version(self): #version을 호출하는 함수를 만든다.
return self.__version
def add(self):
return self.a + self.b
def sub(self):
return self.a - self.b
def mul(self):
return self.a * self.b
def div(self):
return self.a / self.b
- property decorator를 붙여서 version을 호출하는 함수인 version()을 정의하였다. 이렇게 되면 cal1 인스턴스의 버전이 얼마인지를 확인할 수 있다. 그러나 이 경우에 version을 수정하는 것은 불가능하다.
- 그럼 수정까지 가능하게 하려면 어떻게 해야할까? 이 경우에도 속성을 수정하는 함수를 클래스 내부에서 만들고 이 함수를 실행하면 된다.
class Cal:
def __init__(self, a, b, version):
self.__version = version #__을 붙여 정의하면 외부 접근이 불가능한 private 변수가 된다.
self.a = a
self.b = b
@property
def version(self):
return self.__version
@version.setter
def version(self, new_version): #수정도 가능
if new_version < self.version:
raise TypeError("invalid version")
else:
self.__version = new_version
def add(self):
return self.a + self.b
def sub(self):
return self.a - self.b
def mul(self):
return self.a * self.b
def div(self):
return self.a / self.b
- 이 경우에는 'private속성'.setter를 decorator로 달고 속성을 수정하는 함수를 생성한다. 그리고 version을 수정하는 데 있어서 규칙을 부여했는데 현재 버전보다 작은 버전으로는 수정할 수 없다는 것이다.
- 버전을 1로 지정한 계산기는 setter 함수를 사용해 버전을 2로는 바꿀 수 있지만, 현재 버전보다 이전 버전으로 수정하려고 하면 에러를 발생시킨다.
- 설명한 내용들이 앞서 언급한 '속성과 행위를 하나로 묶는'다거나 '속성을 외부로부터 보호하는 것'과 무슨 상관인지 이해하기가 어려울 수 있다. 먼저 우리가 '속성을 외부로 보호하는 것'은 '정말로 보호하고 싶어서 그런 것'일 수도 있지만 대개는 '의도치 않게, 별 생각 없이 중요한 속성을 수정하게 되는 것'으로부터 보호함을 의미한다.
- 따라서 속성 자체를 아예 숨긴다기보다는, 속성으로 접근하는 메서드를 만들고, 또 속성을 조건에 맞게 수정할 수 있는 메서드를 따로 만드는 것이다. 그리고 이 메서드들을 통해서만 접근 및 수정이 가능하게 함으로써(속성과 행위를 하나로 묶어) 의도치 않은 수정이 발생하지 않도록 하는 것이다. 이런 아이디어를 이해한다면 '캡슐화'는 말이 어려워서 그렇지 상당히 합리적인 방식으로 보인다.
4. OOP의 특징 4) 다형성(Polymorphism)
- 블로그에서 다룰 마지막 특징인 다형성이란 동일한 명칭의 속성/메서드가 다양한 형태를 가질 수 있도록 하는 것을 으미한다. 파이썬에서는 주로 동일한 코드가 클래스에 따라 또는 인스턴스를 생성할 때 부여한 인자에 따라 다른 결과를 내는 것을 의미한다.
- 계산기의 예시를 가져와 이해해보자. 두 개의 계산기를 만드는데 각각 다른 버전을 부여했다. 버전 1을 부여한 계산기는 버전 속성에 접근했을 때 1을 리턴하지만, 버전 2를 부여한 계산기는 버전 속성에 접근했을 때 2를 리턴한다. 두 가지 모두 '인스턴스'.version 이라는 동일한 코드에 의해 실행되지만 그 결과는 다르다.
- 이는 어떻게 보면 추상화된 객체가 다양한 형태를 가지고 구체화되는 것이라고도 이해할 수 있겠다.
- 다른 예를 들어보자. 다음과 같이 두 개의 계산기 클래스를 만들고, 각각으로부터 두 개의 인스턴스를 만들었다.
class Cal_add:
def __init__(self, a, b):
self.a = a
self.b = b
def result(self):
return self.a + self.b
class Cal_mul:
def __init__(self, a, b):
self.a = a
self.b = b
def result(self):
return self.a * self.b
- cal1은 덧셈 계산기이고, cal2는 곱셈 계산기이다. 두 계산기에 대하여 result 메서드를 적용하면, 동일한 코드임에도 불구하고 서로 다른 결과를 산출한다.
이상으로 객체 지향 프로그래밍의 기본 특징 4자리를 살펴보았다. 각각의 특징들이 뭘 의미하는지는 알 것 같지만 여전히 이게 굳이 특징이라고까지 이름 붙여서 배워야 할 의미가 있는 건지는 모호하다. 프로그래머들이 실무를 통해서 조금씩 발전시킨 유용한 관습들이라고 생각하고 일단 넘어가자.
'머신러닝&딥러닝' 카테고리의 다른 글
Machine Learning #1 Linear Regression : 근로자 임금 회귀분석 (0) | 2023.02.06 |
---|---|
UNET 구조 구현하기 #2: 데이터 로더 ~ 모델 검증 (0) | 2023.01.30 |
UNET 구조 구현하기 #1: 데이터 저장하기 ~ 모델 클래스 구현 (0) | 2023.01.24 |
객체 지향 프로그래밍 #3 Types (0) | 2023.01.17 |
객체 지향 프로그래밍 #1 클래스와 인스턴스 (0) | 2023.01.15 |