저번 시간에는 객체 지향 프로그래밍의 네 가지 기둥 중 첫 번째 기둥인 추상화에 대해 배웠습니다.
이번 시간에는 두 번째 기둥인 캡슐화에 대해 알아보겠습니다.
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 클래스에 캡슐화를 적용해야 합니다. 캡슐화에는 두 가지 정의가 있습니다.
- 객체의 일부 구현 내용에 대한 외부로부터의 직접적인 액세스를 차단하는 것
- 객체의 속성과 그것을 사용하는 행동을 하나로 묶는 것
캡슐화의 첫번째 정의부터 알아볼까요? 이 말을 쉽게 풀어쓰면 클래스 외부에서 클래스의 어떤 변수나 메소드에 직접 접근하는 것을 막는다는 걸 뜻합니다.
위 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 변수는 캡슐화가 적용되었기 때문에 외부에서 바로 접근할 수는 없습니다. 하지만 해당 변수를 활용하는 메소드를 정의하면 접근이 가능해집니다.
이렇게 접근할 수 있는 메소드를 만드는 것이 바로 캡슐화의 두 번째 정의에 해당됩니다. 두 번째 정의에서 '하나로 묶는 것'이라는 표현이 있었죠? 이 말은 '변수에 접근하는 통로를 메서드로 제한한다'는 것을 의미합니다.
get_age와 같이 변수의 값을 읽는 메소드를 getter 메소드라 하고 변수의 값을 설정하는 메소드를 setter 메소드라 합니다.
__age 변수와 달리 __resident_id 변수에 대해서는 getter 메소드와 setter 메소드를 만들 필요가 없습니다. resident_id는 주민번호를 가리키는 변수였죠? 주민번호는 함부로 읽거나 바꾸면 안되는 민감한 개인 정보이기 때문에 읽을 수도, 수정할 수도 없게끔 해야 합니다.
그런데 authenticate 메소드에서 __resident_id를 활용하고 있죠? 그럼 캡슐화의 두 번째 정의가 적용된 걸까요? 아닙니다. 왜냐하면 authenticate 메소드는 getter 메소드도, setter 메소드도 아니기 때문이죠.
이 메소드는 주민번호를 기준으로 본인이 맞는지, 아닌지를 확인하는 역할을 담당하고 있습니다. 이때, 주민번호를 몰라도 본인 인증은 가능하죠. 따라서, 이 메소드는 값을 읽거나 설정하는 것과는 아무런 관련이 없습니다.
이처럼 캡슐화가 적용된 변수에 대한 getter 메소드와 setter 메소드를 반드시 만들 필요는 없습니다. 모든 변수에 접근 가능하면 캡슐화의 첫 번째 정의가 의미 없게 되니까요.
지금까지 배운 내용을 정리해 보겠습니다. 먼저, 캡슐화는 클래스 밖에서 접근 못하게 할 변수와 메소드를 정합니다. 다음으로 변수나 메소드 이름 앞에 언더바 2개를 붙여 캡슐화를 적용합니다. 마지막으로 변수에 간접 접근할 수 있도록 getter/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의 '객체 지향 프로그래밍' 강의를 기반으로 작성되었습니다.