머신러닝&딥러닝

객체 지향 프로그래밍 #2 OOP의 특징

seungbeomdo 2023. 1. 16. 17:07
 

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

파이썬을 제대로 다룰 줄 아는 건지에 대한 의심이 들었다. 전처리, 기술 분석, 회귀 분석, 가설 검정, 시각화 이런 데이터 분석의 프로세스를 구현하는 능력은 있다고 생각했는데, 어쩐지 넘파

seungbeomdo.tistory.com


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자리를 살펴보았다. 각각의 특징들이 뭘 의미하는지는 알 것 같지만 여전히 이게 굳이 특징이라고까지 이름 붙여서 배워야 할 의미가 있는 건지는 모호하다. 프로그래머들이 실무를 통해서 조금씩 발전시킨 유용한 관습들이라고 생각하고 일단 넘어가자.