“객체지향의 사실과 오해” 책을 바탕으로 정리를 진행했습니다. 제가 이해한 바를 정리하고 싶어서 적은 포스팅이라 책에 있는 내용과는 맞지 않거나 오류가 있을 수 있습니다. 더 많은 내용을 보고 싶으신 분들은 책을 직접 읽으시는걸 추천드립니다.
협력하는 객체들의 공동체
모든 과정은 역할, 책임, 협력이라는 사람의 일상 속에 항상 스며들어 있는 세 가지 개념이 한데 어울려 조화를 이루며 만들어 낸 것입니다.
협력 관계 안에서 스스로 해결하지 못할 문제와 마주치면 문제 해결에 필요한 지식을 알고 있거나 서비스를 제공해줄 수 있는 사람에게 도움을 요청합니다. 이 요청은 또 다른 요청을 유발하게 됩니다. 즉, 요청이 연쇄적으로 발생하게 됩니다.
그러면, 요청을 받은 사람은 주어진 책임을 다해서 필요한 지식, 서비스를 제공합니다. 이를 응답이라고 합니다. 응답은 요청과 반대 방향으로 연쇄적으로 전달되게 됩니다.
책에서는 카페에서 발생하는 협력을 예시로 듭니다. 손님, 캐시어, 바리스타 사이의 협력입니다.
손님은 캐시어에서 주문을 통해서 커피를 줄 것을 요청합니다. 캐시어는 바리스타에게 커피를 만들 것을 요청합니다. 손님이 캐시어에서, 캐시어가 바리스타에게 요청을 한 이유는 스스로 해결하지 못하는 문제이기 때문입니다.
해당 요청에 대해서 바리스타는 커피를 만들어서 캐시어에게 전달해주고, 캐시어는 손님에게 커피가 나왔다고 진동벨로 알려주는 식으로 요청에 대한 응답을 합니다.
협력 속에서는 각자는 역할을 가집니다. 위의 예시에서는 각자 손님, 캐시어, 바리스타의 역할을 맡은겁니다.
각 역할은 협력에 참여하는 특정한 사람이 협력 안에서 차지하는 책임이나 의무를 의미합니다. ‘손님’이라면 음료를 주문할 임무가 있고, ‘바리스타’라면 음료를 제조할 임무가 있습니다.
‘바리스타’라는 역할이 음료를 제조할 임무를 가졌다는 건, 음료를 제조할 책임을 가졌다는 것과 같은 뜻 입니다. 특정한 역할은 곧 특정한 책임을 암시합니다. ‘바리스타’의 역할을 가진 사람이 음료를 제조하지 않는다면 이는 협력이 실패했음을 의미합니다.
협력은 특정한 책임을 수행하는 역할들 간의 연쇄적인 요청과 응답을 통해서 목표를 달성합니다. 즉, 협력의 성공은 특정한 역할을 맡은 개인이 얼마나 요청을 성실히 이행하는가에 달려 있습니다. 특정한 역할을 맡은 사람이 맡은 책임을 성실하게 수행하지 않는다면 협력은 실패합니다.
이는 객체도 동일합니다. 객체의 협력도 객체 간의 연쇄적인 요청과 응답으로 구성됩니다.
객체 협력이 성공하기 위해서는 적절한 객체에게 적절한 책임을 할당해야 합니다. ‘바리스타’라는 객체에게 음료 제조의 책임을 할당해야지, 주문할 책임을 할당하면 안됩니다.
즉, 적절한 책임을 할당하는 것이 객체지향 설계의 품질을 결정하는 중요한 요소라고 볼 수 있습니다.
사람들이 사는 세계처럼 객체의 세계에서도 객체들끼리 협력합니다.
객체는 다른 객체와 조화롭게 협력할 수 있을만큼 충분히 개방적이어야 하고, 스스로 결정할 수 있을만큼 충분히 자율적이어야 합니다.
다른 객체와 협력하기 위해서 개방적이어야 한다는건 알겠는데, 자율적이어야 한다는건 무슨 뜻일까요?
객체가 스스로 판단하고 스스로 결정하는 자율적인 존재라는 겁니다.
‘바리스타’가 스스로 결정하는 자율적인 존재이기 때문에 캐시어로부터 카페라떼 제조를 요청받았을 때, 어떤 원두를 사용할건지, 어떤 우유를 사용할건지, 어떤 방식으로 라떼를 만들지 결정할 수 있는겁니다.
자율적인 존재가 되기 위해서는 필요한 행동과 상태를 함께 가지고 있어야 합니다. 하지만, 완벽하게 자율적인 객체가 되기 위해서는 객체의 내부와 외부를 명확하게 구분해주어야 합니다. 사적인 부분인 객체 내부에 숨겨서 객체 스스로가 그 부분을 관리할 수 있게 만들어 주는 겁니다.
예를 들어서, ‘캐시어’가 주문전달방법이라는 상태를 있다고 생각해볼게요. 주문전달방법은 컵에다가 어떤 주문이 들어왔는지 적어주든, 바리스타에게 말로 전달하든 ‘캐시어’가 스스로 결정한 방법이어야 합니다. 만약, 주문전달방법을 외부에서 주입 시켜줘야 한다면, 손님이 주문할 때 “카페라떼 주문할게요. 바리스타한테 말로 전달해주세요.” 라고 해야 합니다. 이상하지 않나요?
객체 내부에 숨겨버리면 어떻게 다른 객체와 소통하나요?
사적인 부분은 숨기지만 외부로 공개하는 부분도 있습니다. 접근이 허락된 수단을 통해서만 객체와 의사소통을 할 수 있도록 만들어 두는 겁니다. 다른 객체는 접근이 허락된 수단을 통해서 해당 객체가 ‘무엇’을 수행하는지는 알아도 ‘어떻게’ 수행하는지에 대해서는 모르게 됩니다.
아까 위에서 예를 든 ‘캐시어’는 ‘주문’이라는 허락된 수단을 가지고 있습니다. ‘손님’이 주문을 하고 싶다면 ‘캐시어’의 ‘주문’이라는 수단에 접근하면 됩니다. 하지만, ‘손님’은 ‘캐시어’가 어떻게 주문을 수행할지는 모릅니다. 그저 주문을 받아줄 것이라는 건 압니다.
객체가 자율성이 생기면 자신의 상태를 직접 관리하고, 상태를 기반으로 스스로 판단하고 행동할 수 있습니다. 즉, 특정 행동을 수행하는 방법을 스스로 결정할 수 있습니다.
자율적인 존재가 될 수 있는 이유는 자신의 상태와 행위를 객체라는 하나의 덩어리로 묶었기 때문이죠. 관련있는 상태와 행위가 하나로 묶였기 때문에 유지 보수가 쉽고 재사용이 용이한 시스템을 구축할 수 있습니다.
그럼 이 자율적인 객체는 어떻게 다른 객체와 협력하는걸까요?
위의 예시에서 ‘캐시어’가 ‘바리스타’에게 자신만의 주문전달방법을 사용해서 커피 제조를 요청합니다. 사람들이 살아가는 세계에서는 말로 전달할 수도 있고, 컵에다가 표시를 할 수도 있습니다. 하지만, 객체들이 살아가는 세계에서는 오직 메세지 만을 사용해서 다른 객체와 협력합니다.
송신자는 메시지 전달을 통해서 수신자에게 요청합니다. 메시지를 전달받은 수신자는 자신만의 정해진 방법에 따라서 메세지를 처리합니다. 이 자신만의 정해진 방법이 바로 메서드입니다.
‘캐시어’는 ‘손님’으로부터 ‘주문’이라는 메시지를 전달받습니다. ‘캐시어’는 자신만의 정해진 방법에 따라서 주문을 받고 자신만의 주문 전달 방식을 통해서 ‘바리스타’에게 ‘커피 제조’ 메시지를 전달합니다. 커피 제조 요청을 ‘캐시어’로부터 받은 ‘바리스타’는 자신만의 정해진 방법에 따라서 커피를 제조합니다.
메세지와 메서드가 분리되기 때문에 스스로 결정할 사적인 부분이 숨겨져서 객체의 자율성을 높이게 됩니다. 이를 객체지향에서는 “캡슐화”라고 합니다.
이상한 나라의 객체
객체지향을 현실 세계를 모방한 패러다임이라고 합니다. 하지만, 객체지향은 현실세계 모방이 아닌 현실세계를 기반으로 새로운 세계를 창조한겁니다.
객체지향 세계에는 객체가 살고 있습니다. 현실세계에선 스스로 할 줄 모르는 물체도, 객체지향 세계에서는 스스로 자신의 상태를 바꿉니다. 능동적인 물체가 됩니다.
능동적인 객체의 행동에 따라서 객체의 상태는 변하게 됩니다.
‘나’라는 사람이 ‘먹는다’라는 행동을 하게 되면, ‘몸무게’라는 상태가 증가하게 됩니다.
어떻게 ‘몸무게’가 증가했다고 볼 수 있는걸까요?
어떤 행동의 결과는 이전에 어떤 행동들이 있었는가에 의존합니다. 이전에 ‘나’가 ‘먹는다’를 통해서 ‘몸무게’를 증가시켰을 수도 있고, ‘운동하다’를 통해서 ‘몸무게’를 감소시켰을 수도 있습니다. 하지만, 이런식으로 현재의 행동에 대한 결과를 내려면 과거의 모든 행동들을 다 기억해야 합니다.
따라서, 행동의 과정, 결과를 단순하게 기술하기 위해서 상태를 고안했습니다.
상태를 보면 과거의 모든 행동 이력을 설명하지 않고 행동의 결과를 쉽게 예측할 수 있습니다. 과거에 ‘나’가 ‘운동하다’를 했는지, ‘먹는다’를 했는지에 상관없이 ‘몸무게’라는 상태를 보면 됩니다. ‘먹는다’ 이전보다 이후에 ‘몸무게’라는 상태가 가지는 숫자가 증가했기 때문에 ‘증가했다’라고 볼 수 있는겁니다.
상태가 없었다면 이전에 했던 모든 ‘운동하다’와 ‘먹는다’를 기억해야 했을지도 모릅니다. 이젠 과거에 얽매이지 않고 현재를 기반으로 객체의 행동 방식을 이해할 수 있게 된거죠.
근데, ‘몸무게’는 객체가 아닙니다. 몸무게 뿐만 아니라 키, 위치, 양도 객체가 아닙니다.
이런 건 독립적인 의미를 가지기보단 다른 객체의 특성을 표현하는데 사용합니다. 즉, 다른 객체의 상태를 표현하기 위해서 사용합니다.
그러면 상태는 모두 몸무게, 키, 위치, 양과 같은 단순한 값일까요?
아닙니다. 객체도 상태가 될 수 있습니다.
‘나’가 커피를 사서 들고 있습니다. ‘커피’는 ‘나’의 상태가 됩니다. ‘커피’와 ‘나’는 객체 사이의 연결인 링크로 연결되어 있습니다. 링크가 있으면 객체 사이의 요청을 주고 받을 수 있습니다.
‘커피’는 객체이기 때문에 자신의 상태를 스스로 바꿀 줄 알아야 합니다. 현실세계와는 다르게 객체세계에선 능동적으로 행동할 줄 알아야 하거든요. ‘나’가 ‘커피’를 마셨다고 해서 ‘나’가 ‘커피’의 양을 바꾸지 않습니다. 그건 ‘커피’가 내부에 감춰둔 상태이기 때문에 ‘나’는 상태를 변경할 수 없습니다.
그러면, 어떻게 ‘커피’를 마셨을 때 커피의 양이 줄어들 수 있을까요?
‘나’는 ‘커피’에게 ‘마신다’라는 메시지를 전달합니다. 메세지를 수신한 ‘커피’가 자신만의 방식을 사용해서 커피 양을 줄이게 됩니다. ‘커피’의 행동을 유발하는 것은 외부로부터 온 메시지지만 ‘커피’의 상태를 변경하는 것은 스스로가 할 일 입니다.
물론, 양을 줄이고 싶지 않다면 줄이지 않아도 됩니다. 양을 줄일지 어떻게 할 지는 ‘커피’가 결정할 사항입니다. ‘마신다’를 전달한 ‘나’와는 무관한 일입니다. ‘나’는 그저 ‘커피’의 양이 줄어들 것이라 믿고 ‘커피’에게 요청을 전달할 뿐입니다. ‘커피’가 ‘나’를 배신해서 양을 줄이지 않았다고 한들 ‘나’는 알 수 없습니다. ‘커피’의 상태 변경에 대해서 전혀 알지 못하거든요. 사실, 상태가 변경된다는 사실조차 모릅니다.
이렇게 되면 ‘커피’의 자율성이 높아집니다. 자율성이 높아진다는건 곧, 협력이 유연하고 간결해진다는 걸 뜻합니다.
상태를 잘 정의된 행동 집합 뒤로 캡슐화하는 것은 객체의 자율성을 높이고 협력을 단순하고 유연하게 만들어 줍니다. 이것이 상태를 캡슐화해야 하는 이유입니다.
그렇다면, 객체가 가진 상태를 객체 스스로가 바꾸게 된다면, 객체는 전과 다른 객체가 되는걸까요?
값은 이전과 다른 값을 가지면 다른 값이라고 인식합니다. 우리가 50kg인 몸무게를 60kg인 몸무게와 같은 값으로 인식하지 않는 것처럼 말이죠.
하지만, 커피의 양이 달라졌다고 해서 우리는 다른 커피로 인식하지 않습니다.
우리가 다른 커피로 인식하지 않을 수 있는 이유는 객체는 어떤 상태에 있더라도 유일한 존재로 식별 가능하기 때문입니다. 객체는 식별자를 가지고 있기 때문에, 내부에 있는 상태가 달라져도 같은 객체로 생각합니다.
이렇게 식별자를 지닌 객체를 reference object, entity라고 하고, 식별자가 없는 값을 value object라고 합니다.
우리는 이번 챕터에서 행동이 상태를 결정한다는 것만 머릿속에 새기면 됩니다.
그럼 반대의 입장에서 상태가 행동을 결정하게 해보면 어떨까요?
상태가 행동을 결정하게 된다면, 보이지 않아야할 부분들이 외부로 노출되어서 캡슐화가 제대로 되지 않습니다. 이는 곧, 객체를 협력하지 못하는 객체로 만들어 버립니다. 객체가 고립된 섬이 되어 버리는거죠. 고립된 섬이 된 객체는 다른 객체들과 어울리지 못하기 때문에 결국 재사용성이 저하됩니다.
그렇기 때문에 상태가 아니라 행동에 초점을 맞춰야 합니다. 객체의 적합성을 결정하는 것은 상태가 아니라 행동입니다.
객체지향 설계는 ⓵애플리케이션에 필요한 협력을 생각하고, ⓶협력에서 필요한 행동을 생각하고, ⓷행동을 수행할 객체를 선택하는 순서로 진행되어야 합니다. 행동을 결정한 후에 그 행동에 필요한 정보를 결정하면 됩니다.
타입과 추상화
추상화는 현실에서 출발하되 불필요한 부분을 도려내가면서 사물의 놀라운 본질을 드러나게 하는 과정입니다.
즉, 복잡성을 이해하기 쉬운 수준으로 단순화하는 것입니다.
불필요한 부분은 무시함으로써 현실에 존재하는 복잡성을 극복하기 위해서 추상화를 합니다. 하지만, 훌륭한 추상화는 불필요한 부분을 무시하는 걸로 끝나지 않습니다. 목적에 부합해야 합니다.
추상화의 수준, 이익, 가치는 목적에 의존적입니다. 추상화를 했지만 목적에 맞지 않으면 추상화하느니만 못한 결과를 냅니다.
초기 지하철 노선도
지하철 노선도가 처음 나왔을 때, 실제 위치를 반영해서 노선도를 그렸습니다. 해당 노선도는 지하철 노선을 추상화했지만 사람들은 보기 힘들어 했습니다. 실제 위치는 크게 중요한 것이 아니었기 때문이죠. 오히려 노선도를 보기 힘들게 만들었습니다. 후에 실제 위치를 반영하지 않고 역과 역 사이의 관계에 중점을 둔 지하철 노선도가 나왔습니다. 이는 목적에 부합했기 때문에 현재까지도 지하철 노선도로 사용이 되고 있습니다.
추상화는 두 가지 차원에서 이루어집니다.
1.
구체적인 사물 간의 공통점을 취하고 차이점을 버리는 일반화를 통해 단순하게 만듦
우리가 외부에서 교복을 입은 사람들을 만났을 때, “학생”이라고 부릅니다. 우리는 교복을 입은 사람들을 “학생”이라는 하나의 개념으로 단순화해서 바라보게 됩니다.
그들을 “학생”이라고 할 수 있는 이유는 공통적으로 떠오르는 일반적인 외형과 행동 방식을 지니고 있기 때문입니다. 즉, 그들의 차이점을 무시하고 공통적인 특성으로 그들을 설명할 수 있습니다.
2.
중요한 부분을 강조하기 위해서 불필요한 세부 사항을 제거하는 겁니다.
“학생”이 교복을 입었고, 학교를 다니는 사람이라는 걸 제외하고 나머지 세부 사항은 모두 무시합니다.
추상화하게 되면 그 그룹에 속하는 객체와 속하지 않는 객체가 나뉘게 됩니다. 학생인 사람과 학생이 아닌 사람으로 나뉘게 되는거죠. 그렇게 되면, 길거리에 내제된 복잡성이 효과적으로 감소합니다. 우리는 사람들을 개별적으로 바라보지 않고 교복을 입은 사람은 학생으로, 나머지는 학생이 아닌 사람으로 구분하게 됩니다
차이점을 의도적으로 무시하고 공통점만 강조함으로써 그룹에 속할 수 있는 객체를 취사 선택하게 됩니다.
우리는 공통적인 특성을 기준으로 객체를 여러 그룹으로 묶어 동시에 다뤄야 하는 가짓수를 줄입니다. 상황을 단순화하는 겁니다.
공통적인 특성을 기반으로 객체를 분류할 수 있는 일종의 체가 바로, “개념” 입니다. 개념은 특정한 객체가 어떤 그룹에 속할 것인지 결정하는 일종의 분류 장치 역할을 합니다.
특정 개념에 속하는 객체들은 해당 개념을 표현하는 그룹의 일원이 됩니다. 00고등학교 교복을 입은 남학생 김00군은 교복을 입고 있기 때문에 학생이라는 개념의 일원이 될 수 있습니다. 우리는 김00군을 학생의 인스턴스로 볼 수 있습니다.
개념을 이용해서 공통점을 가진 객체들을 분류할 수 있습니다.
“분류”한다는 것은 특정한 객체를 특정한 개념의 객체 집합에 포함시키거나 포함시키지 않는 작업을 말합니다.
객체를 적절한 개념에 따라 분류하지 못한 애플리케이션은 유지보수가 어렵고 변화에 쉽게 대처하지 못합니다. 따라서, 적절한 분류 체계가 꼭 필요합니다. 분류 체계는 애플리케이션을 다루는 개발자의 머릿속에 객체를 더 쉽게 찾고 조작할 수 있는 정신적인 지도를 제공합니다.
따라서, 우리는 최대한 직관적으로 분류해야 합니다.
개발에서도 개념이라는 단어를 다른 형태로 사용합니다. 바로, “타입” 입니다. 타입은 개념이기 때문에 타입에는 특정한 타입에 맞는 데이터가 들어와야 합니다.
특정 타입에 맞다는건 어떻게 결정되나요?
데이터가 어떤 타입에 속하는지 결정하는 것은 데이터에 적용할 수 있는 작업입니다. 즉, 연산자(operator)를 뜻합니다. 어떤 데이터에 어떤 연산자를 적용할 수 있는지가 그 데이터의 타입을 결정합니다.
“어떤 종류의 연산이 해당 데이터에 대한 수행 가능한가”로 메모리 안에 저장된 데이터의 종류를 분류합니다.
즉, 어떤 객체가 어떤 타입에 속하는지 결정하는 것은 객체가 수행하는 행동입니다.
동일하게 행동하면 그 객체들은 동일한 타입에 속하게 됩니다. 어떤 데이터를 보유하고 있는지는 타입 결정에 아무런 영향을 미치지 못합니다.
같은 타입에 속한 객체는 행동만 동일하다면 서로 다른 데이터를 가질 수 있습니다.
이를 “다형성”이라고 합니다. 다형성은 동일한 요청에 대해 서로 다른 방식으로 응답할 수 있는 능력입니다. 동일한 요청을 받을 수 있다는 건 동일한 메세지를 수신할 수 있다는 뜻입니다.
다형적인 객체들은 동일한 타입에 속하게 됩니다.
동일한 요청을 받는지만 본다는건, 데이터가 아닌 행동만이 고려 대상이라는 겁니다.
행동에 따라 객체를 분류하기 위해선 객체 내부 데이터가 아니라 객체 외부에 제공하는 행동을 먼저 생각해주어야 합니다. 또한, 외부에 데이터를 감춰야 합니다. 훌륭한 객체지향 설계는 외부에는 행동만을 제공하고 데이터는 행동 뒤로 감춰야 합니다.
우리는 ⓵외부에 제공해야 하는 책임을 먼저 결정하고, ⓶그 책임을 수행하는데 적합한 데이터를 나중에 결정합니다. 그리고 ⓷데이터를 책임을 수행하는데 필요한 외부 인터페이스 뒤로 캡슐화시킵니다.
이런 식으로 설계는 방식이 바로, 책임-주도 설계 입니다.
객체를 결정하는 것은 행동이고, 데이터는 단지 행동을 따른다고 보는 설계 방식이죠.
아무튼, “학생”이라는 타입으로 분류하던 때로 다시 돌아가 봅시다.
우리는 “고등학생은 학생이다” 라고 생각할 수 있습니다. 즉, 고등학생 타입에 속한 객체는 학생 타입의 객체에도 함께 속해야 합니다. 학생은 좀 더 일반적인 개념입니다. 개념을 포괄하는 개념인거죠.
반대로 고등학생은 좀 더 특화된 행동을 하는 특수한 개념입니다. 학생보다는 좁은 범위에 속하게 되죠.
이를 “일반화/특수화 관계” 라고 합니다.
일반화/특수화 관계를 결정하는 것은 객체의 상태를 표현하는 데이터가 아니라 행동입니다. 더 일반적인, 더 특수한 상태가 있다고 해서 성립하지 않습니다. 중요한 것은 행동입니다.
특수한 타입은 일반적인 타입이 할 수 있는 모든 행동을 동일하게 수행 가능합니다.
고등학생이라는 타입은 학생으로 할 수 있는 모든 행동들을 가지면서 고등학생이기 때문에 할 수 있는 행동을 추가로 합니다. 예를 들어서, “수능 보기”가 있겠네요.
학생같은 일반적인 타입은 슈퍼 타입(super type), 고등학생같은 특수한 타입은 서브 타입(sub type)입니다.
어떤 타입이 다른 타입의 서브 타입이 되기 위해서는 행위적 호환성을 만족시켜야 합니다. 서브 타입은 슈퍼 타입의 행위와 호환되기 때문에 서브 타입은 슈퍼 타입을 대체할 수 있어야 합니다. 슈퍼 타입의 행동은 서브 타입에게 자동으로 상속되기 때문이죠.
슈퍼 타입은 차이점을 배제하고, 공통점만 강조합니다.
고등학생 타입을 좀 더 단순한 관점에서 바라보기 위해 불필요한 특성을 배제하고 좀 더 포괄적인 의미를 지닌 것으로 일반화하면 학생이 되는거죠.
이러한 타입들은 시간에 따라 동적으로 변하는 객체의 상태를 시간과 무관하게 정적인 모습으로 다룰 수 있게 해줍니다.
즉, 시간에 따른 객체의 상태 변경이라는 복잡성을 단순화할 수 있는 효과적인 방법입니다.
객체가 살아 움직이는 동안 상태가 어떻게 변하고, 어떻게 행동하는지 포착하는 것을 “동적 모델”, 객체가 가질 수 있는 모든 상태와 모든 행동을 시간에 독립적으로 표현한 것을 “정적 모델”이라고 합니다.
정적 모델은 타입을 구현하는 가장 보편적인 방법인 클래스를 이용해 구현됩니다.
클래스는 타입을 구현하는 메커니즘 중 하나이고, 타입은 객체를 분류하는 개념입니다. 클래스는 타입과 동일하지 않습니다.