저번 시간에는 캡슐화의 정의와 적용 방법에 대해 함께 알아봤습니다.

이번 시간에는 Python에서 캡슐화를 적용할 때 유의해야 할 사항들에 관한 내용과 데코레이터를 활용한 캡슐화, 객체를 사용할 때 메소드를 최대한 활용해야 되는 이유 등에 대해 알아보겠습니다.

💊 Python 캡슐화의 비밀

Python에서 캡슐화를 하기 위해 변수나 메소드를 숨기려면 이름 앞에 언더바 2개를 붙인다고 배웠습니다. 이 표기에는 특별한 원리가 숨겨져 있는데요.

Citizen 클래스의 내부를 들여다 봅시다.

taki = Citizen("타키탸키", 26, "123456")
print(dir(taki))

dir라는 함수를 사용하면 인스턴스가 가지고 있는 모든 변수와 메소드를 볼 수 있습니다.

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

가장 앞에 있는 _Citizen__age와 _Citizen__resident_id가 보이시나요? 이 둘이 바로 우리가 언더바 두 개를 붙였던 변수 __age와 __resident_id입니다.

사실 변수나 메소드 이름 앞에 언더바 두 개를 쓰면, Python은 그 앞에 추가적으로 "_클래스 이름"을 덧붙여 이름을 바꿔버립니다. 이를 네임 맹글링(name mangling)이라 하는데요. 맹글링(mangling)이라는 단어는 '마구썰다', '엉망진창으로 만들다'라는 뜻을 가지고 있습니다. 여기서는 이름을 새로운 형태로 변환한다 정도로 해석할 수 있습니다.

그럼 이름이 바뀌었으니 클래스 밖에서 접근이 가능할까요?

print(taki._Citizen__age)
print(taki._Citizen__resident_id)
26
123456

놀랍게도 접근이 가능합니다.

원리는 이렇습니다. 변수나 메소드 앞에 언더바 두 개를 붙이면 이 변수는 네임 맹글링이 되어 새로운 이름을 갖게 됩니다. 이름이 바뀌었으니 원래 이름으로는 접근이 안 되는 것이죠.

따라서, 해당 변수나 메소드에 접근하고 싶으면 새로운 이름으로 접근하면 됩니다. 그런데 이렇게 하면 과연 캡슐화가 된 것이 맞을까요?

엄밀히 따지면, 아닙니다. 사실 Python에서는 언어 차원에서 캡슐화를 지원하지 않습니다. 캡슐화처럼 기능하긴 하지만 완전한 캡슐화는 아닌 거죠.

다른 객체 지향 언어인 JAVA에서는 private라는 키워드를 변수 이름 앞에 붙여 외부로부터 접근을 완벽히 차단합니다. 완전한 캡슐화를 지원한다는 것이죠.

그렇다고 Python이 캡슐화를 아예 무시한다는 것은 아닙니다. 방법이 조금 다를 뿐이죠. Python 개발자들은 캡슐화에 관한 독특한 문화를 가지고 있습니다.

Python 개발자들은 어떤 변수나 메소드에 함부로 접근하지 말라는 표시를 하는데요. 변수나 메소드 앞에 언더바 한 개를 적습니다. 이 표기의 뜻은 해당 변수나 메소드를 클래스 밖에서 직접 접근해서 사용하지 말라경고 표시입니다. 개발자들은 이를 약속처럼 생각하고 지킵니다.

사실 언더바 하나는 아무런 기능이 없습니다. 앞서 언더바 두 개는 네임 맹글링을 하여 변수 이름을 바꾸기라도 했지만 언더바 하나로는 아무 변화도 일으키지 않기 때문이죠.

그럼에도 언더바 하나를 사용하는 이유는 이 경고 표시 하나로 다른 개발자들이 서로 약속을 잘 지킬 것이라는 믿음이 있기 때문입니다.

물론 이 약속이 항상 지켜지진 않습니다. 그러나 그렇게 해서 코드가 망가지면 경고 표시를 무시한 개발자가 그 책임을 떠안게 되죠.

앞으로 우리는 Python에서 캡슐화를 적용하고자 할 때, 언더바 두 개가 아닌 언더바 하나를 사용할 것입니다. 그리고 언더바 한 개 변수에 대해서는 getter/setter 메소드를 작성하거나 다른 용도의 메소드를 추가해야 합니다.

하지만 누군가는 아무런 기능이 업는 언더바 한 개의 사용에 의문을 가질 수 있습니다. 차라리 실행을 막기라도 하는 언더바 두 개가 낫지 않겠냐고 말할 수도 있겠네요.

그러나 Python의 공식 가이드에 따르면 언더바 두 개는 혹시나 발생할 수 있는 이름 충돌을 위해 사용하는 것이라 나와있습니다. 그리고 캡슐화에 대해서는 언더바 하나를 권장하고 있죠.

되도록이면 가이드라인에 따라 코딩을 하는 것이 좋겠죠? 엄밀히 따지면 언더바 두 개 또한 완벽한 캡슐화는 아니니기도 하구요.

💊 데코레이터를 사용한 캡슐화

그럼 왜 Python에서는 언어 자체에서 캡슐화를 지원하지 않는 걸까요? 그 이유는 바로 Python의 문화 때문입니다.

Python의 문화에서는 캡슐화를 꼭 지켜야한다는 의식이 약한 편입니다. 따라서, 캡슐화를 해도 좋고 안 해도 좋다는 마인드이죠.

그런데 만약 캡슐화를 적용하지 않고 코드를 작성했는데 나중에 변수를 숨기고 싶어지면 어떻게 해야 할까요?

앞서 Citizen 클래스에 캡슐화를 적용하면서 getter / setter 메소드를 사용하여 코드를 수정했었는데요. 수정할 내용이 적어서 다행이었지 만약 고칠 부분이 많았다면 이 방법은 조금 번거로운 것 같습니다.

사실 좀 더 편리한 방법이 있습니다. Citizen 클래스에서 사용하던 코드를 굳이 수정하지 않아도 활용할 수 있는 방법이 말이죠. 바로 property라는 데코레이터 함수를 사용하는 것입니다.

@property
def age(self):
    print("나이를 리턴합니다.")
    return self._age
    
@age.setter
def age(self, value):
    print("나이를 설정합니다.")
    if value < 0:
        print("나이는 0보다 작을 수 없습니다. 기본 값 0으로 나이를 설정하겠습니다.")
        self._age = 0
    else:
        self._age = value

처음 보는 코드 같지만 자세히 들여다 보면 아까 작성했던 코드에서 살짝 바뀌었을 뿐입니다. 위 코드가 getter 메소드 역할을 하고 아래 코드가 setter 메소드 역할을 합니다.

메소드 위에 @property를 쓰면 그 메소드를 어떤 변수에 대한 getter 메소드로 만들 수 있습니다. 이 메소드는 _age 변수에 대한 getter 메소드입니다. 메소드 내용을 보니 get_age 메소드의 내용과 동일합니다.

그리고 @getter 메소드 이름.setter로 쓰면 어떤 변수에 대한 setter 메소드를 만들 수 있습니다. 메소드 내용을 보니 마찬가지로 set_age 메소드의 내용과 동일합니다.

이렇게 하면 코드를 복잡하게 수정할 필요 없이 데코레이터만 추가해주면 됩니다. 그럼 어떤 원리로 이렇게 되는 걸까요?

print(taki.age)
taki.age = 21
print(taki.age)

데코레이터를 쓰면 print 함수 속 taki.age 부분이 실행될 때 getter 메소드age 메소드자동 실행됩니다. 그런데 이 부분은 원래 age 변수를 가져오라는 의미를 가지고 있었죠?

하지만 데코레이터를 쓰면 이 구문의 의미가 변합니다. 같은 이름을 가진 age 메소드를 실행하라는 뜻으로 말이죠.

그리고 print 함수 아래 taki.age = 21에서는 setter 메소드 age자동 실행됩니다. 원래라면 age라는 변수에 21이라는 수를 대입하라는 뜻인데 말이죠. 하지만 데코레이터가 붙으면서 age라는 이름의 setter 메소드를 실행하라는 뜻으로 바뀐 것입니다. 이때, value 파라미터로는 21이라는 숫자가 넘겨집니다.

이 것이 바로 데코레이터의 힘인데요. property 데코레이터 함수가 있으면 변수의 값을 읽거나 설정하는 구문이 아예 다른 의미로 실행된다고 생각하면 됩니다.

헷갈리기 쉬운 개념을 정리해 봅시다. Citizen 클래스에 있는 인스턴스 변수_age입니다. age는 _age에 대한 getter 메소드의 이름이자 setter 메소드의 이름이기도 합니다.

property 데코레이터를 사용했을 때 좋은 점은 캡슐화 전에 사용하던 코드를 캡슐화 후에 수정할 필요가 없다는 것입니다.

만약 데코레이터 없이 getter/setter 메소드를 적용해야 한다면 저번 시간에 봤던 것처럼 변수를 가져와서 사용하던 코드를 일일히 getter 또는 setter 메소드로 바꾸는 작업을 해야 합니다. 아래와 같이 말이죠.

print(taki.get_age())
taki.set_age(21)
print(taki.get_age())

하지만 property 데코레이터를 사용하면 원래 변수 이름을 가진 getter, setter 메소드를 만들면 됩니다. 메소드 내에서는 새로운 인스턴스 변수를 다루면 되는데요. 보통은 원래 변수 이름 앞에 언더바 하나를 붙인 이름으로 만듭니다.

이렇게 하면 마치 age라는 변수가 있는 것처럼 age의 값을 읽을 때 age라는 getter 메소드가 실행되고 설정할 때도 마찬가지로 age라는 setter 메소드가 실행됩니다. 사실 메소드 내에는 _age라는 변수가 있는데 말이죠.

한 가지 더! 데코레이터 사용 후에는 이닛 메소드 내에서 self.set_age(age)self.age = age교체하면 됩니다. 기존 코드를 수정할 필요가 없으니까요.

💊 변수 직접 사용 최소화

이번에는 Citizen 클래스를 활용해서 술집 출입문 프로그램을 만들어보겠습니다. 이 프로그램은 손님이 음주가 가능한 나이인지 확인합니다.

if taki.age >= Citizen.driking_age:
    print(f"{taki.age}님은 음주 가능 나이입니다!")
    
if taki.age >= Citizen.driking_age:
    print("입장하셔도 됩니다!")
    
if taki.age >= Citizen.driking_age:
    print("어떤 술을 드시겠습니까?")

지금 코드에는 별다른 문제가 없어보입니다. 그런데 국가 정책이 바뀌어 출생 기준 나이를 0세에서 1세로 바꿨다고 합시다. 그럼 age 변수를 사용하는 모든 부분+1을 해야 합니다.

이때, age 변수 대신 able_to_drink 메소드를 사용하면 어떻게 될까요? 이렇게 하면 age 변수에 일일히 1을 더할 필요 없이 able_to_drink 내부 코드만 바꾸면 됩니다.

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

if taki.able_to_drink():
    print(f"{taki.age}님은 음주 가능 나이입니다!")

이렇게 사용 가능한 메소드가 있는데 굳이 age 변수를 직접 가져다 쓰니 어떤가요? 상황이 변했을 때 코드 수정이 번거로웠죠? 이 말은 곧, 코드를 유지보수 하기가 어렵다는 뜻입니다.

변수를 직접 가져다 쓰는 것은 문제 없지만 이미 해당 변수를 활용하여 원하는 기능을 수행하는 메소드가 있는데 변수를 직접 가져다 쓰는 것은 비효율적인 것 같습니다. 유지보수가 어렵기 때문이죠.

이 부분에 대해서는 클래스를 만드는 개발자사용하는 개발자 모두 신경을 써야합니다. 클래스를 만드는 개발자는 사용자를 위해 필요한 메소드를 잘 준비해두는 것이 좋습니다. 클래스를 사용하는 개발자 또한 변수를 직접 사용하기 전에 원하는 기능을 하는 메소드가 있는지 찾아보는 게 좋습니다.

이렇게 변수를 직접 사용하는 것을 최소화 할수록 유지보수가 쉬운 코드를 만들 수 있습니다.


이번 시간에는 Python에서의 캡슐화 문화와 데코레이터의 활용, 변수를 직접 사용하는 것을 최소화할 때의 이점에 대해 알아봤습니다.

다음 시간에는 객체 지향 프로그래밍의 네 가지 기둥 중 세 번째인 상속에 대해 배워보겠습니다.

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

0개의 댓글