Python Property 활용

렁호·2023년 5월 27일
0
post-thumbnail

0. 개요

python에는 우리 나라 1등 언어 java와는 다르게 접근 제한자의 기능이 구현되어 있지 않다. 그렇다면 python에서의 객체는 정보를 은닉 할 수 없다는 뜻일까 ?
그렇지 않다. @property 데코레이터를 통해 특정 속성에 직접 접근을 막음으로서 설계된 코드의 동작을 보장하고 정보를 은닉할 수 있다. (물론 접근이 '불가능' 하다는 것은 아니다.)

1. @property 사용하기

python에서의 접근 제한

앞서 언급했듯이 python에서는 속성에 대한 접근 제한 기능이 직접 구현되어 있지 않다. 객체 지향 프로그래밍에 필수적인 기능인 만큼 직접 구현되어 있지 않을 뿐 python에서는 관례적으로 변수명을 통해 접근 제한에 대해 명시하기를 권장한다.

class Person:
    def __init__(self, security_num):
        self._security_num = security_num

위와 같이 변수명에 언더바를 하나 붙여줌으로서 python 개발자들은 이 속성에 직접 접근하면 안된다는 것으로 인지할 수 있다.

그러나 이 방식은 서로간의 의식적인 약속일 뿐 시스템적으로 데이터 보호 기능을 전혀 하지 못한다.

윤정호 = Person('950303-1')
print(윤정호._security_num)

>> 950303-1

간혹 더블 언더바(aka.던더바)가 priviate 접근 제한이라고 주장하는 사람들이 있는데 결론적으론 접근이 제한된 것처럼 보이지만 이는 사실 'name mangling' 이라는 이름 뭉개기를 시전한 것이다. 객체의 네임스페이스에 이름을 저장할 때 사용자가 저장한 값이 아닌 '_<클래스명><변수명>'의 이름으로 저장하여 속이는 것 뿐이다.

그리고 이 방법은 후에 기능을 확장하고 외부와 연결함에 있어 큰 방해요소가 될 수 있음으로 나는 개인적으로 비추천 하는 방법이다.

getter, setter

변수명에 언더바를 붙임으로서 접근 조심을 주장하는 방식으로는 우리가 기대하는 안정성을 보장 받기 힘들다. 하지만 걱정하지 않아도 된다. 다행히도 python에는 접근제한자는 없어도 getter, setter는 구현되어 있으니 말이다.

python에서는 @property 데코레이터를 통해 getter, setter 메소드의 동작을 구현함으로서 속성에 대한 외부에서의 접근을 제어 할 수 있다.

class Person:
    def __init__(self, security_num):
        self._security_num = security_num
    
    @property
    def security_num(self):
        return self._security_num
        
    @security_num.setter
    def security_num(self, value):
    	self._security_num = value
 
윤정호 = Person('950303-1')
print(윤정호.security_num)
윤정호.security_num = '950303-2'
print(윤정호.security_num)
>>950303-1
>>950303-2

이제 여기서 getter와 setter 메소드에 대해 필요한 로직을 추가하여 외부에서의 접근을 엄격하게 제어 할 수 있다.

예를 들어 주민번호를 반환할 때 뒷자리를 마스킹 하고 주민번호 자리수를 체크하는 로직을 추가할 수 있다.

    def _mask_security_num(self):
        assert '-' in self._security_num
        birth, _ = self._security_num.split('-')
        return birth + '-' + '*' * 7

    @property
    def security_num(self):
        return self._mask_security_num()
        
    @security_num.setter
    def security_num(self, value):
        assert len(value) == 14, '주민번호는 13자리임용'
        self._security_num = value

윤정호 = Person('950303-1234567')
print(윤정호.security_num)
윤정호.security_num = '950303-123456'
print(윤정호.security_num)
>> 950303-*******
Traceback (most recent call last):
AssertionError: 주민번호는 13자리임용

응용

@property의 동작원리는 객체의 딕셔너리에 메소드 명을 추가하여 마치 변수에 접근하는 것과 같이 동작하게 하는 것이다.

이 원리를 잘 사용하면 동적으로 속성을 관리 할 수 있다.

계좌를 추상화하여 클래스를 만들고 잔고를 입출금 하는 기능을 개발했다고 생각해보자.

주의 ! 응용 예시만을 위한 코드임으로 효율성이 떨어질 수 있는 코드입니다.

class 은행계좌:
    def __init__(self, 계좌번호: str):
        self.계좌번호 = 계좌번호
        self.잔고 = 0

    def __repr__(self):
        return f'{self.계좌번호} 계좌 | 잔고: {self.잔고}원'
            
def 입출금(계좌: 은행계좌, 금액: int):
	현재잔고 = 계좌.잔고
    계좌.잔고 = 현재잔고 + 금액 

신한계좌 = 은행계좌('110417071129')

입출금(신한계좌, 10000)
print(신한계좌)
입출금(신한계좌, 15000)
입출금(신한계좌, -30000)
print(신한계좌)
>> 110417071129 계좌 | 잔고: 10000원
>> 110417071129 계좌 | 잔고: -5000원

에러는 발생하지 않지만 계좌에 돈이 없는데도 출금이 되는 이상한 상황이 발생한다.
이때 개발자는 입출금 함수의 금액 파라미터와 현재 잔고를 비교하여 가능한 출금에 대해서만 동작하도록 코들르 추가하여야 할 것이다.

def 입출금(계좌: 은행계좌, 금액: int):
	현재잔고 = 계좌.잔고
    if 금액 < 0 and 현재잔고 < abs(금액):
        raise ValueError
    계좌.잔고 = 현재잔고 + 금액 

이제 어느정도 완성은 된듯하다.
그런데 여기서 비즈니스 요구 사항으로 입출금 내역을 항상 변수에 저장하고 DB에 로그를 남기는 기능까지 추가 된다고 생각해보자.

지금은 외부에서 의존하고 있는게 입출금 함수 하나이지만 여러곳에더 비슷한 동작이 많이 발생한다고 생각해보자, 이자, 자동이체, 자동출금 등에서 코드를 모드 수정해줘야 되는 불상사가 생길 수도 있다.

이때 잔고 property를 생성하고 잔고에 대한 변경 사항에 대해서 객체 내부에서 처리하도록 한다면 계좌 객체의 잔고 속성에 의존하고 있는 모든 코드를 수정할 필요 없이 클래스의 property 메소드만을 수정함으로서 코드의 확장성을 챙기고 유지보수 비용을 줄일 수 있다.

from collections import defaultdict
from datetime import datetime

class 은행입출금내역DB:
    def __init__(self):
        self.로그 = defaultdict(list)

class 은행계좌:
    def __init__(self, 계좌번호: str, 은행DB: 은행입출금내역DB):
        self.계좌번호 = 계좌번호
        self.은행입출금내역DB = 은행DB
        self.입금금액 = 0
        self.출금금액 = 0
    
    def __repr__(self):
        return f'{self.계좌번호} 계좌 | 잔고: {self.잔고}원'

    @property
    def 잔고(self):
        return self.입금금액 - self.출금금액

    @잔고.setter
    def 잔고(self, 요청금액: int):
        assert type(요청금액) is int
        assert 요청금액 != 0, '불가능한 동작입니다.'

        if self.잔고 + 요청금액 < 0:
            raise ValueError('잔고가 부족 합니다.')
        if 요청금액 > 0:
            self.입금금액 += 요청금액
            self.은행입출금내역DB.로그['입금'].append(f'{datetime.now()}|{요청금액} 입금')
        else:
            self.출금금액 += abs(요청금액)
            self.은행입출금내역DB.로그['출금'].append(f'{datetime.now()}|{요청금액} 출금')
            
def 입출금(계좌: 은행계좌, 금액: int):
    계좌.잔고 = 금액 

신한DB = 은행입출금내역DB()
신한계좌 = 은행계좌('110417071129', 신한DB)

입출금(신한계좌, 10000)
입출금(신한계좌, 20000)
입출금(신한계좌, 30000)
입출금(신한계좌, -30000)
입출금(신한계좌, -20000)
print(신한계좌)
print(신한계좌.은행입출금내역DB.로그['출금'])
입출금(신한계좌, -10001)  # 현재 잔고 초과 출금
>> 110417071129 계좌 | 잔고: 10000원
>> ['2023-05-27 17:15:15.282696|-30000 출금', '2023-05-27 17:15:15.282697|-20000 출금']
>> ValueError: 잔고가 부족 합니다.

2. 결론

@property는 단순히 priviate한 변수에 접근할 수 있는 인터페이스를 제공하는 것 이상으로 사용하기에 따라 잠재력이 풍부한 기능이다.

지금은 @property를 통해 객체의 속성을 보호하고 정보를 은닉하였지만 이 밖에도 디스크립터나 메타클래스를 통한 상속을 통해 비슷한 동작을 구현 할 수 있다.

python이 동적 타입 언어이고 접근 제한자도 없어서 타언어에 비해 안정성이 떨어진다고 생각하기 쉽다. 실제로 완전히 틀린말은 아니라고 나도 생각한다. 그러나 python도 pythonic 하게 잘 설계하고 내부 기능을 적재적소에 사용한다면 간결하면서도 안정적인 소프트웨어를 만들 수 있는 잠재력을 가졌다.

그니깐 Python 미워하지말아횽

profile
식욕, 수면욕, 성욕 만땅

0개의 댓글