인간의 두뇌에는 두가지 능력이 있다.

하나는 살아남기 위해서 사실을 객관적으로 기억하고 바라볼 수 있는 능력. 또 하나는 살아남기 위해서 자기의 생각과 행동을 스스로 합리화 하여 살아남을 가치가 있음을 스스로에게 납득시킬 수 있는 능력이다.

인간은 살아 남기 위해서 객관적인 세계로부터의 정보를 정확하게 해석하고 판단하고 기억하는 능력을 발달시켜 왔다. 이것이 인간이 과학을 만들어내고 세계를 향한 지적 탐구를 할 수 있게 된 배경이다. 이 능력은 순수한 지적 탐구로부터 시작하여 새로운 사실을 발견하고, 이를 증명할 수 있는 방법을 고안해 내고, 실험을 통해 검증해 내는 일련의 과정들을 거치면서 객관적인 지식들을 만들어 낸다. 이러한 과정들을 지켜보면 인간은 매우 합리적인 동물이라고 생각할 수 있다.

인간이 생존을 위해 기억하고 쌓아온 지식들을 생각해 보면 인간의 합리성은 당연한 것으로 보인다. 인간이 채집을 하고 농사를 짓고 가축을 기르면서 인류 스스로의 생존을 위해 기억해야 할 일들은 무수하게 많다. 그리고 그 기억들이 조금이라도 불합리하다면 생존은 매우 크게 위태로워진다. 

제레미 다이아몬드는 그의 저서 "총, 균, 쇠"에서 이런 일화를 전하고 있다. 파푸아 뉴기니 원주민들과 함께 국경을 넘어가려고 기다리던 때에 에피소드로 기억한다. 국경을 넘어가는데 시간이 지체될 것이 예상되자 원주민들은 먹을 것을 구해 오겠다고 하면서 주변을 뒤지기 시작했다. 그리고 한동안 시간이 흐른 후 원주민들은 두 손으로 꼽을 수 있을 정도로 많은 종류의 버섯을 채취해 온 후 이를 요리하기 시작했다. 다이아몬드는 그 모습을 보고 혹시라도 버섯에 독이 있을지 모른다고 생각하여 먹지 않겠다고 말했다. 그러자 원주민은 화를 내면서 "버섯에 독이 있는지 모르는지를 모르는 바보 같은 인간들은 미국인 밖에 없다."라고 말하고는 먹을 수 있는 버섯의 종류에 대해서 이야기 하기 시작했는데 총 27가지 식용 버섯의 종류와 모양, 그리고 어디에서 주로 채집할 수 있는지 등을 설명해 주었다.

또 같은 책에서 다른 에피소드가 나온다. 농경이 시작될 무렵, 그러니까 일반적으로 채집을 주로 하면서 이제 막 정착이 시작될 무렵으로 추정되는 집단의 주거지가 발굴되어 조사를 한 내용이 있었다. 이 주거지에서는 야생 곡물들의 종자를 가져와 심어 본 흔적들, 즉 농경을 할 수 있는 가능성을 찾아본 흔적들이 있었는데, 그 주거지에서 발견된 곡물의 종류만 100가지 정도가 되었다고 한다.(기억을 더듬어 적느라 정확한 숫자인지는 잘 모르겠다.)

어쨌든 인간은 이렇게 생존을 위해서 다양한 방법으로 자연을 시험하는 법을 일찍이 터득하고 있었다. 당연히 이들은 실험해 본 대상들의 특성들을 열심히 조사했을 것이다. 씨앗의 크기는 충분한지, 심어진 양 대비 산출량은 적절한지, 식용으로 사용하기까지 필요한 작업들은 어느 정도인지, 추수에는 어려움이 없는지, 그리고 적절히 저장하여 두었다가 다시 심어도 발아에 문제가 없는지 등을 조사했다. 이런 방법은 도구나 대상이 다른 점을 제외하곤 현대의 과학자들이 하는 일이나 크게 차이가 없어 보인다.

이런 관점에서 보면 인간은 당연히 합리적인 이성이 모든 정신을 지배하는 존재여야 할 것처럼 보인다.


하지만 안타깝게도 인간은 그렇지 않다. 인간은 그와는 정 반대의 특성을 가지고 있다. 그리고 이것 역시 생존을 위해서는 꼭 필요한 것이기 때문에 정말로 아이러니한 특성이 아닐 수 없다. 인간은 매우 이성적인 사고를 발달 시킨 것과 같이, 그리고 그 사고가 생존을 위해 꼭 필요했던 것과 같이 어떤 본능 하나를 생존을 위해 키워 나갔다. 그것이 바로 "나는 꼭 존재해야 만 한다"는 비 이성적인 전제, 즉 생존 본능이다.

생존 본능이 어떻게 발달되었는지는 이성적으로 추적하기는 힘들다. 다만 이것은 다른 동물들에게도 매우 강하게 발현되는 것이기 때문에 그 본류를 찾아보기에는 어렵지 않다. 그리고 인간처럼 일반적으로는 생존에 그다지 적합하지 않은 여러 조건들(유아기가 너무 길다든지, 체력이 다른 동물들보다 약하고, 강력한 무기가 될만한 신체 조건이 갖춰져 있지 않다든지 하는 것 들)을 생각해 보면 다른 동물들에 비해 이 생존 본능이 더욱 강하지 않으면 안 될 것이라고 판단된다. 다이아몬드의 책에 다시 넘어가보면 떠돌이 채집 생활을 하는 종족들은 적절하지 않은 시기에 아이를 낳게 되었을 때, 즉 이미 낳은 아이가 아직 어려서 혼자 걷지 못하는 시기에 다음번 아이가 태어났을 경우 어쩔 수 없이 살해해야 하는 경우가 있었다고 한다.

또한 다른 동물들과 달리 유독 자기 생존을 위해서 지금 당장은 필요하지 않고, 아무리 생각을 해봐도 평생 스스로에게 도움을 줄 것 같지 않을 정도의 재산이나 먹을 거리를 쌓아두려고 노력하는 것을 보면 생존 본능에 있어서는 인간이 단연 모든 동물 중에서 으뜸이 아닐까 생각한다.


인간이 왜 비합리적이 되는가? 하는 질문에 대한 답은 매우 합리적으로 사고 하는 부분과 "나는 꼭 존재해야 만 한다"는 본능이 만나는 지점에 있다. 세계를 객관적으로 봐야 하는 두뇌의 대부분은 매우 이성적이다. 세계는 내가 원한다고 원하는 대로 되는 것이 아니다. 사실이라는 것은 내가 부정한다고 해서 거짓으로 바뀌는 성질의 것이 아니다. 따라서 사실은 온전히 사실로만 받아 들여져야 한다. 그리고 이것이 긍정적인 생존 본능을 자극하는 경우, 즉 세계를 객관적으로 바라보고 오직 사실만을 인정하고 받아들이는 것이 나의 생존에 도움이 되는 경우에 인간은 매우 이성적이고 합리적인 인간이 된다.

예를 들어 내가 응용 물리학자라고 하자. 그러면 내가 알고 있는 물리적 지식이 실제 어떤 장치로 만들어 질 수 있어야 한다. 이 장치를 만들어 내는 과정에는 당연히 시간과 열정을 투자해야 한다. 하지만 만일 내가 알고 있는 객관적인 사실, 즉 물리학적인 지식이 잘못되었다면 나는 어떠한 노력을 들여도 성공하지 못할 것이다. 만약 내가 개인적인 사고 편향을 가지고 있어서 상대성 이론은 받아 들여도 양자 역학은 죽어도 못받아 들이겠다면(아인슈타인처럼) 내가 양자 역학에 기초한 장치를 만들어 내는 것은 불가능한 일이다. 즉 정말로 객관적인 사실을 그대로 받아 들이지 않으면 응용 물리학자로서의 내 삶은 매우 고달퍼질 것이다. 이 예에서의 인간의 합리성과 생존 본능은 그대로 이성적인 상태로 유지될 수 있다.


하지만 인간의 합리성과 생존 본능의 결합은 항상 이런 식으로 이루어지지 않는다. 인간의 합리성은 보통 충분히 성숙된 나이에 자리를 잡는다. 인간이 세상을 충분히 알고 이를 객관적으로 분석할 수 있는 시기가 될 때까지 두뇌는 이성적인 사고에 지배 받기 보다는 생존 본능에 더 크게 지배를 받는다. 이 시기에 만약 생존 본능을 크게 자극 받는 일들이 벌어진다면 어떻게 될까?

예를 들어 어린 시절 학대를 받았다든지, 살고 있는 사회가 비 이성적인 행위나 삶을 강요한다든지, 불건전한 사상이나 종교에 물든 어른들 틈에서 자란다든지 하는 상황이 되면 이성적인 사고의 영역은 제대로 발달하지 못하고 그 자리를 생존을 위한 본능이 자리잡게 된다. 하지만 이 부분은 근본적으로 합리의 영역이기 때문에 역시 합리적인 형태로 나타나게 되는데 이것을 보통 생존을 위한 합리성 영역, 즉 실제로는 합리적이지 않지만 생존을 위해서는 이것이 꼭 필요하고, 자신이 생존할 가치가 있음을 지속적으로 찾아 내려는 합리적인 노력의 영역으로 자리 잡게 된다. 이것이 자기 합리화이다.

이 자기 합리화 과정을 좀 더 고찰해보면 다음과 같다. 우선 생존을 위협하는 주변 인자들이 있다. 안타까운 가정이지만 생존을 위협받는 사람을 생각해 보자. 이것은 이 상황을 겪고 있는 사람에게는 지극히 객관적인 사실이다. 그리고 그 머리 속에는 지속적으로 "내가 생존할 만한 가치가 있는 존재"라는 생존 본능의 목소리가 들려 온다. 결국 생존 본능은 합리성 영역과 만나게 된다. 일반적인 경우라면 이 합리성 영역은 외부 세계의 객관적인 사실에 대한 해석을 하는 영역이 되어야 하지만 이 사람은 자꾸 생존 본능을 자극 받고 있기 때문에 외부 세계에 대한 해석을 본능적 해석으로 바꾸게 된다. 이 해석은 일반적인 사람의 해석과 달라지기 때문에 매우 비 상식적인 형태로 바뀐다. 어떤 사람은 "자신이 학대 받아 당연한 사람"이라고 생각하게 되거나, "모든 사람들이 학대 받는다"고 생각할 수 있다. 양상은 다양하지만 그런 생각이 객관적이고 이성적인 것은 아니라는 것은 확실하다.

그런데 문제는 이러한 자기 합리화 과정이 비단 이런 극단적인 상황에서만 발생하는 것이 아니라는데 있다. 아직도 수많은 사람들이 비 이성적인 사상이나 종교의 영향 아래 있다. 이들은 이미 이 사상이나 종교 아래에서 자기 생존을 합리화하는 과정을 거쳤기 때문에 스스로 비 이성적인 행동을 한다고 생각하지 않는다. 하지만 종교에 의한 전쟁은 수세기동안 계속되어온 문제이다.


그러면 이 자기 합리화 문제가 우리 사회에는 없을까? 이 문제에 답하기 위해서는 어떤 생각이 자기 합리화된 생각인지 아닌지를 어떤 기준으로 판단할 수 있을까를 먼저 생각해 봐야 한다. 이것은 지극히 간단한 일이다. 나의 행동이나 생각이 다른 사람의 생존을 위협해서는 안된다는 것이다. 누구나 객관적으로 합리성을 발휘하기 위해서는 서로의 생존을 위협해서는 안된다. 다른 사람들의 생존을 위협하기 시작하는 순간 위에서 이야기 했던 생존 본능을 자극하는 합리화의 기제가 동작하기 시작한다. 그리고 이런 사람들이 늘어나면 늘어날수록 사회는 비 이성적인 사회로 변화하게 된다.


인간의 합리성과 생존 본능에 대해 이해하는 것이 우리가 살고 있는 사회가 정상적인지 아닌지를 판단하는 기준이 된다. 그리고 모든 사람들은 스스로의 생각이 다른 사람의 생존 본능을 자극하는 것은 아닌지, 즉 자기 스스로 자기 합리화에 빠져 있어서 세계를 객관적으로 바라보지 못하고 있지는 않은지를 끊임없이 되물어야 한다. 일단 생존 본능이 자리잡은 이성은 쉽게 치유되지 않는다. 그리고 자기 정화적인 노력이 없이는 스스로 치유될 수도 없다. 내가 생존하고 있는 사회가 끊임없이 자기 정화를 요구하느냐 그렇지 않느냐도 사회를 판단하는 기준이 된다.

사람의 두뇌는 나이가 들수록 굳어간다. 하지만 이것도 절반만 사실이다. 주위 세계가 자기 정화를 강요하는 수준이 생존 본능을 자극할 수준이라면 어느 누구도 현재 자기의 상태에 안주할 수 없게 된다. 인간의 두뇌에 부자연스러운 일이긴 하지만 충분히 훈련하면 할 수 있는 일이다. 이런 훈련이 두뇌를 깨어 있게 만들고, 항상 세계를 지속적으로 진지하게 바라볼 수 있도록 만든다.

Posted by 이세영2
,

우리가 어떻게 해서 유연성을 확보할 수 있었는가?

그것은 추상화(Abstraction)에서부터 시작되었다. 추상화를 통해 우리는 여러 요구사항들 중에서 공통점을 찾고, 이 공통점에서 목표한 것과 관련 없는 것들을 제거하였다. 이를 기반으로 공통점을 캡슐화할 수 있었고, 이 캡슐화된 대상에 타입을 부여할 수 있었다.

이 추상화의 과정을 비유적으로 이야기 해보면 이렇다. 개별적인 것들은 다들 개성이 강하고 다른 듯 하지만 멀리서 보면 대개 비슷하다. 우리 두뇌는 이런 일들을 잘 해낸다. 소위 "패턴"은 이와 맥락을 같이 하는 단어이다. 디자인 패턴이든 건축 패턴이든 패턴이라는 것은 개별적인 시도가 가지는 공통된 맥락을 의미한다. 추상화란 다들 서로 다른 듯 보이는 것들이 내제하고 있는 일반적인 모습, 바로 "패턴"을 찾아내는 과정이다. 사실 세상에는 "패턴"으로 정의할 수 있는 것들이 무수하게 많다. 어느 분야의 대가라고 불리는 사람들은 그 분야에서 벌어지는 다양한 시도들이 어떤 "패턴"을 가지고 있는지를 이해하고 있다. 그들은 그러한 패턴들을 알고 있기 때문에 새로운 시도를 할 때에도 마치 이전에 경험했던 일을 하는 것처럼 쉽게 해낼 수 있다. 

당대에 유명한 사랑꾼으로 통했던 카이사르는 자신이 어떤 후보를 추천하기 위해 추천서를 썼던 것처럼, 이미 작성해 놓은 똑같은 내용의 연애 편지에 이름만 다르게 써서 여자들에게 보냈을지도 모를 일이다. 안타깝게도 카이사르의 연애 편지들은 모두 그 후계자인 아우구스투스가 없애버렸기 때문에 이제는 확인할 길이 없지만 말이다. 어쨌든 똑같은 연애 편지에는 대상자 이름이 적혀 있지는 않았을 것이다. 그래야 연애 편지가 유연성을 가질테니까 말이다. 이것을 좀 더 일반적으로 표현해 보자면 공통된 정보를 모아 놓되 구체적인 정보는 숨겼다는 말이다. 이것을 객체지향에서는 정보 은닉(Information Hiding)이라고 부른다.

진짜 객체지향은 정보 은닉에서부터 시작된다.

객체지향 언어를 통해서 얻고자 하는 것이 유연성(기능의 확장, 교체, 변경)이라면 정보 은닉은 그것을 가능하게 하는 전략이다. 객체, 상속, 캡슐화 등은 정보 은닉의 수단에 불과하다. 그리고 좋은 정보 은닉은 잘 된 추상화를 통해 얻어진다.

많은 개발자들이 객체지향에 들어서면서 캡슐화를 정보 은닉이라고 배운다. 몇몇 훌륭한 블로그들을 제외하고는 대부분의 블로그들이 정보 은닉 = 캡슐화로 설명하고 있다. 매우 안타까운 일이다. 정보 은닉을 캡슐화로만 알고 있으면 아직 객체지향 입구에도 못들어 온 것이다.

정보 은닉과 관련하여 인터넷을 검색해 본 결과, 정확하게 정보 은닉을 설명한 것은 아래 글 밖에 없었다.

http://egloos.zum.com/aeternum/v/1232020


정보 은닉의 정의

- 모든 객체지향 언어적 요소를 활용하여 객체에 대한 구체적인 정보를 노출시키지 않도록 하는 기법.


소프트웨어의 유연성을 확보하는 단 한가지 방법만 있다면 그것은 무엇일까? 그것은 "객체(또는 클래스) 간에 서로를 모르게 하는 것"이다. 어떤 객체가 다른 객체를 생성하든, 다른 객체의 메소드를 호출하든, 다른 객체가 가진 정보를 조회하든, 다른 객체의 타입을 참조하든, 어떤 행위라도 상관이 없이, 안하는 것이 가장 좋다. 두 객체(또는 클래스)가 서로를 모른다는 것은 서로의 코드에 상대 객체나 클래스에 대한 코드가 단 한 줄도 없다는 의미이다. 만약 두 객체간에 전혀 관계가 없다면 두 객체 중 어느 하나가 수정되거나 사라지더라도 다른 객체는 전혀 영향을 받지 않는다. 따라서 두 객체간에는 유연성이 확보된다. 이것은 매우 자명한 이치지만 이런 원칙을 전체 시스템에 확장시킬 수는 없다. 객체지향 언어에서 어떤 목적을 달성하기 위해서는 필연적으로 다른 객체와의 협력이 있어야 하기 때문이다. 이 필연성은 "어떤 객체도 섬이 아니다"라는 워드 커닝헴과 켄트 벡의 말로 대변된다. 객체지향 시스템에 참여하는 모든 객체들은 어떤 형태로든 관계들로 엮여 있다. 객체를 노드로 하고 관계를 엣지로 나타내면 단 한 덩어리의 연결 그래프가 되어야만 한다. 만약 어떤 객체가 다른 어떤 객체와도 관계를 갖지 않는다면 그 객체는 별도의 시스템이다.

일단 발생한 관계는 유연성을 발휘하지 못하게 만든다. A가 B에 책임을 위임한 경우라면 B의 수정은 A의 위임 목적을 해칠 수 있다. 원래 B로 부터 얻고자 했던 결과를 더 이상 얻을 수 없을지 모른다. 같은 관계에서 A의 수정은 B의 책임을 더욱 강화 시킬 수도 있고, 반대로 전혀 필요 없는 객체로 만들어 버릴 수도 있다. 일단 관계가 발생하면 언제라도 관계가 있는 객체에 수정을 발생 시킬 여지가 있다. 그리고 어떤 객체든 적어도 하나 이상의 다른 객체와 관계를 맺어야만 한다. 이것이 어쩔 수 없는 현실이라면, 즉 어떻게든 관계가 있을 수 밖에 없다면, 똑같은 관계라도 더 좋은 관계로 변경해야 한다. 그렇다면 어떤 관계가 좋은 관계일까?


1. 자주 변경될 가능성이 있는 것에는 의존하지 않는다.

2. 외부로 노출된 메소드를 최소한으로 줄인다. 노출된 메소드가 최소인 객체는 노출된 메소드가 많은 객체에 비해 메소드가 적게 호출되고, 이는 다른 객체와의 관계가 발생할 가능성을 줄인다.

3. 객체의 책임을 최소한으로 줄인다. 책임이 작은 객체는 다른 객체와의 관계가 작아진다. 책임이 작아진 객체는 또한 수정될 가능성이 줄어든다. 따라서 다른 객체에 수정의 영향을 줄 가능성도 줄어든다.


정보 은닉의 종류

- 객체의 구체적인 타입 은닉(= 상위 타입 캐스팅)

- 객체의 필드 및 메소드 은닉(= 캡슐화)

- 구현 은닉(= 인터페이스 및 추상 클래스 기반의 구현)


정보 은닉의 목적

- 코드가 구체적인 것들(타입, 메소드, 구현)에 의존하는 것을 막아줌으로써 객체 간의 구체적인 결합도를 약화시켜 기능의 교체나 변경이 쉽도록 함.

- 동일한 타입의 다른 구현 객체들을 교체함으로써 동적 기능 변경이 가능함.

- 연동할 구체적인 구현이 없는 상태에서도 (인터페이스 만으로) 정확한 연동 코드의 생성이 가능함.


자 그러면 본격적으로 구체적인 구현을 통해서 정보 은닉 방법과 이점을 살펴보도록 하자.


"생성부터 은닉질이냐!?"

그렇다. 객체의 생성시부터 정보 은닉을 해야 한다. 아래 코드를 보자.

class Rectangle{

    public void rectangle(){ System.out.println("rectangle"); }

}


public static void main(String[] args) {

    Rectangle rectangle = new Rectangle(); // --- 1

    rectangle.rectangle(); // Rectangle 클래스에 의존적인 코드

}

위의 코드에서 1과 같이 객체를 생성했다고 하자. 객체는 생성 이후에 rectangle이라는 Rectangle 클래스 변수로 참조된다. 따라서 Rectangle에 선언된 모든 메소드를 사용할 수 있게 된다. 따라서 rectangle.rectangle()의 호출이 가능해진다. 이것은 Rectangle라는 객체에 전적으로 의존하게 되는 코드이다.

만약 우리가 좀 더 생각해서 Rectangle과 유사한 기능, 즉 Circle을 구현하게 될지도 모르고, 이에 따라서 Rectangle을 대신해서 Circle을 사용하게 될지도 모른다고 하자. 그래서 Rectangle과 Circle을 모두 지칭할 수 있는 상위 클래스인 Shape을 만들고, 각각의 모양을 그릴 수 있는 메소드(draw() 메소드)를 구현하도록 정의했다고 하자. 

그러면 아래의 2와 같은 코드를 만들 수 있다.

abstract class Shape{

    abstract public void draw();

}


class Rectangle extends Shape{

    public void draw(){ rectangle();}   

    public void rectangle(){ System.out.println("rectangle"); }

}


public static void main(String[] args) {


    Shape shape = new Rectangle();         // --- 2

    shape.draw();  // Shape 클래스에 의존적인 코드

}

코드 2는 동일하게 Rectangle을 생성했지만 곧바로 그 상위 타입인 Shape 클래스 참조 변수인 shape으로 객체를 참조한다. 따라서 이후에 shape 참조 변수를 통해 사용할 수 있는 메소드는 Shape 클래스의 메소드로만 제한된다. 그래서 생성 이후에는 Rectangle 클래스와 관련된 어떤 메소드도 호출되지 않는다. 이것이 구체적인 타입 은닉에 해당된다.

이를 통해서 얻을 수 있는 이점은 다음과 같다.

- Rectangle의 생성 코드 이후에는 어떤 코드도 Rectangle 클래스에 의존하지 않는다.

- 따라서 Rectangle 대신에 Circle을 사용하고 싶어졌을 때에는 Rectangle 대신 Circle을 생성하도록 변경하기만 하면 된다. 그러면 그 이후의 코드들은 전혀 수정될 필요가 없다.

- 만약 Rectangle을 사용하다가 Circle을 사용해야 할 경우도 발생할 수 있다. 바로 동적으로 기능을 교체해야 할 경우이다. 이 때에도 선언되어 있는 shape 참조 변수에 새로 생성한 Circle 객체만 참조로 할당해 주기면 하면 된다. 이런 방법으로 동적인 기능 전환도 쉽게 할 수 있다.


위의 코드에도 문제가 있다고 해서 다양한 디자인 패턴들이 생겨났다. Abstract Factory 패턴, Factory Method 패턴과 같은 경우가 바로 그것이다. 이들 생성 패턴들은 생성과 동시에 구체적인 타입 은닉을 수행하도록 되어 있다. 

- Abstract Factory 패턴

- Factory Method 패턴


아래는 각 생성 패턴들이 공통으로 추구하는 방향만을 간략하게 구현해 본 것이다.

class ShapeFactory{

    public Shape createRectangle(){ return new Rectangle(); }

    public Shape createCircle(){ return new Circle(); }

}


public static void main(String[] args) {

    ShapeFactory factory = new ShapeFactory();


    Shape shape = factory.createCircle();

    shape.draw();

}

위에서 구체적인 객체, 즉 Rectangle과 Circle 객체를 생성하는 책임을 담당하는 클래스가 ShapeFactory 클래스이다. 그리고 이 클래스는 객체의 생성과 함께 객체를 리턴하는데, 리턴하는 타입은 동일하게 Shape 타입으로 객체를 리턴한다.

이것이 어떤 효과를 가져 오는가? main() 메소드에서 ShapeFactory를 사용하게 되어 있는데, main() 메소드가 ShapeFactory의 createCircle() 메소드를 호출해서 객체를 받아 올 때 타입은 이미 Shape으로 변경되어 있다. 따라서 main() 메소드에서는 Rectangle이라는 클래스나 Circle이라는 클래스는 전혀 모르는 상태다. main()이 알고 있는 것은 오직 Shape 객체 뿐이다. 따라서 이 ShapeFactory를 이용해서 객체를 생성하면 생성된 이후의 모든 코드와 Rectangle 또는 Circle과는 전혀 무관한 코드가 된다. 오직 Shape만 이용하게 되기 때문이다.

그러면 어떤 장점이 있을까? 당연히 객체의 교체나 변경이 쉬워지게 된다. 또 다른 Shape 타입을 추가하는 것도 손쉬워진다. ShapeFactory를 거친 이후에는 모두 다 같은 Shape으로 취급될 것이기 때문이다.


캡슐화를 통한 정보 은닉

이 부분에 대해서는 다른 여러 블로그나 책들에서도 언급을 하고 있다. 하지만 상대적으로 덜 강조되고 있는 부분은 짚고 넘어가야 겠다.

일단 변수(필드)에 private 키워드를 이용해서 외부 노출을 줄이는 부분에 대해서는 어떤 책이든 강조를 하고 있다. 이를 통해서 필드를 외부에서 임의로 접근해서 발생할 수 있는 문제들을 없앨 수 있다.

메소드에 대해서는 상대적으로 강조가 적은 편인데 아래 예를 보면서 이야기를 해보자.

class Process{

    public void init(){}

    public void process(){}

    public void release(){}

}

위의 클래스는 public 메소드가 모두 3개이다. 즉, 외부에서 이 클래스의 객체를 사용하는 코드들에서는 모두 3개의 메소드에 의존하게 된다. 이는 혹시라도 Process 객체를 수정하거나, 아예 제거를 하는 등의 수정이 발생했을 때 3개의 메소드보다 적은 수의 메소드에 의존하는 코드들에 비해 수정이 더 많이 되어야 함을 의미한다.

또한 불필요하게 많은 수의 메소드를 노출시키면 여러가지 나쁜 면이 있다. 첫째로 메소드의 호출 순서를 제대로 알지 못해서 발생하는 문제점이 있을 수 있다. 메소드들이 서로 시간적 연관 관계가 있어서 순서대로 호출되어야 하는데 여러 메소드로 나누어져 있을 경우 이를 알지 못해 오류가 발생할 수 있다. 둘째로 구현의 구체적인 사항을 외부에 노출시킨다는 점이다. 이 Process의 세부 단계에 대해서 외부 객체들이 알게 됨으로써 구현을 유추할 수 있거나 유추해야만 하는 문제가 발생한다. 세번째로 어떤 메소드가 중요한지를 알 수 없게 된다. 적절한 수준에서 정보를 숨겨줌으로써 객체를 이해하는 입장에 도움을 주어야 하는데 모든 메소드가 노출되어 있으면 무엇이 중요한 메소드인지를 알 수 없다.

그럼 아래 코드를 보자.

class Process{

    private void init(){}

    private void process(){}

    private void release(){}

    public void work(){

        init();

        process();

        release();

    }

} 

위와 동일한 기능을 하지만 좀 더 나은 모습이다. 일단 이전에 보여졌던 메소드들이 모두 비공개(private) 메소드로 변경되었다. 따라서 외부 객체에서는 이들 메소드를 호출할 수가 없다. 대신에 외부에서 호출이 가능하도록 work() 메소드를 공개하고 있다. 따라서 외부 메소드들은 work() 메소드만을 이용할 수 있다.

이를 통해 얻는 장점은 다음과 같다. 우선 적절한 수준에서 메소드들이 공개와 비공개로 나누어져 있기 때문에 어떤 메소드를 우선 살펴야 할지를 알 수 있다. 또한 개별 메소드들의 호출 순서를 work() 메소드에서 정해주고 있기 때문에 Process 객체 사용에 대한 정보를 더 적게 알아도 된다. 마지막으로 work()라는 메소드만 노출 되었을 때에는 Process 객체가 하는 일의 세부 내용을 덜 노출시킨다. 즉, 외부에서는 Process 객체가 init - process - release 단계를 거친다는 점을 알 수 없다.


오직 인터페이스에만 의존하도록 한다

만일 객체를 잘 설계하여 변수를 private으로 선언하고, 꼭 필요한 메소드만 외부로 공개하였다고 하자. 그러면 외부 객체와 잘 설계된 객체간에 의존성은 오직 공개 메소드에 의해서만 발생하게 된다. 그래서 공개 메소드를 비공개 또는 보호 메소드들과는 구분하기 위해서 인터페이스라는 별도의 용어를 부여하게 되었다. 그만큼 공개 메소드가 개념적으로 중요하기 때문이다.

JAVA 언어에서는 이 개념을 더욱 강화하여 클래스와 유사하게 상속 가능한 타입이면서 구체적인 구현을 배제한 interface 라는 개념을 만들어 냈다. 예제에서는 interface라는 추상화 요소를 사용하게 될텐데 혹시 JAVA가 아닌 다른 객체지향 언어를 사용하고 있다면 interface를 "공개 추상 메소드만을 가지고 있는 추상 클래스" 정도로만 이해하면 된다.

앞서 이야기 했듯이 객체와 외부와의 소통은 오직 공개 메소드만으로 이루어진다. 그렇다면 어떤 객체가 공개 메소드의 모양, 즉 프로토타입(= 공개 추상 메소드)만 가지고 있는 상위 클래스를 상속 받았고, 오직 그 상위 클래스가 제공하는 공개 메소드만을 외부로 공개하였다면, 이 클래스는 상위 클래스로 지칭될 수 있다. 이 때 상위 클래스는 오직 공개 메소드를 선언하는 선언부 역할만을 하고, 하위 클래스는 이를 구체적으로 구현하는 역할만 가지게 된다.

이를 코드로 나타내 보면 아래와 같다.

interface Interface{

    public void method();

}

class ConcreteClass implements Interface{

    public void method(){ System.out.println("method"); }

}

Interface는 오직 공개 추상 메소드만을 정의하고 있다. ConcreteClass는 이 Interface가 제공하는 공개 추상 메소드를 구체적으로 구현하고 있다.

그러면 객체를 사용하는 입장에서는 어떠한가? 객체를 사용하는 입장에서는 ConcreteClass를 Interface 타입으로 지칭할 수 있게 된다. 그리고 모든 기능이 Interface가 정의한 공개 메소드를 통해 실행 가능하므로 ConcreteClass를 이용하는데 전혀 문제가 없다.

이처럼 인터페이스와 구현을 분리하면 다음과 같은 이점이 있다.

- Interface만으로 객체를 다룰 수 있으므로 구체적인 구현에 대해서 전혀 모르더라도 동작이 가능하다. 즉 구현에 대해 관심을 둘 필요가 없다.

- 좀 더 나가보면 Interface만 알고 있어도 Interface에 의존하는 코드를 작성할 수가 있다. 즉, Interface를 상속 받는 임시 객체를 만들어 두고 이를 이용하는 코드들을 만들었다가 추후에 Interface를 구현한 구체 클래스가 완성되었을 때 연결만 시켜주면 된다.

- 모든 클래스들이 오직 Interface에만 의존하게 되므로 구체적인 의존 관계가 없어지면서 각 객체가 서로 분리되어 있기 때문에 구체적인 객체를 다른 객체로 교체한다거나 Interface를 구현한 새로운 객체를 만들어서 제공함으로써 기능을 확장하는 것이 가능해진다.

이러한 인터페이스의 장점을 이용한 디자인 패턴은 모두 열거하기 힘들 정도로 많다. 가장 대표적인 것만 꼽으라면 아래와 같다.

- State 패턴 : 상태를 객체화하고, 인터페이스를 통해 상태화 된 객체를 지칭하게 함으로써 상태가 추가되기 용이하도록 한다.

- Bridge 패턴 : 연관관계가 있는 두 부류의 객체들을 두 개의 인터페이스 간의 연관관계로 바꾸고, 구체적인 객체들을 인터페이스 상속을 통해 구현 함으로써 각 부류의 객체들에 추가/삭제가 발생하더라도 다른쪽 부류에는 영향을 미치지 않도록 한다.

- Stragegy 패턴 : 기능을 담은 객체를 인자로 넘겨 줌으로써 이를 받는 객체의 기능이 변경될 수 있도록 한다. 이 역시 인터페이스를 중심으로 기능을 담은 객체를 지칭함으로써 기능의 확장이나 변경이 용이하도록 한다.

- Observer 패턴 : 관찰자 객체들을 인터페이스로 추상화하고, 관찰 대상 객체에 이벤트가 발생했을 때 인터페이스만을 활용하여 이벤트를 전달함으로써 관찰자와 관찰 대상 간의 구체적인 결합을 제거한다. 이를 통해서 관찰자에 해당하는 구체적인 객체들의 종류가 늘어나더라도 같은 관찰 대상 객체의 구현에는 영향이 없다.


정보 은닉은 객체지향 언어의 목표이다

기능을 간편하게 수정할 수 있으며, 기능을 추가하기 용이하고, 언제든 기능을 교체하는 것이 가능한 소프트웨어를 구현하는 것은 모든 소프트웨어 개발자들의 꿈이다. 그런 꿈이 담겨 있는 것이 객체지향 언어이고, 이 객체지향 언어가 기능의 수정 / 추가 / 교체를 가능하게 하기 위해 세운 기초 전략이 정보 은닉이다.

객체지향 언어를 통해 만들어진 좋은 설계, 즉 디자인 패턴과 같이 좋은 설계를 대표할만한 것들은 모두 정보 은닉 기법을 적어도 일부를 활용하고 있거나, 전적으로 정보 은닉을 통해 이득을 얻기 위해서 만들어진 것들이다. 그리고 객체지향의 설계 원칙(SOLID), 각종 객체지향 설계에 관련된 격언들은 거의 모조리 정보 은닉에 관련된 것들이다. 또한 객체지향이 만들어낸 여러 개념들과 언어적 특성들 중 대부분은 정보 은닉을 위해 만들어진 것들이다.

(예를 들어 상속을 보통 메소드와 변수를 재사용하는 것이라고 이야기하는데 이는 틀린 말이다. 상속을 통해 받을 수 있는 것 중에서 가장 중요한 것은 타입이다. 이 타입을 내려 받을 수 있기 때문에 하위 객체가 상위 클래스로 지칭 될 수 있다. 이것이 정보 은닉을 가져오고 객체지향의 모든 이점들을 가져 온다.)

Posted by 이세영2
,

참고 URL

https://webcourse.cs.technion.ac.il/236700/Spring2013/ho/WCFiles/pp.pdf


일반적으로 프로퍼티라고 하면 "특성" 정도로 번역할 수 있다. 특성이란 어떤 대상이 동 종의 다른 대상과 다른점을 말한다. 이것을 소프트웨어 용어로 표현하자면 대상이란 객체를 말하고, 특성이란 변수라고 이야기 할 수 있다. 그리고 변수는 이름과 값으로 나타내어 질 수 있다. 이렇게 이름과 값으로 나타내어 질 수 있는 것들을 클래스에 직접 선언하지 않고, HashMap과 같은 Collection에 저장하여 둠으로써 프로퍼티의 동적인 변화에 대응할 수 있도록 하는 것이 Property List 패턴이다.


자료에서는 다음과 같은 이름으로도 불릴 수 있다고 한다.

- Prototype

- Property List

- Properties Object

- Adaptive Object Model(이것은 이 프로퍼티 패턴을 확장한 패턴이다. 동일한 것으로 취급되기는 어렵다)


Property List 패턴의 클래스 다이어그램

Property List 패턴은 PropList 객체를 중심으로 구성된다. PropList 객체는 동일한 타입을 parent 참조 변수를 통해 가지고 있는 복합 객체이다. 이 다이어그램에서는 PropList와 그 인터페이스인 IPropList를 구분시켜 두었다. 이는 parent에 대한 null 체크 등을 방지하기 위해서 parent의 기본 참조를 Null 객체인 NullPropList로 가지고 가기 위해서이다. 이 패턴에서 구조적으로 중요한 부분은 PropList를 parent로 가지고 있다는 점이고, 사실 API의 이해에 더 집중해야 하는 패턴이다.


Property List 패턴의 구현

interface IPropList{

    public Object get(String key);   

    public void put(String key, Object value);   

    public boolean has(String key);   

    public void remove(String key);   

    public Collection<String> keys();   

    public IPropList parent();

}


class NullPropList implements IPropList{

    public Object get(String key){return null;}   

    public void put(String key, Object value){/* not use */}   

    public boolean has(String key){return false;}

    public void remove(String key){/* not use */}

    public Collection<String> keys(){return Collections.emptySet();}

    public IPropList parent(){return null;}

}

class PropList implements IPropList{

    private IPropList parent = new NullPropList();

   

    private Map<String, Object> map = new HashMap<String, Object>();

   

    public PropList(IPropList parent){

        if(parent != null) this.parent = parent;

    }

   

    public Object get(String key){

        if(map.containsKey(key)) return map.get(key);

        return parent.get(key);

    }

   

    public void put(String key, Object value){

        map.put(key,  value);

    }

   

    public boolean has(String key) {

        return map.containsKey(key) || parent.has(key);

    }

   

    public void remove(String key){

        map.remove(key);

    }

   

    public Collection<String> keys(){

        List<String> result = new ArrayList<String>();

        result.addAll(map.keySet());

        result.addAll(parent.keys());

        return result;

    }

   

    public IPropList parent(){ return parent; }

} 

이 구현은 참고 URL 자료에 나와 있는 코드를 거의 그대로 사용한 것이다. 다만, parent에 대한 설정이 생성 즉시 이루어지고 있고, 별도로 parent에 대한 의존성 주입 메소드가 없기 때문에 사실상 일부러 null을 넣지 않는 한 null이 발생할 수는 없다. 따라서 Null 객체 패턴을 적용하여 null을 체크하는 부분들을 모두 없앰으로써 전체 소스의 간결함을 유지하도록 하였다.


이 패턴의 동작은 PropList 객체를 생성하면서 시작된다. 생성자를 통해 객체를 생성할 때 인자로 IPropList 타입인 parent를 넣어 주게 되어 있다. 이는 어떤 프로퍼티 리스트가 다른 프로퍼티 리스트들을 메타 데이터로 가지고 있을 경우를 위한 것이다. 예를 들어 아이폰의 특성을 나타낸다면, 제품명은 모두 다 같은 아이폰이다. 개별 제품들의 시리얼 번호는 각각 다를 것이다. 그렇다면 모두 같은 값을 나타내는 제품명이라는 프로퍼티를 모든 개별 제품들에 넣게 되면 메모리 소모가 많아지게 될 것이다. 따라서 이를 방지하기 위한 목적으로 parent를 별도로 둔다.

이 parent는 필요에 따라서는 계층화 될 수도 있다. 즉 parent가 또 그 상위에 메타 프로퍼티들을 가지도록 구성할 수도 있다.

PropList의 생성 이후 동작은 대부분 프로퍼티의 삽입 / 조회 / 삭제에 관한 것들이다. 일반적인 객체들의 경우 변수에 대한 조회, 변경을 통해서 동작하듯이 Property List 패턴도 그런 목적에 맞도록 이들 연산을 지원한다.


자료에서는 이 패턴이 만들어지게 된 배경에 대해 이렇게 이야기 하고 있다.

- No SQL 데이터 베이스를 이용한 어플리케이션 구현

    관계형 DB의 확장성 문제를 해결하기 위해 No SQL 데이터베이스들을 이용할 경우 key - attribute - value 형식의 테이블을 사용하게 되는데 이런 경우 데이터 베이스와의 연동성이 좋다.

- 프로퍼티 리스트의 유연성이 좋다

- 비즈니스 로직이 프로퍼티의 특정한 값들에 대해서 그다지 관심이 없는 경우에 좋다


사용에 있어서 주의할 점이 있다면 다음과 같다.

- 아무래도 객체를 직접 구현하는 것에 비해서 프로퍼티의 조회는 좀 더 구현이 복잡하다. 만약 이 패턴을 사용하여 복잡한 계산 로직을 구현한다면 문제가 될 것이다. 비즈니스 로직은 매우 단순하면서 다루어야 할 객체의 종류가 많은 어플리케이션에 매우 적합한 패턴이다.

- 프로퍼티란 마치 변수와 같은 것이다. 이 패턴은 객체의 생성 없이 객체를 흉내내려는 패턴이라 할 수 있다. 이를 통해 유연성을 얻을 수는 있지만 캡슐화의 장점은 포기해야 한다.

- 특히 value에 해당하는 객체의 관리에 신중해야 한다. 접근 관리가 잘못되면 전역 변수처럼 사용되버릴 수도 있다.


이 패턴에 맞는 응용 분야는 다음과 같다.

- 동일한 프로토콜을 사용하는 여러 장치들의 데이터를 수집해야 하는 센서 관련 소프트웨어

- 다양한 제품군과 제품들을 취급하는 경우

- 사용자가 필요에 따라서 새로운 제품을 계속 추가해야 하는 경우

'5.디자인패턴' 카테고리의 다른 글

Adaptive Object Model(AOM) 패턴 및 그 구현  (0) 2016.10.01
Actor Model 패턴의 구현(Java)  (0) 2016.09.30
Mediator 패턴  (0) 2016.09.18
Facade 패턴  (0) 2016.09.18
Command 패턴  (0) 2016.09.18
Posted by 이세영2
,