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