저번 시간에는 객체 지향 프로그래밍의 네 가지 기둥 중 첫 번째 기둥인 추상화에 대해 배웠습니다.

이번 시간에는 두 번째 기둥인 캡슐화에 대해 알아보겠습니다.

🍬 캡슐화

class Citizen:
    """주민 클래스"""
    drinking_age = 19
    
    def __init__(self, name, age, resident_id):
        """이름, 나이, 주민등록번호"""
        self.name = name
        self.age = age
        self.resident_id = resident_id
    
    def authenticate(self, id_field):
        """본인이 맞는지 확인하는 메소드"""
        return self.resident_id == id_field
        
    def able_to_drink(self):
        """음주 가능 나이인지 확인하는 메소드"""
        return self.age >= Citizen.drinking_age
        
    def __str__(self):
        """주민 정보를 문자열로 리턴하는 메소드"""
        return f"{self.name}님은 {str(self.age)}살입니다."

taki = Citizen("타키탸키", 26, "123456")
fire = Citizen("파이리", 12, "654321")

위 클래스는 주민을 나타내기 위한 클래스입니다. Citizen 클래스는 주민의 이름과 나이, 주민번호를 인스턴스 변수로 가집니다.

메소드로는 주민번호를 이용해서 본인 인증을 하는 authenticate 메소드와 음주가 가능한 나이인지 알려주는 able_to_drink 메소드, 그리고 주민 정보를 문자열로 리턴하는 던더 str 메소드를 가지고 있습니다.

그런데 이 프로그램에는 몇 가지 문제점이 있는데요.

print(taki.resident_id)
taki.age = -26

주민의 resident_id를 출력하면 주민번호가 유출될 수 있고 age 변수의 값을 마음대로 지정할 수 있기 때문에 음수값이 들어갈 수도 있습니다(나이는 양수이죠?).

나이 값을 마음대로 정할 수 있다는 것은 또 다른 문제가 있습니다. 이 프로그램에는 음주가 가능한 나이인지를 확인하는 메소드가 있는데 12살 파이리 어린이의 나이를 바꾸면 음주 가능 나이로 판별이 되기도 합니다.

fire.age = 20
print(fire.able_to_drink())

이런 문제를 해결하려면 Citizen 클래스에 캡슐화를 적용해야 합니다. 캡슐화에는 두 가지 정의가 있습니다.

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

🍬 객체 내부 숨기기

캡슐화의 첫번째 정의부터 알아볼까요? 이 말을 쉽게 풀어쓰면 클래스 외부에서 클래스의 어떤 변수나 메소드에 직접 접근하는 것을 막는다는 걸 뜻합니다.

위 Citizen 클래스를 첫번째 정의에 맞춰 수정해보겠습니다. 방법은 간단한데요. 숨기고자 하는 변수 이름 언더바 두 개를 붙이면 됩니다.

class Citizen:
    """주민 클래스"""
    drinking_age = 19
    
    def __init__(self, name, age, resident_id):
        """이름, 나이, 주민등록번호"""
        self.name = name
        self.__age = age
        self.__resident_id = resident_id
    
    def authenticate(self, id_field):
        """본인이 맞는지 확인하는 메소드"""
        return self.__resident_id == id_field
        
    def able_to_drink(self):
        """음주 가능 나이인지 확인하는 메소드"""
        return self.__age >= Citizen.drinking_age
        
    def __str__(self):
        """주민 정보를 문자열로 리턴하는 메소드"""
        return f"{self.name}님은 {str(self.__age)}살입니다."

이제 다시 타키탸키의 주민번호를 출력해보겠습니다.

print(taki.__resident_id)

그럼 다음과 같은 에러 메시지가 나옵니다.

AttributeError: 'Citizen' object has no attribute '__resident_id'

이 에러 메시지에는 taki 인스턴스에는 __resident_id라는 변수가 없다고 나옵니다. 이는 타키탸키의 나이를 출력해도 마찬가지입니다.

이번에는 메소드 앞에 언더바 두 개를 붙여보겠습니다.

def __authenticate(self, id_field):
        """본인이 맞는지 확인하는 메소드"""
        return self.__resident_id == id_field

__authenticate 메소드를 호출해볼까요?

taki.__authenticate("123456")

이 또한 위와 같은 에러 메시지가 뜹니다. 이 말은 곧, __authenticate 메소드를 클래스 밖에서 호출할 수 없다는 뜻이죠.

이렇게 언더바 두 개를 사용하면 외부 접근 제한이 필요한 메소드와 변수를 숨길 수 있습니다.

🍬 언더바 두 개와 특수 메소드

캡슐화는 언더바 두 개를 통해 구현할 수 있다고 했는데요. 그런데 이 형태가 조금 익숙하지 않으신가요? 네, 맞습니다. 지난 시간에 배웠던 이닛 메소드(__init__)와 던더 str 메소드처럼 특수 메소드들 또한 언더바 두 개를 활용했습니다.

이 두 형식에는 어떤 차이가 있는 걸까요? 사실 특수 메소드는 따지고 보면 언더바가 앞 뒤로 두 개씩 있기 때문에 캡슐화와는 조금 다른 형식이라고 할 수 있습니다.

그 쓰임 또한 특수한 상황에 자동으로 호출되는 것에 있기 때문에 외부로부터 접근을 제한시켜 주는 캡슐화와는 다르죠.

정리하자면, 특수 메소드와 같이 언더바가 앞뒤로 두 개씩 있으면 일반 메소드와 동일하게 사용이 가능한 반면에 __authenticate 메소드처럼 앞에만 밑줄 두 개가 있으면 외부에서 접근이 불가능합니다.

마찬가지로 __authenticate__처럼 뒤에도 언더바 두 개를 추가하면 일반 메소드와 동일하게 사용이 가능합니다.

다만 특수 메소드의 경우 Python에 기본으로 내장되어 있는 것이기에 자동으로 호출이 되지만 단순히 메소드 앞뒤로 언더바 두 개를 붙인다고 해서 특수 메소드가 되지는 않습니다.

🍬 메소드를 통해 변수에 접근하기

이제 캡슐화가 적용된 __resident_id와 __age는 클래스 외부에서 값을 읽을 수도, 설정할 수도 없습니다. 문제는 만약 주민의 나이를 변경하고 싶어도 __age에 접근할 수가 없기 때문에 수정이 불가능하다는 것입니다.

이와 같은 상황을 해결하기 위해서는 변수에 접근할 수 있는 메소드를 따로 만들어야 합니다.

def get_age(self):
    return self.__age

이렇게 하면 Citizen 클래스 밖에서도 __age의 값을 읽을 수 있습니다. 이런 식으로 숨겨진 변수에 접근 가능한 메소드가 있으면 클래스 밖에서도 해당 변수에 바로 접근할 수 있습니다.

이번에는 값을 설정하는 메소드를 추가해보겠습니다.

def set_age(self, value):
    self.__age = value

이렇게 하면 이제 __age 변수는 클래스 밖에서 읽을 수도 있고 설정할 수도 있게 됩니다.

print(taki.get_age())

taki.set_age(12)
print(taki.get_age())
26
12

정리하자면, Citizen 클래스의 __age 변수는 캡슐화가 적용되었기 때문에 외부에서 바로 접근할 수는 없습니다. 하지만 해당 변수를 활용하는 메소드를 정의하면 접근이 가능해집니다.

이렇게 접근할 수 있는 메소드를 만드는 것이 바로 캡슐화의 두 번째 정의에 해당됩니다. 두 번째 정의에서 '하나로 묶는 것'이라는 표현이 있었죠? 이 말은 '변수에 접근하는 통로를 메서드로 제한한다'는 것을 의미합니다.

🍬 getter / setter 메소드

get_age와 같이 변수의 값을 읽는 메소드를 getter 메소드라 하고 변수의 값을 설정하는 메소드를 setter 메소드라 합니다.

__age 변수와 달리 __resident_id 변수에 대해서는 getter 메소드와 setter 메소드를 만들 필요가 없습니다. resident_id는 주민번호를 가리키는 변수였죠? 주민번호는 함부로 읽거나 바꾸면 안되는 민감한 개인 정보이기 때문에 읽을 수도, 수정할 수도 없게끔 해야 합니다.

그런데 authenticate 메소드에서 __resident_id를 활용하고 있죠? 그럼 캡슐화의 두 번째 정의가 적용된 걸까요? 아닙니다. 왜냐하면 authenticate 메소드는 getter 메소드도, setter 메소드도 아니기 때문이죠.

이 메소드는 주민번호를 기준으로 본인이 맞는지, 아닌지를 확인하는 역할을 담당하고 있습니다. 이때, 주민번호를 몰라도 본인 인증은 가능하죠. 따라서, 이 메소드는 값을 읽거나 설정하는 것과는 아무런 관련이 없습니다.

이처럼 캡슐화가 적용된 변수에 대한 getter 메소드와 setter 메소드를 반드시 만들 필요는 없습니다. 모든 변수에 접근 가능하면 캡슐화의 첫 번째 정의가 의미 없게 되니까요.

지금까지 배운 내용을 정리해 보겠습니다. 먼저, 캡슐화는 클래스 밖에서 접근 못하게 할 변수와 메소드를 정합니다. 다음으로 변수나 메소드 이름 앞에 언더바 2개를 붙여 캡슐화를 적용합니다. 마지막으로 변수에 간접 접근할 수 있도록 getter/setter 메소드를 추가합니다. 혹은 다른 용도의 메소드만 추가해줘도 되죠.

🍬 setter 메소드의 점검 코드

앞서 주민의 나이를 음수로 설정하는 문제가 있었습니다. 이를 해결하기 위해서는 set_age 메소드 자체에서 이럴 일이 없도록 막아야 합니다.

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

수정된 set_age 메소드입니다. if 조건문을 활용하여 음수값이 들어오면 경고 메시지를 출력하고 나이를 0으로 설정하도록 변경되었습니다.

그런데 이닛 메소드 내에서도 나이를 설정하는 self.age = age가 있습니다. 따라서, 이 부분을 set_age로 교체해야 합니다.

self.set_age(age)

이렇게 하면 주민 인스턴스를 생성할 때부터 나이에 음수가 들어가는 것을 막을 수 있습니다.

taki = Citizen("타키탸키", -10, "123456")

taki 인스턴스의 age 변수에 음수값을 넣어서 실행해보겠습니다.

나이는 0보다 작을 수 없습니다. 기본 값 0으로 나이를 설정하겠습니다.

그럼 의도했던 대로 경고 메시지가 출력됩니다. 나이가 0으로 설정되었는지 확인하기 위해 print함수를 호출해보겠습니다.

print(taki.get_age())
0

잘 출력되네요.

이번에는 set_age 메소드로 나이에 음수를 설정해보겠습니다.

taki.set_age(-10)
나이는 0보다 작을 수 없습니다. 기본 값 0으로 나이를 설정하겠습니다.

마찬가지로 경고 메시지가 잘 출력됩니다.

print(taki.get_age())
0

다시 나이를 불러오면 0으로 잘 설정된 것이 보입니다.

이와 같이 setter 메소드파라미터로 전달된 값이 적절한지 점검하는 코드를 넣는 것이 좋습니다. 이렇게 해야 객체의 속성이 엉뚱한 값을 갖는 일이 없을 테니까요.


이번 시간에는 객체 지향 프로그래밍의 네 개의 기둥 중 두 번째이자 민감한 정보를 외부로부터 숨길 수 있는 캡슐화에 대해 알아보았습니다.

캡슐화를 잘 활용하면 외부로부터 변수나 메소드가 함부로 읽히고 수정되는 일을 막을 수 있겠죠?

다음 시간에는 이번 시간에 이어 캡슐화에 대한 또 다른 내용을 함께 배워보겠습니다.

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

0개의 댓글