저번 시간에는 객체 지향 프로그래밍을 연습해보는 시간을 가졌습니다.

이번 시간에는 객체 지향 프로그래밍의 기준이 되는 4가지 기둥에 대해 알아보고 첫번째 기둥인 추상화에 대해 배워보겠습니다.

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

객체 지향 프로그래밍을 하기 위해 알아야 할 4가지 기둥이 있습니다. 이 4가지는 각각,

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

입니다. 생소해 보이는 단어들이죠? 하지만 설명을 들으면 그리 어려운 개념이 아니라는 걸 알 수 있습니다. 하나씩 자세히 살펴볼까요?

📋 추상화

우리는 일상생활에서 컴퓨터를 아주 많이 쓰지만 컴퓨터의 내부 구성과 동작 원리에 대해 자세히 알지는 못합니다. 이들은 여러 개발과 연구를 거쳐 만들어진 것이기 때문에 이해하기가 매우 복잡합니다.

이렇게 복잡하고 세밀하며 이해하기 힘든 작업들이 컴퓨터를 사용하고 있는 동안 일어날 것입니다. 이에 대해 자세히는 모르지만 그럼에도 우리는 컴퓨터를 문제 없이 잘 사용하고 있습니다.

만약 컴퓨터를 사용할 때, 컴퓨터의 동작 원리를 모두 알아야만 사용할 수 있다면 매우 힘들 것입니다. 따라서, 컴퓨터를 사용하기 위해 겉으로 드러난 최소한의 정보만 알아도 사용할 수 있어야 합니다.

이처럼 어떤 사물에서 사용에 반드시 필요한 부분만 겉으로 드러내고 사용자는 드러난 부분만 알면 되도록 하는 것이 바로 추상화입니다. 몰라도 되는 정보는 숨기고 꼭 알아야 하는 부분만 드러내는 것이죠.

프로그래밍에서는 프로그래머들이 특정 코드를 사용할 때, 필수적인 정보를 제외한 세부사항을 가리는 것을 추상화라고 합니다.

📋 추상화 사례

사실 우리는 지금까지 추상화를 잘 활용하고 있었습니다. 우리가 코딩을 하며 사용했던 변수와 함수 모두 추상화에 해당하기 때문이죠.

변수의 경우, 한번 값을 지정하면 그 값을 알지 못해도 이름만 가지고 활용할 수 있기 때문에 추상화에 해당됩니다.

hamburger = 5000
coke = 1000

price = hamburger + coke
print(price)

이처럼 햄버거와 콜라의 가격을 미리 변수에 저장해두면 연산을 할 때 굳이 그 값을 불러올 필요가 없이 변수명만 활용하면 됩니다. 이는 출력 함수를 사용할 때도 마찬가지이죠.

함수의 경우, 함수 이름과 파라미터, 그리고 함수의 역할만 간단히 알면 그 함수를 사용할 수 있기 때문에 추상화에 해당됩니다.

def say_hi(name):
    print(f"안녕! 나는 {name}이야!")

say_hi라는 함수의 이름, 함수의 파라미터 name, 그리고 인사 문구를 출력한다는 역할만 알면 언제든 say_hi 함수를 사용할 수 있습니다. 이 함수가 구체적으로 어떻게 돌아가는지는 알 필요 없이 말이죠.

클래스 또한 추상화에 해당합니다. 클래스 내부의 코드를 자세히 들여다 볼 필요없이 클래스에 관해 꼭 필요한 정보만 숙지하면 사용할 수 있기 대문이죠.

리스트 자료형을 생각해봅시다. 리스트 자료형은 Python 개발자들이 사전에 미리 정의한 클래스 중 하나였죠? 리스트에는 append라는 메소드도 있습니다. 우리는 두 개념 모두 그 기능을 잘 알고 있는데요. 여기까지만 알아도 리스트와 append 둘 다 사용이 가능합니다. 리스트의 내부 구조와 동작은 잘 몰라도 말이죠.

추상화를 잘 이해하고 클래스에 적용하면 좋은 점이 많습니다. 우선, 본인이 만든 클래스를 시간이 흐른 후에 다시 사용하려고 할 때 별다른 학습 없이도 쉽게 사용할 수 있습니다. 또, 개발자는 협업을 자주 한다 했었죠? 추상화가 잘 되어 있으면 남이 만들어낸 클래스나 본인이 만든 클래스 또한 무리 없이 잘 사용할 수 있습니다.

📋 추상화 활용: 이름 짓기

추상화를 잘 하려면 어떻게 해야 할까요? 첫 번째 방법은 바로 이름을 잘 짓는 것입니다. 이름을 잘 지어야 어떤 기능을 하는지 한번에 잘 파악할 수 있으니까요.

class SomeClass:
    calss_variable = 0.01
    
    def __init__(self, variable1, variable2):
        self.variable1 = variable1
        self.variable2 = variable2
        
    def method1(self, some_value):
        self.variable2 *= some_value
        
    def method2(self, some_value):
        if self.variable2 < some_value:
            print("잔액이 부족합니다")
        else:
            self.variable2 -= some_value:
            
    def method3(self):
        self.variable2 *= 1 + SomeClass.class_variable

위와 같은 코드는 나쁜 예시입니다. 변수, 함수, 클래스 이름에 명확한 의미가 담겨있지 않아 어디에 어떻게 쓰는 클래스인지 파악하기 어렵기 때문이죠.

class BankAccount:
    interest = 0.01
    
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
        
    def deposit(self, amount):
        self.balance *= amount
        
    def withdraw(self, amount):
        if self.balance < amount:
            print("잔액이 부족합니다")
        else:
            self.balance -= amount:
            
    def add_interest(self):
        self.balance *= 1 + BankAccount.interest

위와 같이 이름만 변경해도 위 코드가 어떨 때 사용되는지 바로 알 수 있습니다. 은행 계좌, 이자, 계좌 주인, 잔액 등의 이름만 봐도 은행 관련 업무를 한다는 걸 알 수 있죠. 입금, 출금, 이자 더하기 등의 메소드 이름을 보면 그 기능도 파악해볼 수 있습니다.

이렇게 이름을 잘 지어두면 위 코드에 대한 설명을 듣지 않아도 바로 사용이 가능합니다. 이름으로부터 의미를 유추해 볼 수 있기 때문이죠. 따라서, 클래스, 변수, 함수 이름 등을 정할 때는 그 의미가 잘 담기도록 지어야 합니다.

📋 추상화 활용: 문서화

사실 이름 짓기에는 한계가 있습니다. 이름 하나에 모든 의미를 담을 수 없기 때문이죠. 이를 극복하는 방법이 바로 문서화입니다.

우리는 문서화를 통해 코드 속에 각 변수와 메소드가 어떤 역할을 하는지 기록할 수 있습니다. Python에서는 docstring을 통해 문서화를 할 수 있는데요. docstring은 'documentation string'의 약어이며 우리말로는 '문서화 문자열'이라고 합니다.

docstring은 큰 따옴표 세 개로 표현할 수 있습니다. 보통 설명하고 싶은 클래스나 메소드 바로 아래에 적습니다.

"""
이것은 docstring입니다.

여러 줄을 사용해도 됩니다.
"""

위 BankAccount 클래스에 docstring을 추가해보겠습니다.

class BankAccount:
    """은행 계좌 클래스"""
    interest = 0.01
    
    def __init__(self, owner, balance):
        """인스턴스 변수: owner(문자열), balance(상수형)"""
        self.owner = owner
        self.balance = balance
        
    def deposit(self, amount):
        """잔액 인스턴스 변수 balance를 파라미터 amount만큼 늘려주는 메소드"""
        self.balance *= amount
        
    def withdraw(self, amount):
        """잔액 인스턴스 변수 balance를 파라미터 amount만큼 줄여주는 메소드"""
        if self.balance < amount:
            print("잔액이 부족합니다")
        else:
            self.balance -= amount:
            
    def add_interest(self):
        """잔액 인스턴스 변수 balance를 이자율만큼 늘려주는 메소드"""
        self.balance *= 1 + BankAccount.interest

이렇게 하면 한층 더 BankAccount를 이해하기 쉬워집니다. 문서화를 참고하면 이 클래스를 사용하는 누구나 사용법을 잘 알 수 있습니다.

📋 추상화 활용: 문서화 결과

문서화된 클래스를 사용할 때, 클래스 중간중간에 있는 docstring을 읽는 것보다 더 편한 방법이 있습니다. 바로 help(클래스 이름)을 활용하는 건데요.

help(BankAccount)

이렇게 하면 클래스 BankAccount에 있는 모든 docstring의 내용을 한번에 볼 수 있습니다.

help(list)

리스트는 Python에 미리 정의된 클래스였죠? 위 코드를 실행하면 list 클래스로 인스턴스를 생성하는 방법과 사용할 수 있는 메소드의 정보가 출력됩니다. Python 개발자들 또한 docstring을 활용했다는 것을 알 수 있죠.

📋 문서화 스타일

문서화에 정해진 규칙은 없지만 흔히 사용하는 포맷은 있습니다.

흔히 사용하는 3가지 포맷을 활용하여 유저를 위한 추천 영화를 찾는 find_suggestion_movies 메소드를 위한 docstring을 작성해보겠습니다.

def find_suggestion_movies(self, num_of_suggestions=5)

❗ Google docstring

"""유저에게 추천할 영화를 찾아준다
Parameters:
  num_of_suggestions (int): 추천하고 싶은 영화 수
    (기본값은 5)

Returns:
  list: 추천할 영화 주소가 담긴 리스트
"""

❗ reStructuredText(Python 공식 문서화 기준)

"""유저에게 추천할 영화를 찾아준다

:param num_of_suggestions: 추천하고 싶은 영화 수
  (기본값은 5)
:type num_of_suggestions: int
:returns: 추천할 영화 주소가 담긴 리스트
:rtype: list
"""

❗ NumPy/SciPy(통계/과학 분야 Python 라이브러리)

"""유저에게 추천할 영상을 찾아준다

Parameters
----------
num_of_suggestions: int
  추천하고 싶은 영상 수 (기본값은 5)
  
Returns
-------
list
  추천할 영상 주소가 담긴 리스트
"""

문서화에서 가장 중요한 것은 프로그램을 함께 만드는 동료들과 사전에 포맷에 대해 약속을 하고 잘 지키는 것입니다. 혼자서 만들더라도 일관성을 유지하면 나중에 프로그램을 수정할 때 편하므로 문서화를 권장합니다.

📋 타입 힌팅

Python동적 타입 언어입니다. 동적 타입 언어의 특징은 변수 선언 시, 자료형을 따로 기술할 필요가 없습니다.

그러나 이와 반대인 정적 타입 언어, 예를 들면 JAVA에서는 변수 선언 시, 자료형을 따로 기술해줘야 합니다.

a = 0
b = 1

def add(x, y):
    # some code
int a = 0;
int b = 1;

public int add(int x, int y) {
    // some code
}

위 코드가 Python, 아래 코드가 JAVA의 예입니다. JAVA 코드의 변수를 보면 int를 통해 정수형을 나타내는 부분이 있습니다. 이는 파라미터와 메소드 앞에도 마찬가지로 나타나 있습니다.

Python에서는 변수에 어떤 값을 넣느냐에 따라 자료형이 결정됩니다. 자료형이 그때그때 바뀔 수 있는 가변적 상태이기 때문에 동적이라고 하는 것이죠.

그냥 보기에는 자료형을 따로 적어줄 필요 없는 동적 타입 언어가 더 좋아보입니다. 하지만 단점이 있는데요. 자료형을 표시하지 않기 때문에 함수를 사용할 때 파라미터에 어떤 자료형의 값을 넣어야 할지 알 수가 없는 것이죠.

이를 극복하기 위해 Python 3.5부터 타입 힌팅(Type Hinting)이라는 기능을 추가했습니다. 타입 힌팅은 Python에서도 정적 타입 언어와 같이 타입을 표시할 수 있는 기능입니다.

BankAccount 클래스에 타입 힌팅을 적용시켜 보겠습니다.

class BankAccount:
    interest: float = 0.01
    
    def __init__(self, owner: str, balance: float) -> None:
        self.owner = owner
        self.balance = balance
        
    def deposit(self, amount: float) -> None:
        self.balance *= amount
        
    def withdraw(self, amount: float) -> None:
        if self.balance < amount:
            print("잔액이 부족합니다")
        else:
            self.balance -= amount:
            
    def add_interest(self) -> None:
        self.balance *= 1 + BankAccount.interest

타입 힌팅을 하려면 변수 이름 뒤에 콜론(:)을 쓰고 자료형을 적어주면 됩니다.

메소드의 파라미터도 마찬가지로 콜론(:)을 쓰고 자료형을 적어줍니다. self인스턴스 자신을 나타내기 때문에 따로 타입 힌팅을 해줄 필요가 없습니다.

메소드의 리턴값은 함수 정의 부분 뒤에 화살표를 쓰고 자료형을 적어줍니다. 리턴값이 없을 때None을, 나머지는 자료형에 맞게 적으면 되겠죠?

타입 힌팅을 적용시켜도 프로그램은 정상적으로 동작합니다.

타입 힌팅 상으로 balance의 자료형은 실수형인데요. 만약 이 파라미터에 문자열이 들어오면 어떻게 될까요? 사실 프로그램은 에러 메세지 없이 동작하긴 합니다.

그러나 이렇게 하면 deposit 메소드에서 문자열끼리 더하기 연산을 하여 엉뚱한 문자열 값을 출력하게 되면서 원래 의도와는 다른 방식으로 프로그램이 동작하게 되죠. 그리고 입력값에 마우스 커서를 올리면 팝업으로 경고 메시지가 뜨긴 합니다.

클래스를 올바르게 사용하려면 타입 힌팅대로 변수를 설정하고 메소드에 파라미터를 전달해야 합니다. 실행에는 직접적인 영향을 끼치진 않지만 개발자들로 하여금 어떤 값을 넣어야 하고 어떤 값이 반환되는지 쉽게 파악할 수 있도록 도움을 줍니다.

동적 타입 언어인 Python과 타입 힌팅이 어울리지 않다는 의견도 있지만 그럼에도 프로그래밍에 도움이 되는 기능이므로 알아두는 것이 좋습니다.


이번 시간에는 알 것만 알아도 사용할 수 있다는 편의를 제공하는 추상화에 대해 알아봤습니다. 추상화가 있기에 복잡한 이해 없이도 프로그래밍이 가능한데요.

다음 시간에는 객체 지향 프로그래밍의 네 가지 기둥 중 또 다른 하나인 캡슐화에 대해 알아보겠습니다.

* 이 자료는 CODEIT의 '객체 지향 프로그래밍' 강의를 기반으로 작성되었습니다.
profile
There's Only One Thing To Do: Learn All We Can

0개의 댓글