오브젝트 7장 객체 분해
7장 객체 분해 #
서론 #
- 사람의 기억은 단기 기억과 장기 기억으로 분류된다.
- 장기기억은 경험한 내용을 수개월에서 길게는 영구적으로 보관하는 저장소이다.
- 일반적으로 장기기억 안에 보관되어 있는 지식은 직접 접근하는 것이 불가능하고 먼저 단기 기억 영역으로 옮긴 후 처리해야 한다.
- 반면, 단기 기억은 보관되어 있는 지식에 직접 접근할 수 있지만 정보를 보관할 수 있는 속도와 공간적인 측면 모두에서 제약을 받는다.
- 조지 밀러의 매직넘버7의 규칙에 따르면 동시에 단기 기억안에 저장할 수 있는 정보는 5~9개 뿐이다.
- 핵심은 실제로 문제를 해결하기 위해 사용되는 저장소는 장기 기억이 아니라 단기기억이라는 점이다.
- 문제를 해결하기 위해서는 필요한 정보들을 먼저 단기기억으로 불러들여야 한다.
- 그러나 문제 해결에 필요한 요소의 수가 단기기억의 용량을 초과하는 순간 문제 해결 능력은 급격하게 떨어지고 만다.
- 이런 현상을 인지 과부하라고 한다.
- 인지 과부하를 방지하는 좋은 방법은 단기기억 안에 보관할 정보의 양을 조절하는 것이다.
- 한 번에 다룰 정보의 양을 줄인다.
- 이처럼 불필요한 정보를 제거하고 문제 해결에 필요한 핵심만을 남기는 작업을 추상화라고 한다.
- 가장 일반적인 추상화 방법은 한 번에 다뤄야 할 문제의 크기를 줄이는 것이다.
- 큰 문제를 해결 가능한 작은 문제로 나누는 작업을 분해 라고 부른다.
- 분해의 목적은 큰 문제를 인지 과부하의 부담 없이 단기 기억 안에서 한 번에 처리할 수 있는 규모의 문제로 나누는 것이다.
- 한 번에 단기 기억에 담을 수 있는 추상화 수에는 한계가 있지만 추상화를 더 큰 규모의 추상화로 압축시킴으로써 단기 기억의 한계를 초월할 수 있다.
- 따라서 추상화와 분해는 인간이 세계를 인식하고 반응하기 위해 사용하는 기본적인 사고 도구이다.
- 복잡성이 존재하는 곳에 추상화와 분해 역시 존재한다.
01 프로시저 추상화와 데이터 추상화 #
- 프로그래밍 언어의 발전은 좀 더 효과적인 추상화를 이용해 복잡성을 극복하려는 개발자들의 노력에서 출발했다.
- 언어를 통해 표현되는 추상화의 발전은 다양한 프로그래밍 패러다임으로 이어진다.
- 프로그래밍 패러다임이란 적절한 추상화의 윤곽을 따라 시스템을 어떤 식으로 나눌 것인지를 결정하는 원칙과 방법의 집합이다.
- 패러다임은 프로그래밍을 구성하기 위해 사용하는 추상화의 종류와 이 추상화를 이용해 소프트웨어를 분해하는 방법의 두 가지 요소로 결정된다.
- 따라서 모든 프로그래밍 패러다임은 추상화와 분해 관점에서 설명할 수 있다.
- 현대 프로그래밍 언어를 특징 짓는 중요한 추상화 매커니즘 두 가지
- 프로시저 추상화는 소프트웨어가 무엇을 해야 하는지를 추상화한다.
- 데이터 추상화는 소프트웨어가 무엇을 알아야 하는지를 추상화한다.
- 소프트웨어는 데이터를 이용해 정보를 표현하고 프로시저를 이용해 조작한다.
- 시스템 분해 방법을 결정하려면 프로시저 추상화를 중심으로 할 것인지, 데이터 추상화를 중심으로 할 것인지를 결정해야 한다.
- 프로시저 추상화를 중심으로 시스템을 분해하기로 결정했다면 기능 분해(알고리즘 분해)의 길로 들어서는 것이다.
- 데이터 추상화를 중심으로 시스템을 분해한다면 두 가지 방법 중 선택해야 한다.
- 데이터를 중심으로 타입 추상화 - 추상 데이터 타입
- 데이터를 중심으로 프로시저를 추상화 - 객체지향
- 지금까지 객체지향 패러다임을 역할과 책임을 수행하는 자율적인 객체들의 협력 공동체를 구축하는 것으로 설명했다.
- 여기서 ‘역할과 책임을 수행하는 자율적인 객체’가 객체지향 패러다임이 이용하는 추상화이다.
- ‘협력하는 공동체’를 구성하도록 객체를 나누는 과정이 바로 객체지향 패러다임의 분해에 해당한다.
- 언어 관점에서 객체지향을 바라보면, 기능을 구현하기 위해 필요한 객체를 식별하고 협력 가능하도록 시스템을 분해한 후에는 프로그래밍 언어라는 수단을 이용해 실행 가능한 프로그램을 구현해야 한다.
- 객체지향이란 데이터를 중심으로 데이터 추상화와 프로시저 추상화를 통합한 객체를 이용해 시스템을 분해하는 방법이다.
- 이런 객체를 구현하기 위해 대부분 클래스를 사용한다.
- 따라서 프로그래밍 언어 관점에서 객체지향을 바라보는 일반적인 관점은 데이터 추상화와 프로시저 추상화를 함께 포함한 클래스를 이용해 시스템을 분해하는 것.
02. 프로시저 추상화와 기능 분해 #
메인 함수로서의 시스템 #
- 기능은 과거 오랜시간 동안 시스템을 분해하기 위한 기준으로 사용 되었음.
- 프로시저는 반복적으로 실행되거나 거의 유사하게 실행되는 작업들을 하나의 장소에 모아놓음으로써 로직을 재사용하고 중복을 방지할 수 있는 추상화 방법이다.
- 프로시저를 추상화라고 부르는 이유는 내부의 상세한 구현 내용을 모르더라도 인터페이스만 알면 사용할 수 있기 때문이다.
- 프로시저는 잠재적으로 정보은닉의 가능성을 제시하지만 뒤에서 살펴보는 것처럼 프로시저만으로 효과적인 정보은닉 체계를 구축하는 데는 한계가 있다.
- 전통적인 기능 분해 방법은 하향식 접근법을 따른다.
- 하향식 접근법이란 시스템을 구서앟는 가장 최상위 기능을 정의하고, 이 최상위 기능을 좀 더 작은 단계의 하위 기능으로 분해해 나가는 방법을 말한다.
- 분해는 세분화된 마지막 하위 기능이 프로그래밍 언어로 구현 가능한 수준이 될 때까지 계속된다.
- 각 세분화 단계는 바로 위 단계보다 더 구체적이어야 한다.
- 상위 기능은 하나 이상의 더 간단하고 더 구체적이며 덜 추상적인 하위 기능의 집합으로 분해된다.
급여 관리 시스템 예시 #
-
기능 분해 방법에서는 기능을 중심으로 필요한 데이터를 결정한다.
- 기능분해라는 무대의 주연은 기능이며 데이터는 기능을 보조하는 조연의 역할에 머무른다.
- 이는 유지보수에 다양한 문제점을 야기한다.
- 하향식 기능 분해 방식이 가지는 문제점을 이해하는 것이 객체지향의 장점을 이해할 수 있는 좋은 출발점.
-
하향식 기능 분해는 시스템을 최상위의 가장 추상적인 메인 함수로 정의하고, 메인 함수를 구현 가능한 수준까지 세부적인 단계로 분해하는 방법이다.
- 하향식 기능 분해는 논리적이고 체계적인 시스템 개발 절차를 제시한다.
- 커다란 기능을 좀 더 작은 기능으로 단계적으로 정제해 가는 과정은 구조적이며 체계적인 동시에 이상적인 방법으로까지 보일 것이다.
- 문제는 우리가 사는 세계는 그렇게 체계적이지도, 이상적이지도 않다는 점이다.
- 체계적이고 이상적인 방법이 불규칙하고 불완전한 인간과 만나는 지점에서 혼란과 동요가 발생한다.
하향식 기능 분해의 문제점 #
- 문제점
- 시스템은 하나의 메인 함수로 구성돼 있지 않다.
- 기능 추가나 요구사항 변경으로 인해 메인 함수를 빈번하게 수정해야 한다.
- 비즈니스 로직이 사용자 인터페이스와 강하게 결합된다.
- 하향신 분해는 너무 이른 시기에 함수들의 실행 순서를 고정시키기 때문에 유연성과 재사용성이 저하된다.
- 데이터 형식이 변경될 경우 파급효과를 에측할 수 없다.
- 설계는 코드 배치 방법이며 설계가 필요한 이유는 변경에 대비하기 위한 것이라는 점을 기억하라.
- 변경은 성공적인 소프트웨어가 맞이해야 하는 피할 수 없는 운명이다.
- 현재의 요구사항이 변하지 않고 코드를 변경할 필요가 없다면 소프트웨어를 어떻게 설계하던 아무도 신경쓰지 않을 것이다.
- 하지만 설계는 변경된다.
하나의 메인 함수라는 비현실적인 아이디어 #
- 어떤 시스템도 최초에 릴리즈됐던 당시의 모습을 그대로 유지하지는 않는다.
- 시간이 지나고 사용자를 만족시키기 위한 새로운 요구사항을 도출해나가면서 지속적으로 새로운 기능을 추가하게 된다.
- 이것은 시스템이 오직 하나의 메인 함수만으로 구현된다는 개념과는 완전히 모순된다.
- 대부분의 경우 추가되는 기능은 최초에 배포된 메인 함수의 일부가 아닐 것이다.
- 결국 처음에는 중요하게 생각됐던 메인 함수는 동등하게 중요한 여러 함수들 중 하나로 전락하고 만다.
- 어느 시점에 이르면 유일한 메인함수라는 개념은 의미 없어지고 시스템은 여러 개의 동등한 수준의 함수 집합으로 성장하게 될 것이다.
- 대부분의 시스템에서 하나의 메인 기능이란 개념은 존재하지 않는다.
- 모든 기능들은 규모라는 측면에서 차이가 있을 수는 있겠지만 기능성의 측면에서는 동등하게 독립적이고 완결된 하나의 기능을 표현한다.
- 하향식 접근법은 하나의 알고리즘을 구현하거나 배치 처리를 구현하기에는 적합하지만 현대적인 상호작용 시스템을 개발하는 데는 적합하지 않다.
메인 함수의 빈번한 재설계 #
- 시스템 안에는 여러 개의 정상이 존재하기 때문에 결과적으로 하나의 메인 함수를 유일한 정상으로 간주하는 하향식 기능 분해의 경우 새로운 기능을 추가할 때마다 매번 메인 함수를 수정해야 한다.
- 기존 로직과는 아무런 상관없는 새로운 함수의 적절한 위치를 확보해야 하기 때문에 메인 함수의 구조를 급격하게 변경할 수 밖에 없다.
- 기존 코드를 수정하는 것은 항상 새로운 버그를 만들어낼 확률을 높인다.
비즈니스 로직과 사용자 인터페이스의 결합 #
- 하향식 접근법은 비즈니스 로직을 설계하는 초기 단계부터 입력 방법과 출력 양식을 함께 고민하도록 강요한다.
- 결과적으로 코드 안에서 비즈니스 로직과 사용자 인터페이스 로직이 밀접하게 결합된다.
- 문제는 비즈니스 로직과 사용자 인터페이스가 변경되는 빈도가 다르다.
- 당연히 사용자 인터페이스는 자주 변경되고, 반면 비즈니스 로직은 비교적 변경이 적게 발생한다.
- 하향식 접근법은 사용자 인터페이스 로직과 비즈니스 로직을 한데 섞기 때문에 사용자 인터페이스를 변경하는 경우 비즈니스 로직까지 변경에 영향을 받게 된다.
- 따라서 하향식 접근법은 근본적으로 변경에 불안정한 아키텍처를 낳는다.
- 하향식 접근법은 기능을 분해하는 과정에서 사용자 인터페이스의 관심사와 비즈니스 로직의 관심사를 동시에 고려하도록 강요하기 때문에 ‘관심사의 분리’라는 아키텍처 설게의 목적을 달성하기 어렵다.
성급하게 결정된 실행 순서 #
- 하향식으로 기능을 분해하는 과정은 하나의 함수를 더 작은 함수로 분해하고, 분해된 함수들의 실행 순서를 결정하는 작업으로 요약할 수 있다.
- 이는 설계를 시작하는 시점부터 시스템이 무엇을 해야 하는지가 아니라 어떻게 동작해야 하는지에 집중하도록 만든다.
- 하향식 접근법은 처음부터 구현을 염두에 두기 때문에 자연스럽게 함수들의 실행순서를 정의하는 시간제약을 강조한다.
- 메인 함수가 작은 함수들로 분해되기 위해서는 우선 함수들의 순서를 결정해야 한다.
- 실행 순서나 조건, 반복과 같은 제어구조를 미리 결정하지 않고는 분해를 진행할 수 없기 때문에 기능 분해 방식은 중앙집중 제어 스타일의 형태를 띨 수 밖에 없다.
- 결과적으로 모든 중요한 제어 흐름의 결정이 상위 함수에서 이뤄지고 하위 함수는 상위 함수의 흐름에 따라 적절한 시점에 호출된다.
- 문제는 함수의 제어 구조가 빈번한 변경의 대상이라는 점이다.
- 기능을 추가하거나 변경하느 작업은 기존에 결정된 함수의 제어구를 변경하게 만든다.
- 이를 해결하기 위한 한 가지 방법은 자주 변경되는 시간적인 제약에 대한 미련을 버리고 좀 더 안정적인 논리적인 제약을 설계의 기준으로 삼는 것이다.
- 객체지향은 함수 간의 호출 순서가 아니라 객체 사이의 논리적인 관계를 중심으로 설계를 이끌어 나간다.
- 결과적으로 전체적인 시스템은 어떤 한 구성요소로 제어가 집중되지 않고 여러 객체들 사이로 제어 주체가 분산된다.
- 하향식 접근법을 통해 분해한 함수들은 재사용하기도 어렵다.
- 모든 함수는 상위 함수를 분해하는 과정에서 필요에 따라 식별되며, 상위 함수가 강요하는 문맥 안에서만 의미를 가지기 때문이다.
- 재사용이라는 개념은 일반성이라는 의미를 포함한다.
- 함수가 재사용 가능하려면 상위 함수보다 더 일반적이어야 한다.
- 하지만 하향식 접근법을 따를 경우 분해된 하위 함수는 항상 상위 함수보다 문맥에 더 종속적이다.
- 이는 정확하게 재사용성과 반대되는 개념임.
- 하향식 설게와 관련된 모든 문제의 원인은 결합도다.
- 함수는 상위 함수가 강요하는 문맥에 강하게 결합된다.
데이터 변경으로 인한 파급효과 #
- 하향식 기능 분해의 가장 큰 문제점은 어떤 데이터를 어떤 함수가 사용하고 있는지를 추적하기 어렵다.
- 따라서 데이터 변경으로 인해 어떤 함수가 영향을 받을지 예상하기 어렵다.
- 이는 의존성과 결합도의 문제다.
- 데이터의 변경으로 인한 영향은 데이터를 직접 참조하는 모든 함수로 퍼져나간다.
- 모든 함수를 분석해서 영향도를 파악하고 변경될 전역 변수에 의존하는 함수를 찾는것은 어려운 일.
- 코드가 성장하고 라인 수가 증가할수록 전역 데이터를 변경하는 것은 악몽으로 변해간다.
- 데이터 변경으로 인한 영향을 최소화하려면 데이터와 함께 변경되는 부분과 그렇지 않은 부분을 명확하게 분리해야 한다.
- 데이터와 함께 변경되는 부분을 하나의 구현 단위로 묶고 외부에서는 제공되는 함수만 이용해 데이터에 접근해야 한다.
- 즉, 잘 정의된 퍼블릭 인터페이스를 통해 데이터에 대한 접근을 통제해야 하는 것이다.
- 이것이 의존성 관리의 핵심이다.
- 변경에 대한 영향을 최소화하기 위해 영향을 받는 부분과 받지 않는 부분을 명확하게 분리하고 잘 정의된 퍼블릭 인터페이스를 통해 변경되는 부분에 대한 접근을 통제하라.
언제 하향식 분해가 유용한가? #
- 하향식 아이디어가 매력적인 이유는 설계가 어느 정도 안정화 된 후에는 설계의 다양한 측면을 논리적으로 설명하고 문서화하기 용이하기 때문이다.
- 그러나 설계를 문서화 하는 데 적절한 방법이 좋은 구조를 설계할 수 있는 방법과 동일한 것은 아니다.
- 마이클 잭슨(개발자)의 하향식 방법 설명
- 하향식은 이미 완전히 이해된 사실을 서술하기에 적합한 방법이다.
- 그러나 하향식은 새로운 것을 개발하고, 설계하고, 발견하는 데는 적합한 방법이 아니다.
- 시스템이나 프로그램 개발자가 이미 완료한 결과에 대한 명확한 아이디어를 가지고 있다면 머릿속에 있는 것을 종이에 서술하기 위해 하향식을 사용할 수 있다.
- 이것이 사람들이 하향식 설계나 개발을 할 수 있고, 그렇게 함으로써 성공할 수 있다고 믿게 만드는 이유다.
- 하향식 단계가 시작될 때 문제는 이미 해결됐고, 오직 해결돼야 하는 세부사항만이 존재할 뿐이다.
03. 모듈 #
정보 은닉과 모듈 #
- 시스템 변경을 관리하는 기본적인 전략은 함께 변경되는 부분을 하나의 구현단위로 묶고 퍼블릭 인터페이스를 통해서만 접근하도록 만드는 것이다.
- 즉, 기능을 기반으로 시스템을 분해하는 것이 아니라 변경의 방향에 맞춰 시스템을 분해하는 것이다.
- 정보 은닉은 시스템을 모듈 단위로 분해하기 위한 기본 원리로 시스템에서 자주 변경되는 부분을 상대적으로 덜 변경되는 안정적인 인터페이스 뒤로 감춰야 한다는 것이 핵심이다.
- 시스템을 모듈로 분할하는 원칙은 외부에 유출돼서는 안 되는 비밀의 윤곽을 따라야 한다고 주장한다.
- 모듈과 기능 분해는 상호 배타적인 관계가 아니다.
- 시스템을 모듈로 분해한 후에는 각 모듈 내부를 구현하기 위해 기능 분해를 적용할 수 있다.
- 기능 분해가 하나의 기능을 구현하기 위해 필요한 기능들을 순차적으로 찾아가는 탐색의 과정이라면 모듈 분해는 감춰야 하는 비밀을 선택하고 비밀 주변에 안정적인 보호막을 설치하는 보존의 과정이다.
- 비밀을 결정하고 모듈을 분해한 후에는 기능 분해를 이용해 모듈에 필요한 퍼블릭 인터페이스를 구현할 수 있다.
- 모듈은 다음 두 가지 비밀을 감춰야 한다.
- 복잡성
- 모듈이 너무 복잡한 경우 이해하고 사용하기 어렵다.
- 외부에 모듈을 추상화할 수 있는 간단한 인터페이스를 제공해서 모듈의 복잡도를 낮춘다.
- 변경 가능성
- 변경 가능한 설계 결정이 외부에 노출될 경우 실제로 변경이 발생했을 때 파급효과가 커진다.
- 변경 발생 시 하나의 모듈만 수정하면 되도록 변경 가능한 설계 결정을 모듈 내부로 감추고 외부에는 쉽게 변경되지 않을 인터페이스를 제공한다.
- 복잡성
- 비밀이 반드시 데이터일 필요는 없다. 복잡한 로직이나 변경 가능성이 큰 자료 구조일 수도 있음.
모듈의 장점과 한계 #
- 장점
- 모듈 내부의 변수가 변경되더라도 모듈 내부에만 영향을 미친다.
- 비즈니스 로직과 사용자 인터페이스에 대한 관심사를 분리한다.
- 전역 변수와 전역 함수를 제거함으로써 네임스페이스 오염을 방지한다.
- 모듈은 기능이 아니라 변경의 정도에 따라 시스템을 분해하게 만든다.
- 각 모듈은 외부에 감춰야 하는 비밀과 관련성 높은 데이터와 함수의 집합이다.
- 따라서 모듈 내부는 높은 응집도를 유지한다.
- 모듈과 모듈 사이에는 퍼블릭 인터페이스를 통해서만 통신하므로 낮은 결합도를 유지한다.
- 한계
- 모듈은 인스턴스 개념을 제공하지 않음.
- 이 한계를 극복하기 위해 추상 데이터 타입이 나옴.
04. 데이터 추상화와 추상 데이터 타입 #
추상 데이터 타입 #
-
타입이란 변수에 저장할 수 있는 내용물의 종류와 변수에 적용될 수 있는 연산의 가짓수를 의미한다.
- 타입은 저장된 값에 대해 수해오딜 수 있는 연산의 집합을 결정하기 때문에 변수의 값이 어떻게 행동할 것이라는 것을 예측할 수 있게 한다.
-
리스코프는 프로시저 추상화를 보완하기 위해 데이터 추상화의 개념을 제안했다.
- 추상 데이터 타입은 추상 객체의 클래스를 정의한 것으로 추상 객체에 사용할 수 있는 오퍼레이션을 이용해 규정한다.
- 이는 오퍼레이션을 이용해 추상 데이터 타입을 정의할 수 있음을 의미한다.
- 추상 데이터 객체를 사용할 때 프로그래머는 오직 객체가 외부에 제공하는 행위에만 관심을 가지며 행위가 구현되는 세부적인 사항에 대해 무시한다.
- 객체가 저장소 내에서 어떻게 표현되는지와 같은 구현 정보는 오직 오퍼레이션을 어떻게 구현할 것인지에 집중할 때만 필요하다.
-
비록 추상 데이터 타입 정의를 기반으로 객체를 생성하는 것은 가능하지만 여전히 데이터와 기능을 분리해서 바라본다는 점에 주의하자.
- 추상 데이터 타입은 말 그대로 시스템의 상태를 저장할 데이터를 표현한다.
- 추상 데이터 타입으로 표현된 데이터를 이용해서 기능을 구현하는 핵심 로직은 추상 데이터 타입 외부에 존재한다.
- 추상 데이터 타입은 데이터에 대한 관점을 설계 표면으로 끌어올리기는 하지만 여전히 데이터와 기능을 분리하는 절차적인 설계의 틀에 갇혀 있다.
05. 클래스 #
클래스는 추상 데이터 타입인가? #
- 명확한 의미에서 추상데이터 타입과 클래스는 동일하지 않다.
- 클래스는 상속과 다형성을 지원한다.
- 추상 데이터 타입은 타입을 추상화한 것이고 클래스는 절차를 추상화한 것이다.
- 추상 데이터 타입은 오퍼레이션을 기준으로 타입을 묶고, 객체지향은 타입을 기준으로 오퍼레이션을 묶는다.
변경을 기준으로 선택하라 #
-
클래스가 추상 데이터 타입의 개념을 따르는지를 확인하는 간단한 방법은 클래스 내부에 타입을 표현하는 변수가 있는지를 살펴보는 것이다.
- 추상 데이터 타입으로 구현된 클래스(예제 코드)를 보면 hourly를 통해 직원의 유형을 유추한다. (hourly가 true면 아르바이트생, false면 정직원)
- 이처럼 인스턴스 변수에 저장된 값을 기반으로 메서드 내에서 타입을 명시적으로 구분하는 방식은 객체지향을 위반하는 것으로 간주된다.
-
객체지향에서는 타입 변수를 이용한 조건문을 다형성으로 대체한다.
- 클라이언트가 객체 타입을 확인한 후 적절한 메서드를 호출하는 것이 아니라 객체가 메시지를 처리할 적절한 메서드를 선택한다.
-
이처럼 기존 코드에 아무런 영향도 미치지 않고 새로운 객체 유형과 행위를 추가할 수 있는 객체지향의 특성을 개방-폐쇄 원칙이라고 부른다.
- 이것이 객체지향 설계가 전통적인 방식에 비해 변경하고 확장하기 쉬운 구조를 설계할 수 있는 이유다.
-
객체지향과 추상 데이터 타입 중 어느 방식으로 설계해야 하는가?
- 설계의 유용성은 변경의 방향성과 발생 빈도에 따라 결정된다.
- 타입 추가에 대한 변경의 압박이 강하다면 객체지향으로,
- 오퍼레이션 변경에 대한 압박이 강하다면 추상 데이터 타입이 유용하다.
- 변경의 축을 찾고 접근하자.