객체지향을 설명하는 3가지 원리 (principles)로 Encapsulation (캡슐화), Inheritance (상속), Polymorphism (다형성)을 얘기한다. 이번 글에서는 그 중에서 첫번째 원리인 Encapsulation에 대해서 살펴보도록 한다. 이 과정을 통해 인스턴스 변수는 객체의 상태를 나타내고, 객체의 메쏘드는 객체에 대한 인터페이스 (interface)에 해당한다는 것을 이해하기를 바란다.
항상 하는 말이지만, 객체는 변수와 함수를 묶어 놓은 것이다. 그러면 바로 떠오르는 질문은 "왜 묶은거지?"이다. 같이 답을 한번 찾아보도록 하자.
객체란 변수와 함수를 묶어 둔 것이라고 했으니 우선 변수와 함수를 제대로 이해하는 것이 먼저겠다. 앞의 글에서도 계속해서 말해왔던 부분이지만 객체에 들어가기 전에 한번만 더 정리해 보자.
먼저 함수이다.
○ 메쏘드와 메쏘드의 호출
우선, 우리가 일상적으로 보는 함수의 정의(definition)는 아래와 같은 구조를 갖는다.
여기서, 함수의 이름과 인자가 정의되는 부분 (#1)을 보통 함수의 시그너쳐(signature), 또는 헤드(head) 라고 부르고, 실제 수행되는 코드에 해당되는 #2 부분을 함수의 바디(body)라고 부른다. 이 함수를 호출(invocation, call)하고자 하는 경우에는 아래와 같이 함수의 이름이 하나의 명령어가 된다.
객체 안에서 정의된 함수를 보통은 메쏘드(method)라고 부른다. 만약에 앞의 함수 f( )를 객체의 메쏘드로 선언한다면 아래와 같은 형태를 가지게 된다. 함수 전체의 정의가 클래스의 정의 (#3) 안으로 옮겨진 형태가 된다. 물론, 파이썬에서 함수가 클래스의 메쏘드가 되기 위해서는, 첫번째 인자에 self 라는 이름으로 참조되는 객체의 참조자 변수가 들어가야 하는 차이가 있기는 하다.
지금의 클래스 정의를 우리 말로 옮겨 보자. 무슨 뜻인가?
세상에서 A 라고 불리는 것들은 f( ) 할 줄 안다. 즉, 어떤 A 한테 f( ) 하라고 요청(request) 하면 정해진 절차에 따라 행위가 이루어진다.
그래서 A 객체를 하나 생성(아래의 #4) 한 후에 실제 f( ) 하라고 요청(#5)해 보면 아래와 같은 응답(response)가 나타나는 것을 확인할 수 있다. 여기서, a.f( ) 는 "a 객체가 f( ) 하기를 요청한다"는 의미가 된다.
보통 요청 (request) 은 서비스 (service) 와 연관된 용어이다. 서비스를 제공하는 역할을 하는 무엇인가를 보통 서버 (server)라고 부르고, 그 서버에게 무엇인가를 요청하는 역할은 클라이언트(client)라고 부른다. 그래서, 이렇게 정리할 수 있겠다. 어떤 클라이언트가 어떤 서버에게 어떤 서비스를 요청한다. 이러한 그림을 우리의 문제와 비교해 보면, 클라이언트는 우리(we)가 되고, 서버는 a 객체가 되고, 우리가 서버에게 요청한 서비스는 f 가 되는 셈이다. 그런 의미에서 a 객체의 f( ) 함수를 호출한다라고 하지 않고 요청한다라고 하며, 요청하는 서비스의 이름인 f 를 서버에게 전달하는 일종의 메시지 (message)로 해석한다. 다시 정리해 본다면, 우리가 a 객체가 f 라는 기능을 해 주기를 바라고 있다. 그래서, a 에게 f 해 달라고 메시지를 보낸다. 메시지를 받은 a 는 해당하는 함수를 "주체성을 갖고" 실행한다 (물론, 여건에 따라서 실행하지 못하는 경우도 있겠다). 그런 의미를 다음의 그림에서 한번 찾아보기 바란다. 객체 메쏘드의 호출을 함수호출로 이해하는 경우는 [그림1]과 같은 이미지일 것이고, 서비스의 요청으로 이해하는 경우는 [그림2]와 같이 생각할 수 있다. 당연히, 정답은 [그림2]이다.
○ Interface와 Implementation
객체의 메쏘드 호출에 대해서 조금 다른 접근으로 생각해 보라는 얘기를 하고 있는데, 그런 의미의 확장으로 인터페이스와 구현 (Implementation) 이라는 의미에 대해서 생각해 보자. 세상에 존재하는 모든 전자제품들은 전원을 켜거나(On) 끄는(Off) 기능을 가지고 있다. 우리가 집에서 늘상 사용하는 텔레비전을 우리가 원할 때 켤 수 없거나 우리가 원할 때 끌 수 없다면 큰 문제가 생기겠다. 여기서, 텔레비전도 하나의 객체라고 생각해 보자. 그러면, 텔레비전을 켜는 것도 하나의 서비스 요청이 된다. 우리가 텔레비전에게 on 해 달라고 요청하는 것이다. 그래서, 모든 텔레비전들은 외부에서 그런 요청을 할 수 있게 전원 버튼을 제공하고 있다. 음성인식이 일반화된다면 음성을 통해 텔레비전에게 메시지를 전달할 수 도 있겠지만, 아직까지 그렇지는 못하다.
우리가 가지고 있는 텔레비전은 on 하는 기능이 있다. 그 기능을 호출하기 위해서 전원 버튼을 가지고 있다. 그래서 외부의 클라이언트들은 텔레비전이 켜지기를 원할 때 마다 텔레비전이 제공하는 on 버튼을 누른다. 그러면, (하드웨어적이든 소프트웨어적이든) 미리 만들어진 on 기능이 실행되어 텔레비전이 켜지게 되는 것이다. 여기서 on 버튼을 인터페이스 (interface)라고 부른다. 사람과 텔레비전 간에 의사 소통을 위해 텔레비전이 제공하는 있는, 외부로 공개되어 있는 일종의 장치이다. 텔레비전이 가지고는 있지만, 자신을 위해서가 아니고 오로지 외부의 남들을 위해서 제공하고 있는 장치이다. 즉, 텔레비전이 가지고는 있지만, 절대로 텔레비전의 것이라고 할 수 없는 장치인 것이다. 마치 사람과 텔레비전 사이에 on 버튼이 있는 것 같은 그림이 그려진다. 그 버튼을 통해 사람이 텔레비전에 어떤 명령 또는 의사를 보내고, 그 버튼을 통해 텔레비전이 외부에서 어떤 요청이 왔는지를 해석할 수 있는 것이다. 이런 장치를 일반적으로 인터페이스 (interface)라고 부른다. 요즘의 전자제품들은 기능도 좋아야 하고 고장도 안나야 하는데 가볍고 가격도 저렴해야 한다. 그런데 사용성(usability)까지 좋아야 한다. 사용성이라고 하면 사람들이 얼마나 쉽게 사용할 수 있는가를 나타내는 척도 쯤이라고 생각하자. 제품의 사용성은 그 제품이 제공하는 인터페이스에 의해 결정된다. 이를 사용자 인터페이스 (user interface, UI) 라고 부른다.
그런데 이러한 on 버튼은 왠만한 기계들 (전기를 동력으로 이용하는 모든 기계들)은 모두 가지고 있다. 텔레비전도 가지고 있지만, 집에 전기밥솥도 on 버튼이 있고, 컴퓨터에도 on 버튼이 있다. 그리고, 자동차에도 on 버튼이 있다. 물론, 조금 다른 모양의 인터페이스를 가지고 있는 경우도 있다. 자동차 열쇠를 끼우고 돌리는 형태 말이다. 모양은 조금 다르지만, 똑~같은 on 버튼이다. 하지만, 텔레비전이 on 되는 방식과 전기밥솥이 on되는 방식, 컴퓨터가 on 되는 방식, 그리고 자동차가 on 되는 방식은 모두 다르다. 즉, 인터페이스는 동일하지만, 실제로 on 버튼이 눌러졌을 때 구동되는 방식은 모두 다르다. 이와 같이 실제 만들어지는 구동방식을 구현 (implementaion)이라고 부른다. 우리가 공부하고 있는 함수의 경우와 비교해서 보면, 함수의 signature 가 인터페이스인 것이고, 그 함수의 body 가 implementation이 되는 셈이다.
아래의 예를 살펴보자. 아래에서 Television과 Car 클래스를 선언했고, 둘 모두 on 이라는 인터페이스를 갖도록 했다 (on 이라는 이름의 메쏘드가 있다는 뜻이다). 하지만, In[2]와 In[3]에서 보다시피 서로 다른 반응이 나온다. 즉, Television과 Car 모두 on이라는 요청에 반응하도록 만들어졌다. 하지만, 실제 on 되는 방식은 서로 다른 것을 보여주고 있다. "동일한 인터페이스에 서로 다른 구현" 인 셈이다.
그럼, 하나의 질문이 구현이 다르면 그 이름 (인터페이스)도 달라져야 하지 않을까? 하는 점이다. 즉, Televison이 켜지는 것을 on 이라고 했으니, Car 가 켜지는 것은 (텔레비전이 켜지는 것과 엄연히 다르니) 다른 이름으로 불러야 하지 않을까 하는 점이다. 그렇다면, 어떤 이름으로 부르는 것이 좋을까? 그럼 세상에 있는 모든 (우리의 관념상으로 ) on 하는 것들의 (우리의 관념상) on 기능을 모두 다른 이름으로 불러야 할까? 그렇다면 인간의 언어는 얼마나 복잡해 지고, 그에 따라 세상은 또 얼마나 복잡해 질까. 생각만 해도 끔찍하다.
형태가 똑같지는 않지만 관념상 그 의미가 비슷하면 똑같은 단어를 사용한다. 비행기가 나는 것도 "fly" 이고 잠자리가 나는 것도 "fly" 라고 하는 이유이다. 이와 같이 하나의 인터페이스에 여러가지 implementation 이 가능한 것을 다형성 (polymorhism)이라고 부르고 객체지향 언어의 아주 중요한 특징 중의 하나로 설명되고 있다. 이에 대해서는 다음에 상속을 얘기할 때 한번 더 얘기해 보도록 하자.
○ 메쏘드 실행의 조건 (Condition)
다시 "요청"(request) 으로 돌아가 보자. 요청의 특성상, 여건이 맞지 않으면, 서비스 요청을 거절할 수도 있겠다. 꼭 요청이 아니래도, 일반적으로, 어떤 기능의 실행은 그 기능이 실행되기 위한 조건이 만족되어야 한다. 예를 들어, 전원 플러그가 빠져 있는 상태에서, 텔레비전의 on 기능이 실행될 수는 없다. 즉, 텔레비전의 on 기능이 실행되기 위한 사전 조건 (중의 하나)으로 전원 플러그가 전원 inlet (벽에 붙어 있는 전원 소켓) 에 끼워져 있어야 한다.
또 하나의 예로, 은행 ATM 기에 가서 돈을 찾으려고 하는 경우를 보자. ATM기가 현금을 지급하는 기능은 언제 실행되나? 고객이 요청할 때. 요청이 있으면 항상 현금을 지급하나? 아니다. 계좌에 인출하고자 하는 금액보다 많은 돈이 예금되어 있는 경우에만.
즉, 모든 서비스는 요청이 있을 때 마다 항상 실행될 수 있는 것은 아니다. 일정한 어떤 조건(들)을 가지고 있는 경우가 대부분이다. 예를 들어, 위의 Car 클래스의 예를 다시 보자. 사용자가 on 버튼을 누른다. Car의 on( ) 함수가 호출된다. 항상 실행되나? 즉, 항상 자동차에 시동이 걸리나? 즉, (우리 문제의 경우) 화면에 'Car ON'이라는 문자열이 출력되나? 아니다. Car 에 gas가 충분히 있는 경우에만 시동이 걸리게 된다.
즉, 모든 Car 들은 on( )이라는 기능을 수행하는데 영향을 주는 "gas의 양" 이라는 특성을 가진다. 함수적 특성이 아니고 데이터적 특성이다. 따라서, 객체의 인스턴스 변수로 선언되어야 한다. 이를 아래와 같이 표현할 수 있겠다. 즉, gas의 양이 최소 10은 있어야 시동이 걸릴 수 있다는 의미를 if 문을 이용해서 표현하였다.
하지만, 일단 인스턴스 변수를 하나 추가하기로 했다면 추가적으로 정해주어야 할 요소들이 있다. 첫번째는 인스턴스 변수의 초기화 (initialization)이다. 즉, 객체가 만들어질 때, 각 객체의 인스턴스 변수 값을 얼마로 할 것인지에 대한 고려이다. 우리가 이미 너무 잘 알고 있다시핑, __init__( ) 함수를 이용해서 객체의 인스턴스 변수를 초기화할 수 있다. 초기값을 결정하는 방법은 2가지가 있다. 첫째는 디폴트(default) 값을 사용하는 경우이다. 아래의 코드를 참고해 보자. __init__( ) 함수에서 gas 값을 15로 초기화하였고, 이후에 on( )함수 호출에서 if 조건이 체크가 되고 참(True)이 되므로 이 Car는 결국 시동이 걸리게 된다.
하지만, 초기값을 보다 유연하게 주기 위해서는 다음과 같이 방식이 사용된다. 외부에서 값을 주는 경우에는 그 값으로, 외부에서 값을 주지 않는 경우에는 디폴트 값으로 초기화 하는 방식이다. 아래의 코드를 보자.
하지만, 상식적으로 Car가 움직이게 되면 gas를 사용하게 되고 그 양이 최소값보다 작아지게 되면, 그 다음에는 시동을 켤 수가 없게 된다. 즉, 한번 셋팅(setting)된 인스턴스 변수의 값은 시간의 흐름에 따라 변하게 되고, 그렇게 변한 값은 객체의 메쏘드 호출에 영향을 주게 된다. 즉,
인스턴스 변수의 값에 따라 메쏘드가 어떻게 실행되는지가 결정된다.
메쏘드가 실행되면 인스턴스 변수의 값에 영향을 준다.
정리해 보면, 메쏘드의 실행은 객체의 인스턴스 변수 값에 따라 결정된다. 여기서, 객체의 인스턴스의 현재 값을 객체의 현재 상태(state)라고 부른다. 즉, 어떤 메쏘드들은 현재 상태에서 실행될 수 있지만, 또 다른 어떤 메쏘드들은 현재 상태에서 실행될 수 없게 되는 것이다. 물론, 또 다른 어떤 메쏘드들은 객체가 어떤 상태에 있든 무관하게 실행이 되는 경우도 있음이다.
그리고, 메쏘드가 실행이 되면 인스턴스 변수의 값이 변경된다. 이렇게 변경된 인스턴스 변수값은 객체의 상태를 변하게 만들고, 이후의 메쏘드 호출에 영향을 주게 된다. 즉, 인스턴스 변수들의 값은 시시각각으로 변하게 된다. 아래에서 인스턴스 변수값의 변경에 대해 조금 더 살펴보도록 하자.
○ 인스턴스 변수의 set과 get
아래는 현재의 Car 클래스의 정의이다. 그리고 gas 값이 15로 초기화된 c1 객체가 있다.
이 c1 객체의 gas 값을 알고 싶으면 (참조하고 싶으면) 다음과 같이 하면 된다. 객체가 가지는 인스턴스 변수는 외부에서 객체이름.인스턴스이름 의 형태로 직접적인 참조가 가능하다.
위와 같은 문법을 이용해 Car 객체의 gas 값을 변경할 수도 있다. 아래의 예를 보자
전혀 어렵지 않다. 하지만, 이렇게 하면 안된다. 왜 그런가? 위의 코드만 보더라도, 모든 Car 객체들은 가스통 (연료 탱크)의 용량이 정해져 있어서 용량을 넘어서는 값으로 바뀌지는 못한다. 우리 학생 독자 여러분의 실감을 위해 인터넷으로 검색해 봤더니, 우리나라에서 만들어지고 있는 경차의 연료탱크 용량은 35리터 정도인데 비해, 대형 세단의 경우는 60 리터가 넘어가고, SUV 의 경우에는 80리터 가까이 되는 것으로 나타난다.
우리가 방금 막 만든 c1은 경차이든 대형세단이든 SUV 든 상관없이 연료(gas)의 양이 100리터가 될 수는 없는 것이다. 즉, 각 객체가 가지고 있는 인스턴스 변수란 객체의 어떤 특성을 표현하는 값이다 보니, 어떤 값들은 허용될 수 있는 값이지만 어떤 값들은 그렇지 못한다. 즉, 모든 변수들은 그 의미상 허용될 수 있는 값들의 범위라는 것이 존재하는 것이다.
하지만, 위의 In[13]에서와 같이 (그러한 로직을 전혀 모르는, 또는 전혀 개의치 않는) 외부에서 마음대로 변경할 수 있도록 하면 잘못된 값으로 설정될 수 있게 되는 것이다. 즉, 객체가 (처음에 만들때와는 달리) 예상하지 못하는 상태에 들어가게 되고, 그 상태에서 실행되는 메쏘드들에 영향을 미치게 되어 오류가 증폭되는 문제가 발생하게 된다. 내가 만든 세상이고, 내가 만든 프로그램이지만, 전혀 예측하지 못하는 상태로 움직이게 되는 것이다. 지금 같이 단순한 프로그램이야 문제가 없겠지만, 만약에 무인자동차를 움직이는 컨트롤러 프로그램이라면 승객의 생명과 직결되는 문제가 되어 버린다.
이와 같은 문제로 자바와 C++ 같은 다른 객체지향 언어인 경우에는 인스턴스 변수에 대해 외부에서의 접근을 제한한다. 즉, 위에서 봤던 파이썬의 이와 같은 방식으로는 인스턴스 값을 변경할 수 없다는 뜻이다. 그럼 어떻게 해야 할까? 단순하게 생각해서, 인스턴스 변수의 값의 변경을 "요청" 하는 방식으로 한다. "c1 에게 gas의 값을 100으로 바꿔달라"고 요청하는 것이다. 그럼 자신의 연료탱크의 용량에 비추어 요청된 100이란 값을 허용할지 말지를 c1 객체가 결정하는 것이다. 우리가 위에서 메쏘드의 호출이 있으면 객체는 자신의 상태에 비추어 그 호출을 받아들일지 거절할지를 결정한다고 했는데, 이와 똑 같은 로직인 셈이다.
파이썬 클래스를 정의할 때 만들어진 함수 중에서 몇몇은 특별한 의미를 가지는 함수들이다. 우리가 이미 봤듯이 생성자 함수인 (물론, 조금 다른 의미가 있지만) __init__( )함수를 포함해서 인스턴스 변수와 관련된 setter, getter 함수들이 그러하다. 인스턴스 변수 값의 변경을 요청하는 함수를 setter (또는, set 메쏘드) 라고 부록, 현재의 인스턴스 변수 값을 참조하기 위한 함수를 getter (또는, get 메쏘드)라고 부른다.
Car 클래스의 gas 인스턴스 변수에 대해 setter와 getter의 예를 보이면 다음과 같다.
위에서 set_gas( ) 메쏘드는 gas 인스턴스 변수의 setter 메쏘드이고, get_gas( ) 메쏘드는 gas 인스턴스 변수의 getter 메쏘드이다. 일반적으로 setter 메쏘드는 set 이란 이름으로 시작한다. 인스턴스 변수가 여러개일 수 있으니 set 다음에 인스턴스 변수의 이름을 같이 적는다. 예를 들어, setgas( )라고 이름을 짓기도 하고, setGas( )라고 짓기도 한다. 후자의 경우를 Camel(낙타) 노테이션 (표기법)이라고 부른다. 여러개의 단어를 모아서 새로운 이름을 짓는 경우에는 두번째, 세번째, ... 단어의 첫글자는 대문자를 쓴다. 첫번째 단어의 첫글자는 클래스의 경우에는 대문자로, 나머지는 소문자를 사용한다. 물론, 이렇게 안한다고 에러가 되는 것은 아니다. 오래된 전통이다. 그렇게 하면 가독성이 좋아지는 장점이 있다. 예를 들어 예전에 유명했던 인터넷 사이트의 이름이었던 아이러브스쿨이라면 ILoveSchool이라고 적거나 iLoveSchool이라고 적는다는 뜻이다. 글자의 스카이라인(skyline)이 낙타등을 닮은 것 처럼 보인다고 해서 Camel notation 이라고 부른다. 이와 별개로 우리가 예제에서 본 것 처럼 중간에 언더스코어( _ )를 사용하는 경우가 많다.
[잠시 확인하고 가실께요~]
클래스, 변수 또는 함수의 이름을 지을 때에는 몇가지 특별한 규칙이 있다. 우리 블로그 어디엔가 있을 텐데, 간단하게 소개하면, 이름은 알파벳과 숫자만을 사용해서 지어야 한다. 특수문자는 사용할 수 없다. 단, 언더 스코어( _ )만 사용할 수 있다. 언더스코어도 하나의 알파벳으로 사용할 수 있다는 뜻이다. 그래서 언더스코어는, 우리가 __init__( )에서 보다시피, 이름의 첫글자로도 사용될 수 있다.
setter, getter 얘기를 하다 잠시 삼천포로 빠졌는데 다시 원래 위치로 돌아와 보자. set_gas( ) 메쏘든 Car 객체에게 "gas 값을 바꿔주세요" 라고 하는 요청이 된다. 하지만, 이 자체 만으로는 문장이 완성되지 못한다. "gas 값을 바꿔 주세요" 무엇으로? "50으로". 즉, 바꿀 값을 인자값으로 넘겨주는 형태가 되어, 메쏘드의 정의는 def set_gas(self, x)의 형태가 되고, 함수 호출도 c1.set_gas(50)의 형태가 된다. getter 메쏘드의 경우에는 Car 객체에게 "현재의 gas 값을 알려주세요"라는 요청이 되어, 메쏘드 정의는 def get_gas(self)의 형태가 되고, 현재의 gas 값을 리턴(return)하게 된다. 거의 모든 setter와 getter가 이런 형태를 갖게 된다. 실제 실행된 In[19] 의 setter와 In[20]의 getter를 살펴보기 바란다.
다시 원래대로 가서, setter와 getter를 사용하는 이유가 인스턴스 변수가 가지는 값의 범위를 통제하기 위해서라고 했는데 이를 반영해서 위의 코드를 조금 고쳐보면...
In[26]에서 30인자 값은 __init__( )함수 호출될 때 y 매개변수 값이 되고, 그 값은 결국 self.cap 값으로 저장된다. 즉, 현재의 self.cap 값은 30이 된다. 참고로, self.cap은 Car 객체의 연료탱크 용량을 나타내는 변수로 사용되었다. 즉, In[26]에서 연료탱크의 크기가 30인 Car 객체를 하나 만들고, 이제부터 그 객체를 c1이라고 부르겠다는 의미다. In[27]에서 이 객체에게 gas 값을 100으로 바꾸라고 요청했다. 아마 c1 객체를 초대형 SUV나 버스 쯤으로 생각한 모양이다. (인터넷으로 버스의 연료탱크 용량을 찾아봤더니 300~400 정도라고 한다). 하지만, 경차인 c1은 그런 요청(가솔린을 100리터나 담으라고 하는 요청)을 받아들일 수는 없겠다. 그래서 'Gas: out of range'라는 거절 메시지를 출력한다. 당연히 현재값은 초기화할 때 셋팅된 15값으로 남아있다 ( In[28] 참고)
하지만 문제는 여전히 다음과 같은 코드가 작동한다는 것이다.
여전히 In[26]에서와 같이 인스턴스 변수에 대한 "직접적인" 접근이 가능하다. 이것을 파이썬에서는, 다른 객체지향 언어와 다르게, 시스템 차원에서 막아두지 않았다. 사용자들이 조금은 더 쉽게 객체를 다룰 수 있도록 하는 배려(?) 일 수는 있으나 잘못 사용할 경우 중대한 오류를 유발할 가능성이 존재한다.
사실 너무 타이트한 언어는 배우기 어렵고, 사용하기 어려울 수 있다. ( "어렵다"라고 단정짓기 어렵다 ㅠ) 그래서, 파이썬은 배우기 쉽고 사용하기 쉽도록 일종의 제약사항을 풀어놓은 것이다. 대신, 프로그래머가 "알아서 잘" 사용하기를 바라는 것이다. 내가 만든 객체이면 내가 잘 알고 있을테니 문제가 될 소지가 적겠지만 (물론, 만든지 오래되면 내가 만든건지 남이 만든건지도 헷갈리는 사태가 되기도 한다) 남이 만든 객체를 사용하는 경우에는 위와 같은 직접적인 접근은 좋지 않다. 그래서 객체를 만드는 모든 개발자들은 필요한 최소한의 setter와 getter를 만들어서 같이 제공하고 있는 것이다. 실제로, 표준 라이브러리의 객체들의 정보를 dir( )을 통해 살펴보면, 유독 set과 get으로 시작하는 메쏘드 이름들이 많음을 확인할 수 있다.
그나마 인스턴스 변수 이름 앞에 언더스코어를 붙여서 이 변수는 직접적인 접근을 자제해 주기 바란다는 일종의 경고를 표현하기도 한다. 아래의 코드를 보자. 위의 코드와 다른 점은 gas 변수 이름을 _gas 로 바꿔준 것 외에는 없다.
변수의 이름이 _gas 이므로 setter 메쏘드도 set과 _gas를 합친 set_gas( )가 되는 셈이 된다. 물론, getter도 그러하다.
똑같은 코드인데 굳이 이렇게 하는 이유가 있는가? 좀 혼동스럽긴 한데, 예를 들어, In[33]처럼 명령어를 적다가 보면, "아, 내가 지금 언더스코어로 시작하는 인스턴스 변수에 직접적인 접근을 하려고 하고 있구나. 아, 이러면 문제가 생길수도 있으니 setter 메쏘드를 사용해야 겠다" 하는 생각을 떠올리면 좋겠다는 정도의 의미로 해석하면 되겠다.
조금 강하게 직접적인 접근을 막기 위해서 더블 언더스코어( __ ) 라고 해서 언더스코어 2개를 붙이는 경우가 있는데 위의 경우와 다르기는 하지만 어차피 돌아갈 수 있는 방법이 있다. 결국, 프로그래머 개인의 생각에 달린 문제라고 생각된다.
잠깐 결론을 맺어 본다면, "데이터에도 로직이 있다. 그 로직을 정의한 사람의 로직에 맞게 데이터를 사용하려면 그 로직이 정의되어 있는 setter, getter 메쏘드를 이용해서 인스턴스 변수에 접근 (읽거나 쓰거나) 해야 한다" 정도로 이해하면 되겠다. 배우기 쉽고, 사용하기 쉽도록 만들었다고 하는데 오히려 설명하기는 더 어려워진 느낌적인 느낌 !! ㅠ
[참고] 더블 언더스코어 : 맹글링(mangling)
[참고] property 함수와 @property 데코레이터
○ 그래서 객체의 Encapsulation (캡슐화) 란?
위에서 객체의 인스턴스 변수와 메쏘드에 대해 살펴보았는데, 객체의 기본적인 의미를 데이터를 중심으로 보면, 아래와 같이 해석해 볼 수 있다.
객체는 데이터의 모음이다. 우리가 데이터를 다룬다고 할 때에는 한두개 데이터를 다루는 것이 아니라, 여러개의 데이터를 다룬다. 여러개의 데이터를 모으는 단위로 데이터 구조가 있고, 대표적으로 리스트, 튜플, 딕셔너리가 있음은 우리가 너무 잘 알고 있다. 이와는 조금 다르게 "여러개의 서로 연관된 데이터" 가 있는 경우에 이 데이터들을 묶을 수 있는 수단이 바로 객체가 된다. 객체 하나에 묶인 여러개의 서로 연관된 데이터가 결국 인스턴스 변수가 되는 것이다.
객체도 결국은 데이터의 묶음이라고 했는데, 그 데이터들을 다룰려고 하면 그 데이터들을 쉽게 다룰 수 있도록 그 데이터를 다루데 필요한 메쏘드들도 함께 제공되어야 한다. 즉, 그렇게 제공된 메쏘드들을 통해서 데이터를 다루는 것이다. 내 맘대로 다루다 보면, 그 데이터 내부에 있는 본연의 로직을 깨게 되어 결국은 오류를 낳게 된다.
즉, 객체에서 제공된 메쏘드 (방법)을 통해서 데이터 (세상)를 다루는 것이다. 다르게 얘기하면, 그 데이터를 다룰 수 있는 방법은 함께 제공된 메쏘드를 통하는 것 이외에는 방법이 없다는 것이다. 제공된 메쏘드를 통해서만 데이터를 다루게 함으로써 데이터를, 데이터에 숨겨진 본연의 의미와 로직을, 보존할 수 있게 되는 것이다.
결론적으로, 이와 같이 메쏘드라는 캡슐 안에 데이터를 넣어둠으로써 외부로 부터의 임의적인 접근을 제한함으로써 데이터의 일관성을 보장할 수 있게 된다. 이것이 객체의 첫번째 특징인 Encapsulation의 의의이다.
'파이썬 (python)' 카테고리의 다른 글
[Python 35] 오류 메시지를 통해 본 파이썬 (2) | 2020.07.30 |
---|---|
[Python 34] 파이썬의 matplotlib와 예제 (0) | 2020.07.30 |
[Python 31] 코딩, 이것만 알자 (1) | 2020.07.18 |
[python 25] 파이썬 1부, 알고리즘 편을 끝내며 (0) | 2020.05.07 |
[python 24] 알고리즘 연습 - 4 : 두번째로 큰 값 찾기 (0) | 2020.04.25 |