객체 지향 프로그래밍을 위한 4가지 기본개념 (추상화, 캡슐화, 상속, 다형성)

정재욱·2022년 7월 12일
0

Development_common_sense

목록 보기
3/4

객체 지향 프로그래밍의 4가지 기둥

추상화(Abstraction)
캡슐화(Encapsulation)
상속(Inheritance)
다형성(Polymorphism)

1) 추상화(Abstraction)

우선 추상화라는 사전적 정의를 한번 짚어봅시다. 추상화는 필요한 부분, 중요한 부분을 통합하여 하나로 만드는 것을 말합니다. 좀 더 쉽게 말하면 중요한 특징을 찾아낸 후 간단하게 표현하는 것이죠. 예를 들어, 강아지 집을 만들고 싶어서 강아지 집 설계도를 만들어 보려고 합니다. 설계도를 만들기 전에 필수적으로 떠올려 하는 요소들이 있을 겁니다.

예를 들어 지붕, 창문, 누울 공간등이 있습니다. 이런 내용을 바탕으로 설계도 안에는 지붕 설계, 창문 설계, 누울 공간설계등이 들어갑니다. 이 과정이 추상화 과정입니다.

어떠한 class를 만들기 전에 그 class를 어떻게 만들 것인지 미리 설계하고 들어가는 과정으로 추상화 클래스(abstract class)라는 것을 만듭니다. 추상화 클래스란 추상화 메소드가 하나라도 있는 클래스를 추상화클래스라고 합니다.(추상화 클래스안에 일반메소드가 있어도 상관없습니다) 
만약 추상화 클래스안의 메소드가 모두 추상화 메소드라면 인터페이스라고도 합니다.

아무튼, 추상화 클래스에서 만들어준 추상화 메소드나 변수는 다음에 상속 받을 자식 클래스안에서 반드시 오버라이딩 되어야 합니다. 

예시로 한번 강아지 집(DogHouse) 클래스를 만들어봅시다. 지붕 메소드, 창문 메소드, 공간 메소드 그리고 변수 하나를 자식 클래스에서 만들어줄겁니다. 

from abc import ABC, abstractmethod

class DogHouse(ABC):
    @abstractmethod  
    def create_roof(self, roof_color):  # 지붕 만들기
        pass

    @abstractmethod
    def creat_windows(self, window_count):  # 창문 만들기
        pass

    @abstractmethod
    def creat_space(self, where):  # 공간 사이즈 정하기
        pass

    @property
    def count(self):  # 집에 살 강아지 수
        pass

추상화 클래스를 만들기 위해 ABC 모듈과 abstractmehtod를 import를 한 후 ABC를 상속받습니다. 모든 메소드마다 pass라고 되어 있는건 DogHouse 클래스의 메소드들을 MyDogHouse에서 오버라이딩하여 완성시켜주기 위함입니다. 

그리고 마지막으로 dog_count라는 변수를 자식 클래스에서 만들어주고 싶어서 @property 데코레이팅하여 count라는 함수를 만들어주었습니다. 그리고 추상화 메소드로 만들고 싶은 메소드 위에 @abstractmethod라고 데코레이팅 해줍니다.

이제 위에서 만든 추상화 클래스를 상속받아 MyDogHouse 클래스를 만들어보겠습니다.

class MyDogHouse(DogHouse):

    def create_roof(self, roof_color):
        self.roof_color = roof_color


    def creat_windows(self, window_count):
        self.window_count = window_count


    def creat_space(self, where):
        self.where = where


    @property
    def count(self):
        return self.dog_count
    
    
    @count.setter
    def count(self, dog_count):
        self.dog_count = dog_count
    

    def __str__(self):
        return "강아지집 정보\n\n지붕 색: {}\n창문 갯수: {}\n공간: {}\n집에 사는 강아지수: {}".format(self.roof_color,self.window_count, self.where, self.dog_count)

DogHouse에 있던 모든 추상화 메소드를 오버라이딩하였고 count 함수에 대해 getter와 setter를 만들었습니다.

그리고 마지막으로 _str__라는 일반 메소드도 추가해주었습니다. 

my_dog_house = MyDogHouse()

my_dog_house.create_roof("빨강")
my_dog_house.creat_windows(2)
my_dog_house.creat_space("4평")
my_dog_house.count = 3 # count함수의 setter를 이용하였습니다

print(my_dog_house)

>>>
강아지집 정보

지붕 색: 빨강
창문 갯수: 2
공간: 4평
집에 사는 강아지수: 3

2) 캡슐화(Encapsulation)

  • 객체의 일부 구현 내용에 대한 외부로부터의 직접적인 액세스를 차단하는 것.
  • 객체의 속성과 그것을 사용하는 행동을 하나로 묶는 것.

외부 접근 차단하는 법:

  • 변수나 메서드 이름 앞에 언더바 두개(__)를 붙여준다.
class Citizen:
    """주민 클래스"""
    drinking_age = 19 # 음주 가능 나이

    def __init__(self, name, age, resident_id):
        """이름, 나이, 주민등록번호"""
        self.name = name
        self.set_age(age)                   
        self.__resident_id = resident_id
    
    def authenticate(self, id_field):
        """본인이 맞는지 확인하는 메소드"""
        return self.__resident_id == id_field

    def can_drink(self):
        """음주 가능 나이인지 확인하는 메소드"""
        return self.__age >= Citizen.drinking_age      

    def __str__(self):
        """주민 정보를 문자열로 리턴하는 메소드"""
        return self.name + "씨는 " + str(self.__age) + "살입니다!"

    def get_age(self):
        """숨겨 놓은 인스턴스 변수 __age의 값을 받아오는 메소드"""
        return self.__age

    def set_age(self, value):
        """숨겨 놓은 인스턴스 변수 __age의 값을 설정하는 메소드"""
        if value < 0:
            print("나이는 0보다 작을 수 없습니다. 기본 값 0으로 나이를 설정하겠습니다")
            self.__age = 0
        else:
            self.__age = value

# 주민 인스턴스 생성
young = Citizen("younghoon kang", 18, "87654321")

print(young.__str__()) # 출력: younghoon kang씨는 18살입니다!
print(young.__authenticate("87654321")) # 에러가 난다!!!

__가 붙은 변수에 접근하는 메서드

  • can_drink메서드는 self.__age를 사용한다.
  • 이처럼 __가 붙은 변수는 외부에서 사용 불가능하지만, 내부 메서드에선 접근이 가능하다.
  • 이것이 바로 캡슐화의 두 번째 정의인 "객체의 속성과 그것을 사용하는 행동을 하나로 묶는 것" 에 해당하는 내용이다.

getter, setter
캡술화된 변수의 값을 읽어주는 메서드는 getter, 할당해 주는 메서드를 setter라고 한다.

파이썬의 캡슐화와 캡슐화 문화
파이썬에서 캡슐화를 하기 위해 변수나 메소드를 숨기려면 이름 앞에 밑줄 2개(__)를 붙여야 한다고 했다. 그런데 사실 여기에는 특별한 원리가 숨어있다.

class Citizen:
    """주민 클래스"""
    drinking_age = 19 # 음주 가능 나이

    def __init__(self, name, age, resident_id):
        """이름, 나이, 주민등록번호"""
        self.name = name
        self.set_age(age)                   
        self.__resident_id = resident_id
    
    def authenticate(self, id_field):
        """본인이 맞는지 확인하는 메소드"""
        return self.__resident_id == id_field

지금 Citizen 클래스의 변수 __age, __resident_id는 클래스 밖에서 접근할 수가 없다.

여기서 Citizen 클래스의 내부를 들여다 보면

# 시민 인스턴스 생성
young = Citizen("younghoon kang", 18, "87654321") # (1)
print(dir(young))                                 # (2)

(1) Citizen 클래스로 young이라는 인스턴스를 하나 생성
(2) dir라는 함수를 사용하면 인스턴스가 갖고 있는 모든 변수와 메소드를 볼 수 있다.

young 인스턴스의 모든 변수 및 메소드 이름을 확인해보자.
위 코드를 실행하면 아래와 같이 출력된다.

['_Citizen__age','_Citizen__resident_id','__class__', '__delattr__', '__dict__', '__dir__' ... ]

가장 앞에 있는 '_Citizen__age', '_Citizen__resident_id'는 바로 우리가 이름 앞에 밑줄 2개(__)를 붙였던 변수 __age, __resident_id이다.

사실 변수나 메소드 이름 앞에 밑줄 두 개(__)를 쓰며느 파이썬은 그 앞에 추가적으로 "_클래스 이름"을 덧붙여서 이름을 바꿔버린다. 이걸 파이썬에서는 네임 맹글링(name mangling)이라고 한다. 맹글링의 동사형인 맹들은 영어로 "마구 썰다", "엉망진창으로 만들다"라는 뜻이다. 여기서는 이름을 새로운 형태로 변환하는 것을 맹글링이라고 한다.

즉, 방금 봤던 것 처럼 __age_Citizen__age로, __resident_id_Citizen__resident_id로 바뀌는 것 이다.

그럼 이 바뀐 이름으로는 클래스 밖에서도 접근할 수 있을까?

# 시민 인스턴스 생성
young = Citizen("younghoon kang", 18, "87654321")

print(young._Citizen__age) # 출력: 18
print(young._Citizen__resident_id) # 출력: 87654321

young._Citizen__age = -10
print(young) # 출력: younghoon kang씨는 -10살입니다!

출력 결과를 보면 바뀐 이름으로는 클래스 밖에서도 접근이 가능하다.

정리하면 클래스 안에서 이름 앞에 밑줄 2개(__)를 붙인 변수나 메소드는 네임 맹글링되어 아예 새로운 이름을 가지게 된다. 그리고 새 이름으로는 클래스 밖에서 접근이 가능하다. 그럼 결국 클래스 밖에서 접근할 수 있는 방법이 있으니까 캡슐화가 안 된 것 아닐까? 맞다. 캡슐화가 안 된 것이다. 아까는 밑줄 2개(__)만 붙이면 된다더니 무슨 말일까?

사실 파이썬은 언어 차원에서 캡슐화를 지원하지 않는다. 캡슐화처럼 보이긴 하지만 알고보면 완벽한 캡슐화는 아니다. 다른 객체 지향 언어인 Java에서는 private이라는 키워드를 변수 이름 앞에 붙이면, 외부로부터의 접근이 완벽히 차단된다. 파이썬처럼 바뀐 새 이름으로 접근할 수 있다거나 하는 방법도 없다.

하지만 파이썬이 캡슐화를 지원하지 않는다고 해서 캡슐화를 아예 무시하는 것은 아니다. 파이썬 세계의 개발자들은 조금 다른 방식으로 캡슐화를 한다.

파이썬의 캡슐화 약속: _한개

  • 파이썬 개발자들은 캡슐화를 위해 변수나 메소드 앞에 언더바를 하나만 붙인다.
  • 이렇게 하면 외부에서 해당 데이터에 직접 접근하지 말라는 경고다.
  • 물론 언더바 하나 더한 것 자체는 어떤 기능도 없다. 그냥 서로에게 주는 싸인일 뿐이다.
  • 여하튼 앞으로 캡슐화를 할 땐 언더바 하나를 앞에 붙이자.

@property, @age.setter

  • 클래스에 _age라는 변수가 있다 치자.
  • 이 변수는 캡슐화되어 외부에서 접근하지 말라는 뜻을 담고 있다.
  • 이 변수의 getter, setter 메서드를 @property, @age.setter를 통해 만들 수 있다
class Citizen:
    def __init__(self, name, age, id):
        self.name = name
        self.age = age
        self._id = id
        
    def authenticate(self, id):
        return self._id == id

    @property
    def age(self):
        print("나이를 리턴합니다")
        return self._age

    @age.setter
    def age(self, age):
        print("나이를 지정합니다.")
        if age > 0:
            self._age = age
        else:
            print("나이가 0보다 작습니다. 나이를 0으로 설정하겠습니다.")
            self._age = 0
            
            
person = Citizen("김판다", 25, "123456-1234567")
print(person.age)
person.age = 10
print(person.age)

>>>
나이를 지정합니다
나이를 리턴합니다
25
나이를 지정합니다
나이를 리턴합니다
10
  • 이 데코레이터를 통해 실제 _age를 사용할 때 age라고 쓰면서 사용할 수 있다.
  • 예시 코드에서 print(person.age) 이 실행되면 데코레이터에 덕에 @property가 달려있는 age 메서드(getter)가 실행된다.
  • 예시 코드에서 person.age = 10이 실행되면 데코레이터 덕에 @age.setter가 자동으로 실행된다.
  • 즉, _변수명의 변수가 있을 경우 변수명으로 함수를 만들고 그 위에 @property, @변수명.setter를 만들어주면 된다.

객체를 사용할 땐 최대한 메소드로

  • 변수에 직접 접근하는 코드가 많을수록 유지보수 하기가 어렵다.
  • 메서드로 변수에 접근하고, 그 메서드를 다른데서 가져가 쓰는 것이 제일 좋다.

3) 상속(Inheritance)

1. 상속이란?

  • 두 클래스 사이에 부모-자식 관계를 설정하는 것.
  • 벤츠-자동차 관계에서 벤츠는 자동차지만 자동차는 벤츠가 아니다.
  • 자동차는 벤츠의 부모다.
  • 벤츠는 자동차를 상속받는다.
  • 상속은 반복을 줄여준다.

2. mro, isinstance, issubclass 함수

mro(Method resolution order)

  • mro함수는 어떤 클래스의 상속 관계를 보여준다.
  • 아래 결과를 보면 Son -> Mom -> object로 가는 상속 관계를 보여준다.
  • ject는 보다시피 모든 클래스의 조상이다.
  • 로 help(class_name)으로도 상속 관계를 볼 수 있다.
class Mom:
    pass


class Son(Mom):
    pass

print(Son.mro())

>>>
[<class '__main__.Son'>, <class '__main__.Mom'>, <class 'object'>]

isinstance

  • 이 함수는 어떤 인스턴스가 주어진 클래스의 인스턴스인지 알려준다.
  • 첫 파라미터로 검사할 인스턴스 이름,
  • 두 번째 파라미터로 기준 클래스의 이름을 넘겨준다.을
  • 리턴값은 Boolean
class Mom:
    pass


class Son(Mom):
    pass


son = Son()

print(isinstance(son, Mom))
print(isinstance(son, object))
print(isinstance(son, list))

>>>
True
True
False

issubclass

  • 어떤 클래스가 다른 클래스의 자식 클래스인지 알려주는 함수다.
  • 첫 번째 파라미터로 검사할 클래스의 이름을,
  • 두 번째 파라미터로 기준이 되는 클래스의 이름을 넘겨준다.
  • 리턴값은 Boolean
class Mom:
    pass


class Son(Mom):
    pass


son = Son()

print(issubclass(Son, Mom))
print(issubclass(Son, object))
print(issubclass(Son, list))

>>>
True
True
False

3. overriding

  • 부모의 변수를 오버라이드 하려면 똑같은 변수명으로 오버라이드 하면 된다. (아래의 DeliveryMan의 raise_percentage처럼)
  • super().__init__(똑같은 인자 념겨주기)로 상속을 명시할 수 있다. (이렇게 안해도 자동으로 상속은 된다.)
class Employee:
    """직원 클래스"""
    company_name = "코드잇 버거"
    raise_percentage = 1.03

    def __init__(self, name, wage):
        """인스턴스 변수 설정"""
        self.name = name
        self.wage = wage


class DeliveryMan(Employee):
    """배달원 클래스"""
    raise_percentage = 1.1

    def __init__(self, name, wage, on_standby):
        super().__init__(name, wage)
        self.on_standby = on_standby

상속의 원리

  • 부모와 자식이 같은 이름의 메서드를 갖고 있다면 mro에서 제일 빠른 순번의 것이 먼저 호출된다.
  • 즉 자식에서 오버라이딩 한 메서드가 호출된다. 이런 원리를 통해서 오버라이딩이 가능한 것이다.

4. 다중상속
파이썬은 다중상속이 가능하다 (Java같은 언어는 상속 딱 하나만 가능)

방법
아래 예시의 C 클래스는 A, B를 다중상속 받았다.

class A:
    def __init__(self, a):
        self.a = a


class B:
    def __init__(self, b):
        self.b = b


class C(A, B):
  • 그런데 만약 C에서 super().init을 실행하면 A, B 중 어떤 클래스의 메서드가 실행되는 걸까?
  • 이런 점이 모호 하기 때문에 다중상속의 경우 super대신 부모 클래스의 이름을 명시해주거나 메소드 자체를 오버라이드 한다.

부모 클래스의 이름을 명시해주는 예제

class C(A, B):
    def __init__(self, a, b):
        A.__init__(self, a)
        B.__init__(self, b)

메소드 오버라이드 하는 예시

class C(A, B):
    def __init__(self, a, b):
        self.a = a
        self.b = b
  • 다중상속은 위험한 면이 있다. 그래서 어떤 언어들은 다중상속 자체를 지원 안한다.
  • 이런 위험성을 해결하기 위해 아래 두가지 솔루션을 제시한다.

다중상속 문제 해결 솔루션

  • 부모클래스끼리 같은 이름의 메소드 갖지 않기
  • 같은 이름의 메소드는 자식클래스에서 오버라이딩

4) 다형성(polymorphism)

  • 상속과 함께 OOP에서 중요한 개념 중 하나
  • 동일한 코드이지만 동작방법, 결과가 다른 것을 의미한다.
  • 다형성의 쉬운 예제는 Java의 오버로딩이다. 오버로딩은 같은 의미지만 매개변수의 데이터타입이 무엇이냐에 따라서 다른 메소드가 호출되는 방식이다. 참고 (참고로 파이썬은 오버로딩을 허용하지 않는다.)
  • 다형성을 잘 쓰면 IF / ELSE 문을 많이 줄일 수 있다.
  • 다형성은 코드의 양을 줄이고, 여러 객체 타입을 하나의 타입으로 관리가 가능하여 유지보수에 좋다.

Method Override도 다형성의 한 예이다.

# 클래스 선언
class Person:
	def __init__(self, name):
    	self.name = name
        
    def work(self):
    	print(self.name + " works hard")
    
class Student(Person):
	def work(self):
    	print(self.name + " studies hard")
        
class Engineer(Person):
	def work(self):
    	print(self.name + " develops something")
        
# 객체 생성
student1 = Student("Kane")
developer1 = Engineer("Mark")
student1.work()
developer1.work()

>>>
Kane studies hard
Mark develops something

메서드명을 동일하게 해서 같은 모양의 코드가 다른 동작을 하도록 하는 다형성 예

참고

https://www.fun-coding.org/PL&OOP1-8.html
https://seungjuitmemo.tistory.com/50?category=908971

profile
AI 서비스 엔지니어를 목표로 공부하고 있습니다.

0개의 댓글