Book - 객체지향의 사실과 오해 (3)
1. Chapter 06. 객체 지도
유일하게 변하지 않는 것은 모든 것이 변한다는 사실뿐이다..
여행 중에 다른 마을로 이동해야 하는데 길을 모른다고 가정해 보자.
이 경우 사람들은 두 가지 방법중 하나를 이용해 길을 찾는다.
- 지나가는 사람에게 마을까지 가는 길을 직접 물어본다.
- 지도에 표시된 길을 따라간다.
어떤 방법을 사용하건 상관없이 목적지로 이동할 수 있겠지만
방법에 따라 길을 찾는 과정과 난이도의 차이가 있다.
1번은 기능적이고 해결책 지향적인 접근법이다.
올바른 길을 알려주고 지시를 올바르게 따른다면 원하는 마을로 이동할 수 있다.
하지만 이 방법은 일반적이거나 재사용 가능하지 못하다.
2번은 구조적이고 문제 지향적인 접근법이다.
지도는 실세계의 지형을 기반으로 만들어진 추상화된 모델이다.
지도는 길을 찾는 데 필요한 구체적인 기능이 아니라 길을 찾을 수 있는 ‘구조’를 제공한다.
지도는 현재의 목적뿐 아니라 다양한 목적을 위해 재사용될 수 있다. 즉 범용적이다.
지도를 사용하는 사람들의 요구사항은 계속 바뀐다.
마을까지의 길을 찾을 수도 있고,
기차역으로 이동하는 길을 찾을 수도 있고,
공원으로 가는 길을 찾을 수도 있다.
2번의 방식은 이러한 변화하는 요구사항에 모두 대응할 수 있는 반면,
1번의 방식은 그러지 못한다.
지도는 기능에 비해 상대적으로 잘 변하지 않는 안정적인 지형 정보를 기반으로 하고 있기 때문이다.
지도 은유의 핵심은 기능이 아니라 구조를 기반으로 모델을 구축하는 편이
좀 더 범용적이고 이해하기 쉬우며 변경에 안정적이라는 것이다.
역할과 책임을 수행하며 협력하는 자율적인 객체들의 공동체.
<책임 - 주도>의 설계 방식.
‘전통적인 소프트웨어 개발 방법’은 변경이 빈번하게 발생하는 기능에 안정적인 구조를 종속시키는 길을 묻는 방법과 유사하다.
반면에 ‘객체지향 개발 방법’은 안정적인 구조에 변경이 빈번하게 발생하는 기능을 종속시키는 지도의 방법과 유사하다.
이것이 OOP가 전통적인 소프트웨어 개발 방식보다 범용적이고, 재사용성이 높으며, 변경에 안정적인 이유다.
OOP는 기능이 아닌 구조를 기반으로 시스템을 구조화한다.
1.1. 기능 설계 vs 구조 설계
‘기능 측면’의 설계는 제품이 사용자를 위해 무엇을 할 수 있는지에 초점을 맞춘다.
‘구조 측면’의 설계는 제품의 형태가 어떠해야 하는지에 초점을 맞춘다.
설계의 가장 큰 도전은 기능과 구조라는 두 가지 측면을 함께 녹여 조화를 이루도록 만드는 것이다.
소프트웨어를 개발하는 일차적인 이유는 사용자에게 훌륭한 기능을 제공하기 위해서이다.
따라서 개발 초기 단계에서는 사용자가 무엇을 원하는지, 어떤 기능을 제공해야 하는지에 초점을 맞춰야 한다.
이후 소프트웨어의 유지보수 단계를 거치면서 새로운 기능을 추가할 필요가 생긴다.
만약 사용자의 요구사항이 더 이상 추가되거나 변경되지 않는다면, 설계를 위해 이렇게 골치를 썩일 필요가 없다.
더 이상 코드를 보지 않으면 되기 때문이다.
불행하게도 요구사항은 변경된다.
모든 소프트웨어에서 예외 없는 유일한 규칙이다.
개발자의 삶이 고단하면서 흥미로운 이유는 요구사항이 예측 불가능하게 변경되기 때문이다.
요구사항을 만족하는 다양한 설계안을 저울질하며, 단순하면서 유연한 설계를 창조하는 것은 공학보다는 예술에 가깝다
훌륭한 설계자는 미래에 구체적으로 어떤 변경이 발생할 것인지를 예측하지 않는다.
단지 언젠가는 변경이 발생할 것이며 아직은 그것이 무엇인지 모른다는 사실을 겸허하게 받아들인다.
전통적인 기능 분해는 자주 변경되는 기능을 중심으로 설계한 후 구조가 기능에 따르게 한다.
이것이 바로 기능 분해 방법이 변경에 취약한 이유다.
기능이 변경될 경우 기능의 축을 따라 설계된 소프트웨어가 전체적으로 요동치게 된다.
이에 비해 OOP는 자주 변경되지 않는 안정적인 객체 구조를 바탕으로, 시스템 기능을 객체 간의 책임으로 분배한다.
객체의 구조에 집중하고 기능이 객체의 구조를 따르게 만든다.
시스템 기능은 더 작은 책임으로 분할되고 적절한 객체에 분배되기 때문에 기능이 변경되더라도 객체 간의 구조는 유지된다.
이것이 객체를 기반으로 책임과 역할을 식별하고, 메시지를 기반으로 객체들의 협력 관계를 구축하는 이유이다.
1.2. 두 가지 재료: 기능과 구조
객체지향 세계를 구축하기 위해서는 기능과 구조라는 재료가 준비되어 있어야 한다.
기능은 사용자가 자신의 목표를 달성하기 위해 사용하는 시스템의 서비스다.
구조는 시스템의 기능을 구현하기 위한 기반으로, 기능 변경을 수용할 수 있도록 안정적이어야 한다.
일반적으로 기능을 수집하고 표현하기 위한 기법 을 ‘유스케이스 모델링’이라고 하고
구조를 수집하고 표현하기 위한 기법 을 ‘도메인 모델링’이라고 한다.
1.3. 안정적인 재료: 구조
‘병원’은 환자의 진료 기록을 보관하고 분석하기 위해 소프트웨어를 사용한다.
‘은행’은 고객의 소중한 자산을 관리하고 보호하기 위해 소프트웨어를 사용한다.
소프트웨어를 사용하는 사람들은 문제를 해결하기 위해 소프트웨어를 사용한다.
이처럼 사용자가 프로그램을 사용하는 대상 분야를 도메인이라고 한다.
도메인 모델에서 모델이란 대상을 추상화하고 단순화해서 표현한 것이다.
모델은 복잡성을 관리하기 위해 사용하는 기본적인 도구다.
은행 업무에 종사하는 사람들은 은행 도메인을 고객과 계좌 사이의 돈의 흐름으로,
중고 자동차 판매상은 구매되는 자동차와 판매되는 자동차의 교환으로 도메인을 바라본다.
도메인 모델은 디자이너, 사용자, 시스템과 같은 이해관계자들이 바라보는 멘탈 모델이며,
멘탈 모델은 다음과 같이 분류할 수 있다.
- 디자인 모델 (디자이너)
- 사용자 모델 (사용자)
- 시스템 이미지 (시스템 == 서비스)
사용자의 모델과 디자인 모델이 동일하다면 이상적이겠지만,
사용자와 디자이너는 직접적으로 상호작용할 수 없으며
단지 최종 제품인 시스템 그 자체를 통해서만 의사소통할 수 있다.
따라서 설계자는 디자인 모델을 기반으로 만든 시스템 이미지가
사용자 모델을 정확하게 반영하도록 노력해야 한다.
도메인 모델은 도메인에 대한 사용자 모델, 디자인 모델, 시스템 이미지를 포괄하도록 추상화한 소프트웨어 모델이다.
1.4. 도메인의 모습을 담을 수 있는 객체지향
최종 제품은 사용자의 관점을 반영해야 하며,
최종 코드는 사용자가 도메인을 바라보는 관점을 반영해야 한다.
곧 애플리케이션이 도메인 모델을 기반으로 설계돼야 한다는 것을 의미한다.
도메인 모델의 세 가지 측면을 모두 모델링할 수 있는 유사한 모델링 패러다임을 사용할수록 소프트웨어 개발이 쉬워질 것이다.
객체지향은 이런 요구사항을 가장 범용적으로 만족시킬 수 잇는 거의 유일한 모델링 패러다임이다.
객체지향 패러다임은 사용자의 관점, 설계자의 관점, 코드의 모습을
모두 유사한 형태로 유지할 수 있게 하는 유용한 사고 도구와 프로그래밍 기법을 제공한다.
결과적으로 객체지향을 이용하면 도메인에 대한 사용자 모델, 디자인 모델 시스템 이미지 모두가 유사한 모습을 유지하도록 만들 수 있다.
1.5. 표현적 차이
소프트웨어의 객체는 현실 객체에 대한 추상화가 아니다. 은유다.
(like 앨리스 이야기의 ‘트럼프 인간’)
대부분의 소프트웨어 도메인은 현실에 존재하지 않는 가상의 세계를 대상으로 한다.
게임 도메인은 현실에는 존재하지 않는 강력한 마법과 괴물들의 천국이다.
우리가 은유를 통해 투영해야 하는 대상은 무엇일까?
사용자가 도메인에 대해 생각하는 개념인 도메인 모델을 은유한다.
1.6. 불안정한 기능을 담는 안정적인 도메인 모델
사용자들은 도메인을 구성하는 중요한 개념과 개념 간의 관계를 가장 잘 알고 있는 사람들이다.
소프트웨어 개발의 가장 큰 적은 ‘변경’이며, 변경은 항상 발생한다.
사용자 모델에 포함된 개념과 규칙은 비교적 변경될 확률이 적기 때문에,
사용자 모델을 기반으로 설계와 코드를 만들면 변경에 쉽게 대처할 가능성이 커진다.
안정적인 구조를 제공하는 도메인 모델을 기반으로 소프트웨어의 구조를 설계하면
변경에 유연하게 대응할 수 있는 탄력적인 소프트웨어를 만들 수 있다.
도메인 모델은 여러분이 기능을 구현할 때 참조할 수 있는 궁극적인 지도다.
1.7. 느낀 점
소프트웨어 개발에서 요구사항 변경은 불가피하고,
이러한 변경에 **'대비'**하여 얼마나 범용적인 구조를 설계하느냐가 중요하다는 것을 깨닫게 되었고,
OOP가 이러한 경우 아주 적합한 범용적인 모델링 패러다임이라는 사실에 대해 알 수 있었습니다.
그리고 도메인 모델과 유스케이스 모델에 대한 내용이 나왔는데
내용이 잘 와닿지 않네요..
1.8. 논의 사항
여러분이 생각하는 6장에서 가장 중요한 키워드는 무엇인가요?
2. Chapter 07. 함께 모으기
코드와 모델을 밀접하게 연관시키는 것은 코드에 의미를 부여하고 모델을 적절하게 한다.
객체지향 설계 안에 존재하는 세 가지 상호 연관된 관점에 대해 각각 다음과 같이 나눌 수 있다.
- 개념 관점
- 명세 관점
- 구현 관점
개념 관점은 도메인 안에 존재하는 개념과 개념들 사이의 관계를 표현한다.
이 관점은 사용자가 도메인을 바라보는 관점을 반영한다.
명세 관점은 사용자의 영역인 도메인을 벗어나 개발자의 영역인 소프트웨어로 초점이 옮겨진다.
도메인의 개념이 아니라, 실제로 소프트웨어 안에서 살아 숨 쉬는 객체들의 책임에 초점을 맞추게 된다.
즉, 객체의 인터페이스를 바라보게 되며, 객체가 협력을 위해 '무엇'을 할 수 있는가에 초점을 맞춘다.
구현 관점은 프로그래머인 우리에게 가장 익숙한 관점으로, 실제 작업을 수행하는 코드와 연관돼 있다.
초점은 객체들이 책임을 수행하는 데 필요한 동작하는 코드를 작성하는 것이다.
'어떻게' 수행할 것인가에 초점을 맞추면 인터페이스를 '구현'하는 데 필요한 속성과 메서드 클래스에 추가한다.
2.1. 커피 전문점을 객체지향으로 설계하기
객체지향 설계의 첫 번째 목표는 훌륭한 객체를 설계하는 것이 아니라 훌륭한 협력을 설계하는 것이다.</p> 훌륭한 객체는 훌륭한 협력을 설계할 때만 얻을 수 있다.
2.1.1. 커피를 주문하기 위한 협력 찾기
현재 설계하고 있는 협력은 커피를 주문하는 것이다.
아마도 첫 번째 메시지는 ‘커피를 주문하라’일 것이다.
메시지를 찾았으니 이제 메시지를 처리하기에 적합한 객체를 선택해야 한다.
메시지를 처리할 객체를 찾고 있다면 먼저 도메인 모델 안에 책임을 수행하기에 적절한 타입이 있는지 살펴보라.
‘커피를 주문하라’라는 메시지를 수신할 객체는 당연히 ‘손님’일 것이다.
따라서 메시지를 처리할 객체는 손님 타입의 인스턴스다.
그 다음 손님이 커피를 주문하는 도중에 스스로 할 수 없는 일이 무엇인지 생각해 보자.
손님은 메뉴 항목에 대해서 알지 못한다.
따라서 ‘메뉴 항목을 찾아라’라는 새로운 메시지가 등장한다.
이 경우 ‘메뉴 이름’이라는 인자를 포함해 함께 전송하고
메시지를 수신한 객체는 ‘메뉴 이름에 대응되는 ‘메뉴 항목’을 반환한다.
이 메시지를 전달받을 객체는 ‘메뉴판’이 될 것이다.
현실 세계에서 메뉴판은 수동적인 존재이지만, 객체지향 세계에서는 능동적이고 자율적인 존재이다.
메뉴판은 자기 스스로 메뉴 항목을 찾는다. 따라서 메뉴판을 생물처럼 ‘의인화’한다.
이렇게 손님과 메뉴판 사이의 협력 관계가 형성된다.
손님은 이제 메뉴 항목을 얻었으니, 항목에 맞는 커피를 제조해 달라고 요청할 수 있다.
요청은 새로운 메시지의 등장 신호이다.
손님은 메뉴 항목을 전달하고 반환 값으로 제조된 커피를 반환받는다.
누가 커피를 제조해야 하는가? 당연히 바리스타이다.
바리스타는 메뉴에 있는 모든 항목에 대한 제조법을 알고 있을 것이다.
그러한 지식과 기술은 바리스타 객체의 ‘상태’와 ‘행동’으로 간주할 수 있다.
그 이후 커피 주문을 위한 협력은 ‘커피를 생성하라’라는 메시지와
메시지를 수신하는 ‘커피’ 객체를 끝으로 마무리된다.
출저 : https://techblog.woowahan.com/2502/
의사소통이라는 목적에 부합한다면 용도에 맞게 얼마든지 UML(Unified Modeling Language)을 수정하고 뒤틀어라 UML은 의사소통을 위한 표기법이지 꼭 지켜야 하는 법칙이 아니다.
2.1.2. 인터페이스 정리하기
출저 : https://techblog.woowahan.com/2502/
우리가 힘들게 얻어낸 것은 객체들의 인터페이스이다. 객체가 수신한 메시지가 객체의 인터페이스를 결정한다.
2.1.3. 구현하기
클래스의 인터페이스를 식별했으므로 이제 메서드로 구현하자.
객체가 다른 객체에게 메시지를 전송하기 위해서는 먼저 객체에 대한 참조를 얻어야 한다.
따라서 Customer 객체는 어떤 방법으로든 자신과 협력하는 Menu 객체와 Barista 객체에 대한 참조를 알고 있어야 한다.
출저 : https://techblog.woowahan.com/2502/
구현하지 않고 머릿속으로만 구상한 설계는 코드로 구현하는 단계에서 대부분 변경된다.
최대한 빨리 코드를 구현해서 설계에 이상이 없는지, 설계가 구현할 수 있지를 판단해야 한다.
코드를 통한 피드백 없이는 깔끔한 설계를 얻을 수 없다.
그리고 order() 메서드의 구현을 채우는 것뿐이다.
출저 : https://techblog.woowahan.com/2502/
Menu는 menuName에 해당하는 MenuItem을 찾아야 하는 ‘책임’이 있다.
이 책임을 수행하기 위해서는 Menu가 내부적으로 MenuItem을 관리하고 있어야 한다.
(Menu의 생성자에서 메뉴항목 리스트를 입력받는다)
출저 : https://techblog.woowahan.com/2502/
이후 Barista는 MenuItem을 이용해서 커피를 제조하고
출저 : https://techblog.woowahan.com/2502/
Coffee는 자기 자신을 생성하기 위한 생성자를 제공한다.
Coffee 인스턴스는 전달받은 MenuItem에 부합하는 ‘커피 이름’과 ‘커피 가격’을 ‘상태’로 가지고 있다.
출저 : https://techblog.woowahan.com/2502/
그 과정에서 MenuItem 클래스는 getName()
과 cost()
메시지에 응답할 수 있도록 메서드를 구현해야 한다.
출저 : https://techblog.woowahan.com/2502/
커피 전문점 코드를 클래스 다이어그램으로 나타낸 것이다.
출저 : https://techblog.woowahan.com/2502/
2.2. 코드와 세 가지 관점
개념 관점에서 코드를 바라보면 Customer, Menu, MenuItem, Barista, Coffee 클래스가 보인다.
이 클래스들을 자세히 보면 커피 전문점 도메인을 구성하는 중요한 개념과 관계를 반영한다는 사실을 쉽게 알 수 있다.
소프트웨어 클래스와 도메인 클래스 사이의 간격이 좁을수록 기능을 변경하기 위해 뒤적거려야 하는 코드의 양도 줄어든다.
명세 관점에서 클래스의 인터페이스를 바라본다.
클래스의 public 메서드는 다른 클래스가 협력할 수 있는 공용 인터페이스를 드러낸다.
인터페이스를 수정하면 해당 객체와 협력하는 모든 객체에게 영향을 미칠 수밖에 없다.
또한 변화에 탄력적인 인터페이스를 만들기 위해 구현과 관련된 세부 사항이 드러나지 않게 해야 한다.
이는 객체지향 설계자의 수준을 가늠하는 중요한 척도다.
객체의 인터페이스는 수정하기 어렵다는 사실을 명심하라
구현 관점은 클래스의 내부 구현을 바라보며, 클래스의 메서드와 속성은 구현에 속하며 공용 인터페이스의 일부가 아니다.
따라서 메서드의 구현과 속성의 변경은 원칙적으로 외부의 객체에게 영향을 미쳐서는 안 된다.
즉 메서드와 속성이 철저하게 클래스 내부로 캡슐화돼야 한다는 것을 의미한다.
개념 관점, 명세 관점, 구현 관점은 동일한 코드를 바라보는 서로 다른 관점이다.
코드를 읽으면서 세 가지 관점을 쉽게 포착하지 못한다면,
세 가지 관점이 명확하게 드러날 수 있게 코드를 개선하라.
그것이 변경에 유연하게 대응할 수 있는 객체지향 코드를 작성하는 가장 빠른 길이다.
2.3. 인터페이스와 구현을 분리하라
인터페이스와 구현을 분리하라.
명세 관점과 구현 관점이 뒤섞여 여러분의 머릿속을 함부로 어지럽히지 못하게 하라.
명세 관점은 클래스의 안정적인 측면(구조)을 드러내야 하며,
구현 관점은 클래스의 불안정한 측면(기능)을 드러내야 한다.
인터페이스가 구현 세부 사항을 노출하기 시작하면 아주 작은 변동에도 전체 협력이 요동치는 취약한 설계를 얻을 수밖에 없다.
프로그래머 입장에서 가장 많이 접하게 되는 것은 코드이므로,
구현 관점을 가장 빈번하게 사용하겠지만,
실제로 훌륭한 설계를 결정하는 것은 명세 관점인 객체의 인터페이스이다.
이는 설계의 품질을 결정하는 중요한 요소임을 명심하라.
무엇보다 중요한 것은 클래스를 봤을 때, 클래스를 명세 관점과 구현 관점으로 나눠볼 수 있어야 한다는 것이다.
2.4. 느낀점
이 책은 역할
, 책임
, 협력
의 키워드를 중심으로 객체지향을 설명했고,
객체지향에 대한 사실과 나의 오해에 대해 알 수 있었다.
객체지향에 대한 개념이 잡혀가면서도, 막상 설계를 해보려고 적용이 잘 되지 않고
그저 그전에 하던 방식대로 (객체의 상태를 먼저 정의하고, 협력관계를 잘 생각하지 않는) 코드를 짰다.
그렇게 낙담하고 있었는데 이번 7장을 계기로 그동안 글쓴이가 설명했던 객체지향에 대한 철학들과 개념들이
맞물리면서 좀 더 와닿도록 객체지향 설계를 이해하는 것 같다는 느낌이 들었다.
7장에서 등장했던 커피 전문점의 ‘협력 관계의 구상’, ‘인터페이스 설계’, ‘메서드 구현’ 단계를 거치면서
아주 단순한 객체지향 설계이지만 그동안 배운 개념들이 적절히 녹아든 예시라고 생각한다.
2.5. 스터디 마무리
이 책을 마무리하면서 가장 기억에 남는 점은 여러 비유다.
어떠한 개념을 그저 글로만 설명하는 데에는 한계가 있는데,
이 책에서는 커피 전문점
, 이상한 나라의 앨리스
, 지하철 노선도
, 객체 지도
, 등 다양한 비유가 등장할 때마다
나를 아주 명료하게 이해시켜 주었다.
내가 객체지향을 객체지향답게 바라볼 수 있게 만들어준 책인 것 같아 너무 고맙다.
그리고 이 책을 계기로 책을 통해 내 프로그래밍을 바라보는 시야를 확장하는 시간도 종종 가져야겠다고 생각했다.
다음 북클럽 스터디도 참가해서 책 읽는 프로그래머라는 좋은 습관을 계속 이어 나가고 싶다.
2.6. 논의 사항
평소 설계할 때 UML을 자주 이용하시나요?
댓글남기기