TL;DR

  • 추상화의 한 가지 방법인 분해의 역사를 다룹니다.
  • 프로시저 추상화와 데이터 추상화 사이의 갈등과 분쟁의 역사를 이해하면 기능 분해에서 시작해서 객체지향에 이르기까지 소프트웨어 패러다임의 변화를 자연스럽게 이해하게 될 것입니다.
  • 사람이 동시에 단기 기억 안에 저장할 수 있는 정보의 개수는 5개에서 많아 봐야 9개 정도를 넘지 못합니다. 새운 정보를 받아 들이는 데 5초 정도의 시간이 소요됩니다. 문제 해결에 필요한 요소의 수가 단기 기억의 용량을 초과하는 순간 문제 해결 능력은 급격하게 떨어집니다. 이런 현상을 인지 과부하(cognitive overload)라고 부릅니다.
  • 인지 과부하를 방지하는 가장 좋은 방법은 단기 기억 안에 보관할 정보의 양을 조절하는 것입니다. 한 번에 다뤄야 하는 정보의 수를 줄이기 위해 본질적인 정보만 남기고 불필요한 세부사항을 걸러내면 문제를 단순화할 수 있을 것입니다. 이처럼 불필요한 정보를 제거하고 현재의 문제 해결에 필요한 책심만 남기는 작업을 추상화라고 부릅니다.
  • 가장 일반적인 추상화 방법은 한 번에 다뤄야 하는 문제의 크기를 줄이는 것입니다. 사람들은 한 번에 해결하기 어려운 커다란 문제에 맞닥뜨릴 경우 해결 가능한 작은 문제로 나누는 경향이 있습니다. 이렇게 나워진 문제들 역시 한 번에 해결하기 어려울 정도로 크다면 다시 더 작은 문제로 나눌 수 있습니다. 이처럼 큰 문제를 해결 가능한 작은 문제로 나누느 작업을 분해(decomposition)라고 부릅니다.
  • 정보의 가장 작은 단위로서의 개별 항목을 의미하는 것이 아니라 하나의 단위로 취급될 수 있는 논리적인 청크(chunk)를 의미한다는 점입니다. 청크는 더 작은 청크를 포함할 수 있으며 연속적으로 분해 가능합니다.
  • 한 번에 단기 기억에 담을 수 있는 추상화의 수에는 한계가 있지만 추상화를 더 큰 규모의 추상화로 압축시킴으로써 단기 기억의 한계를 초절할 수 있습니다. 따라서 추상화의 분해와 인간이 세계를 인식하고 반응하기 위해 사용하는 가장 기본적인 사고 도구라고 할 수 있습니다. 복잡성이 존재하는 곳에 추상화와 분해 역시 함께 존재합니다. 따라서 추상화와 분해가 인류가 창조한 가장 복잡한 분야의 문제를 해결하기 위해 사용돼 왔다고 해도 놀잡지 않을 것입니다. 그 분야는 바로 소프트웨어 개발 영역 입니다.

01. 프로시저 추상화와 데이터 추상화

  • 프로그래밍 언어의 발전은 좀 더 효과적인 추상화를 이용해 복잡성을 극복하려는 개발자들의 노력에서 출발 했습니다. 어셈블리어는 숫자로 뒤범벅이 된 기계어에 인간이 이해할 수 있는 상징을 부여하려는 노력의 결과입니다. 고수준 언어는 기계적인 사고를 강요하는 낮은 수준의 명령어들을 탈피해서 인간의 눈높이에 맞는 기계 독집적이고 의미 있는 추상화를 제공하려는 시도의 결과였습니다.
  • 프로그래밍 언어를 통해 표현되는 추상화의 발전은 다양한 프로그래밍 패러다임의 탄생으로 이어졌습니다. 프로그래밍 패러다임은 프로그램은 프로그램을 구성하기 위해 사용하는 추상화의 종류와 이 추상화를 이용하는 소프트웨어를 분해하는 방법의 두 가지 요소로 결정됩니다. 따라서 모든 프로그래밍 패러다임은 추상화와 분해의 관점에서 설명할 수 있습니다.
  • 현대적인 프로그래밍 언어를 특정 짓는 중요한 두 가지 추상화 메커니즘은 프로시저 추상화(procedure abstraction)데이터 추상화(data abstraction)입니다.
    • 프로시저 추상화는 소프트웨어가 무엇을 해야 하는지를 추상화합니다.
    • 데이터 추상화는 소프트웨어가 무엇을 알아야 하는지를 추상화합니다.
    • 소프트웨어는 데이터를 이용해 정보를 표현하고 프로시저를 이용해 데이터를 조작합니다.
  • 프로그래밍 패러다임이란 적절한 추상화의 윤곽을 따라 시스템을 어떤 식으로 나눌 것인지를 결정하는 원칙과 방법의 집합 입니다. 따라서 현대의 설계 방법에 중요한 영향을 끼치는 프로그래밍 패러다임들은 프로시저 추상화나 데이터 추상화를 중심으로 시스템의 분해 방법을 설명합니다.
  • 시스템을 분해하는 방법을 결정하려면 먼저 프로시저 추상화를 중심으로 할 것인지, 데이터 추상화를 중심으로 할 것인지를 결정해야 합니다.
    • 프로시저 중심으로 시스템을 분해하기로 결정했다면 기능 분해(functional decomposition)의 길로 들어서는 것입니다. 기능 분해는 알고리즘 분해(algorithmic decomposition)라고 부르기도 합니다.
    • 데이터 추상화를 중심으로 시스템을 분해하기로 결정했다면 다시 두 가지 중 하나를 선택해야 합니다. 하나는 데이터를 중심으로 타입을 추상화(type abstraction)하는 것이고 다른 하나는 데이터 중심으로 프로시저를 추상화(procedure abstraction)하는 것입니다. 전자를 추상 데이터 타입(Abstract Data Type)이라고 부르고 후자를 객체지향(Object-Oriented)이라고 부릅니다.
  • 객체지향 패러다임을 역할과 책임을 수행하는 자율적인 객체들의 협력 공동체를 구축하는 것으로 설명했습니다. 여기서 ‘역할과 책임을 수행하는 객체’가 바로 객체지향 패러다임이 이용하는 추상화입니다. 기능을 ‘협력하는 공동체’를 구성하도록 객체들로 나누는 과정이 바로 객체지향 패러다임에서의 분해를 의미합니다.
  • 기능을 구현하기 위해 필요한 객체를 식별하고 협력 가능하도록 시스템을 분해한 후에는 프로그래밍 언어라는 수단을 이용해 실행 가능한 프로그램을 구현해야 합니다. 프로그래밍 언어의 관점에서 객체지향이란 데이터 중심으로 추상화와 프로시저 추상화를 통합한 객체를 이용해 시스템을 분해하는 방법입니다. 그리고 이런 객체를 구현하기 위해 대부분의 객체지향 언어는 클래스라는 도구를 제공합니다. 따라서 프로그래밍 언어적인 관점에서 객체지향을 바라보는 일반적인 관점은 데이터 추상화와 프로시저 추상화를 함께 포함한 클래스를 이용해 시스템을 분해하는 것입니다.
  • 복잡성을 극복하는 방법은 현재의 문제를 해결할 수 있는 효과적인 추상화 메커니즘과 분해 방법을 찾는 것임을 이해할 수 있습니다.

02. 프로시저 추상화와 기능 분해

메인 함수로서의 시스템

  • 기능과 데이터의 첫 번째 전쟁에서 신은 기능의 손을 들어 주었습니다. 기능은 오랜 시간 동안 시스템을 분해하기 위한 기준으로 사용됐으며, 이 같은 시스템 분해 방식을 알고리즘 분해 또는 기능 분해라고 부릅니다. 기능 분해의 관점에서 추상화의 단위는 프로시저이며 시스템은 프로시저를 단위로 분해됩니다.
  • 프로시저는 반복적으로 실행되거나 거의 유사하게 실행되는 작업들을 하나의 장소에 모아놓음으로써 로직을 재사용하고 중복을 방지할 수 있는 추상화 방법입니다. 프로시저를 추상화라고 부르는 이유는 내부의 상세한 구현 내용을 모르더라도 인터페이스만 알면 프로시저를 사용할 수 있기 때문입니다. 따라서 프로시저는 잠재적으로 정보은닉(information hiding)의 가능성을 제시하지만 뒤에서 살펴보는 것처럼 프로시저만으로 효과적인 정보은닉 체계를 구축하는 데는 한계가 있습니다.
  • 프로시저 중심의 기능 분해 관점에서 시스템은 입력 값을 계산해서 출력 값을 반환하는 수학의 함수와 동일합니다. 시스템은 필요한 더 작은 작업으로 분해될 수 있는 하나의 커다란 메인 함수 입니다.
  • 전통적인 기능 분해 방법은 하향식 접근법(Top-Down Approach)을 따릅니다. 하향식 접근법이란 시스템을 구성하는 가장 최상위(topmost) 기능을 정의하고, 이 최상위 기능을 좀 더 작은 단계의 하위 기능으로 분해해 나가는 방법을 말합니다. 분해는 세분화된 마지막 하위 기능이 프로그래밍 언어로 구현 가능한 수순이 될 때까지 나가는 방법을 말합니다. 분해는 세분화된 마지막 하위 기능이 프로그래밍 언어로 구현 가능한 수준이 될 때까지 계속됩니다. 각 세분화 단계는 바로 위 단계보다 데 구체적이어야 합니다. 다시 말해 정제된 기능은 자신의 바로 상위 기능보다 덜 추상적이어야 합니다. 상위 기능은 하나 이상의 더 간단하고 더 구체적이며 덜 추상적인 하위 기능의 집합을 분해됩니다.

급여 관리 시스템

  • 전통적으로 기능 분해 방법은 하향식 접근법을 따르며 최상위의 추상적인 함수 정의에서 출발해서 단계적인 절차를 따라 시스템을 구축합니다. 이때 최상위의 추상적인 함수 정의는 시스템의 기능을 표현하는 하나의 문장으로 나타내고, 이 문장을 구성하는 좀 더 세부적인 단계의 문장으로 분해해 나가는 방식을 따릅니다. 기능 분해의 초점은 하나의 문장으로 표현된 기능을 여러 개의 더 작은 기능으로 분해하는 것입니다.
  • 각 정제 단계는 이전 문장의 추상화 수준을 감소사켜야 합니다. 즉, 모든 문장이 정제 과정을 거치면서 하나 이상의 좀 더 단순하고 구체적인 문장들의 조합으로 분해돼야 합니다. 개발자는 각 단계에서 불완전하지고 좀 더 구체화될 수 있는 문장들이 남아있는지 검토합니다. 만약 좀 더 가능한 문장이 존재하면 동일한 과정을 거쳐 구현이 가능할 정도로 충분히 저수준의 문장이 될 때까지 기능을 분해해야 합니다.
  • 기능 분해의 결과는 최상위 기능을 수행하는 데 필요한 절차들을 실행되는 시간 순서에 따라 나열한 것입니다. 기본적으로 기능 분해는 책의 목차를 정리하고 그 안에 내용을 채워 넣는 것과 유사합니다.
  • 기능 분해 방법에서는 기능을 중심으로 필요하 데이터를 결정합니다. 기능 분해하는 무대의 주연은 기능이며 데이터는 기능을 보조하는 조연의 역할에 머무릅니다. 기능이 우선이고 데이터는 기능의 뒤를 따릅니다. 기능 분해를 위한 하향식 접근법은 먼저 필요한 기능을 생각하고 이 기능을 분해하고 정제하는 과정에서 필요한 데이터의 종류와 저장 방식을 식별합니다.
  • 하향식 기능 분해 방식은 유지보수에 다양한 문제를 야기합니다. 이 문제점을 이해하는 것은 유지보수 관점에서 객체지향의 장점을 이해할 수 있는 좋은 출발점 입니다.

급여 관리 시스템 구현

  • 하향식 기능 분해는 시스템을 최상위의 가장 추상적인 메인 함수로 정의하고, 메인 함수를 구현 가능한 수준까지 세부적인 단계로 분해하는 방법입니다. 하향식 기능 분해 방식으로 설계한 시스템은 메인 함수를 루트로 ‘트리(tree)‘로 표현할 수 있습니다. 트리에서 각 노드(node)는 시스템을 구성하는 하나의 프로시저를 의미하고 한 노드의 자식 노드는 부모 노드를 구현하는 절차 중의 한 단계를 의미합니다. 이처럼 하향싯 기능 분해는 논리적이고 처계적인 시스템 개발 절차를 제시합니다. 커다란 기능을 좀 더 작은 기능으로 단계적으로 정제해 가는 과정은 구조적이며 체계적인 동시에 이상적인 방법으로까지 보일 것입니다. 문제는 우리가 사는 세계는 그렇게 체계적이지도, 이상적이지도 않다는 것입니다. 체계적이고 이상적인 방법과 불규칙하고 불완전한 인간과 만나는 지점에서 혼란과 동요가 발생합니다.

하향식 기능 분해의 문제점

  • 시스템은 하나의 메인 함수로 구성돼 있지 않습니다.
  • 기능 추가나 요구사항 변경으로 인해 메인 함수를 빈번하게 수정해야 합니다.
  • 비즈니스 로직이 사용자 인터페이스와 강하게 결합됩니다.
  • 하향식 분해는 너무 이른 시기에 함수들의 실행 순서를 고정시키기 때문에 유연성과 재사용성이 저하됩니다.
  • 데이터 형식이 변경될 경우 파급효과를 예측할 수 없습니다.
  • 설계는 코드의 배치 방법이며 설계가 필요한 이유는 변경에 대비하기 위한 것이라는 점을 기억해야합니다. 하향식 접근 접근법과 기능 분해가 가지는 근본적인 문제점은 변경에 취햑한 설계를 낳는다는 것입니다.

하나의 메인 함수라는 비현실적인 아이디어

  • 어떤 시스템도 최초에 릴리지됐던 당시의 모습을 그대로 유지하지는 않습니다. 시간이 지나고 사용자를 만족시키기 위한 새로운 요구사항을 도출해나가면서 지속적으로 새로운 기능을 추가하게 됩니다. 이것은 시스템이 오직 하나의 메인 함수만으로 구현된다는 개념과는 완전히 모순됩니다.
  • 대부분의 경우 추가되는 기능은 최초에 배포된 메인 함수의 일부가 아닐 것입니다. 결국 처음에는 중요하게 생각됐던 메인 함수는 동등하게 중요한 여러 함수들 중 하나로 전락하고 맙니다. 어느 시점에 이르면 유일한 메인 함수라는 개념은 의미가 없어지고 시스템은 여러 개의 동등한 수준의 함수 집합으로 성장하게 될 것입니다.
  • 대부분의 시스템에서 하나의 메인 기능이란 개념은 존재하지 않습니다. 모든 기능들은 규모라는 측면에서 차이가 있을 수는 있겠지만 가능성의 측면에서는 동등하게 독립적이고 완결된 하나의 기능을 표현합니다.
  • 하향식 접근법은 하나의 알고리즘을 구현하거나 배치 처리를 구현하기에는 적합하지만 현대적인 상호작용 시스템을 개발하는 데는 적합하지 않습니다.
  • 현대 시스템은 동등한 수준의 다양한 가능으로 구성됩니다. ‘실제 시스템에 정상(top)‘이란 존재하지 않습니다.

메인 함수의 빈번한 재설계

  • 시스템 안에는 여러 개의 정상이 존재하기 때문에 결과적으로 하나의 메인 함수를 유일한 정상으로 간주하는 하향식 기능 분해의 경우에느 새로운 기능을 추가할 때마다 매번 메인 함수를 수정해야 합니다. 기존 로직과는 아무런 상관이 없는 새로운 함수의 적절한 위치를 확보해야 하기 때문에 메인 함수의 구조를 급격하게 변경할 수밖에 없는 것입니다. 기존 코드를 수정하는 것은 항상 새로운 버그를 만들어낼 확률을 높입니다.

비즈니스 로직과 사용자 인터페이스의 결합

  • 하향식 접근법은 비즈니스 로직을 설계하는 초기 단계부터 입력 방법과 출력 양식을 함께 고민하도록 강요합니다. 이로 인해 코드 안에서 비즈니스 로직과 사용자 인터페이스 로직이 밀접하게 결합됩니다.
  • 문제는 비즈니스 로직과 사용자 인터페이스가 변경되는 빈도가 다르다는 것입니다. 사용자 인터페이스는 시스템 내에서 가장 자주 변경되는 부분입니다. 반면 비즈니스 로직은 사용자 인터페이스에 비해 변경이 적게 발생합니다. 하향식 접근법은 사용자 인터페이스 로직과 비즈니스 로직을 한데 섞기 때문에 사용자 인터페이스를 변경하는 경우 비즈니스 로직까지 변경에 영향을 받게 됩니다. 따라서 하향식 접근법은 근본적으로 변경에 불안정한 아키텍처를 낳습니다.
  • 하향식 접근법을 따르는 설계는 중요한 비즈니스 로직과 인터페이스 로직이 main 함수 안에 뒤섞여 있어서 사용자 인터페이스를 변경하는 유일한 방법은 전체 구조를 재설계하는 것뿐입니다.
  • 하향식 접근법은 기능을 분해하는 과정에서 사용자 인터페이스의 관심사와 비즈니스 로직의 관심사를 동시에 고려하도록 강요하기 때문에 관심사의 분리라는 아키텍처 설계의 목적을 달성하기 어렵습니다.

성급하게 결정된 실행 순서

  • 하향식으로 기능을 분해하느 과정은 하나의 함수를 더 작은 함수로 분해하고, 분해한 함수들의 실행 순서를 결정하는 작업으로 요약할 수 있습니다. 이것은 설계의 시작하는 시점부터 시스템이 무엇(what)을 해야 하는지가 아니라 어떻게(how) 동작해야 하는지에 집중하도록 만듭니다. 하향식 접근법의 첫 번째 질문은 무엇이 아니라 어떻게 입니다.
  • 하향식 접근법의 설계는 처음부터 구현을 염두에 두기 때문에 자연스럽게 함수들의 실행 순서를 정의 하는 시간 제약(temporal constraint)을 강조합니다. 메인 함수가 작은 함수들로 분해되기 위해서는 우선 함수들의 순서를 결정해야 합니다.
  • 실행 순서나 조건, 반복과 같은 제어 구조를 미리 결정하지 않고는 분해를 진행할 수 없기 때문에 기능 분해 방식은 중앙집중 제어 스타일(centralized control style)의 형태를 띨 수밖에 없습니다. 결과적으로 모든 중요한 제어 흐름의 결정이 상위 함수에서 이뤄지고 하위 함수는 상위 함수의 흐름에 따라 적절한 시점에 호출됩니다.
  • 문제는 중요한 설계 결정사항인 함수의 제어 구조가 빈번한 변경의 대상이라는 점입니다. 기능이 추가되거나 변경될 때마다 초기에 결정된 함수들의 제어 구조가 올바르지 않다는 것이 판명됩니다. 결과적으로 기능을 추가하거나 변경하는 작업은 매번 기존에 결정된 함수의 제어구조를 변경하도록 만듭니다.
  • 이를 해결할 수 있는 한 가지 방법은 자주 변경되는 시간적인 제약에 대한 미련을 버리고 좀 더 안정적인 논리적 제약(logical constraint)을 설계의 기준으로 삼는 것입니다.
  • 객체지향 함수 간의 호출 순서가 아니라 객체 사이의 논리적인 관계를 중심으로 설계를 이끌어 나갑니다. 결과적으로 전체적인 시스템은 어떤 한 구성요소로 제어가 집중되지 않고 여러 개게들 사이로 제어 추체가 분산됩니다.
  • 하향식 접근법을 통해 분해한 함수들은 재사용하기도 어렵습니다. 모든 함수는 상위 함수를 분해하는 과정에서 필요에 따라 식별되며, 그에 따라 상위 함수가 강요하는 문맥(context) 안에서만 의미를 가지기 때문입니다. 재사용이라는 개념은 일반성이라는 의미를 포함합니다. 함수가 재사용 가능하려면 상위 함수보다 더 일반적이어야 합니다. 하지마 하햘ㅇ식 접근법을 따를 경우 분해된 하위 함수는 항상 상위 함수보다 문맥에 더 종속적입니다. 이것은 정확하게 재사용성과 반대되는 개념입니다.
  • 하향식 설계와 관련된 모든 문제의 원인은 결합도입니다. 함수는 상위 함수가 강요하는 문맥에 강하게 결합 됩니다. 함수는 함께 절차를 구성하는 다른 함수들과 시간적으로 강하게 결합돼 있습니다. 강한 결합도는 시스템을 변경에 취약하게 마들고 이해하기 어렵게 만듭니다. 강하게 결합된 시스템은 아주 사소한 변경만으로도 전체 시스템을 크게 요동치게 만들 수 있습니다. 현재의 문맥에 강하게 결합된 시스템은 현재 문맥을 떠나 다르 문맥으로 옮겨갔을 때 재사용하기 어렵습니다. 가장 큰 문제는 전체 시스템의 핵심적인 구조를 결정하는 함수들이 데이터와 강하게 결합된다는 것입니다.

데이터 변경으로 인한 파급효과

  • 하향식 기능 분해의 가장 큰 문제점은 어떤 데이터를 어떤 함수가 사용하고 있는지를 추적하기 어렵다느 것입니다. 따라서 데이터 변경으로 인해 어떤 함수가 영향을 받을지 예상하기 어렵습니다. 물론 개별 함수의 입장에서 사용하는 데이터를 파악하는 것은 어렵지 않습니다. 함수의 본체를 열어 참조하고 있는 모든 지역 변수, 인자, 전역 변수를 살펴보면 됩니다. 그러나 반대로 어떤 데이터가 어떤 함수에 의존하고 있는지를 파악하는 것은 어려운 일인데 모든 함수를 열어 데이터를 사용하고 있는지를 모두 확인해봐야 하기 때문입니다.
  • 이것은 코드 안에서 텍스트를 검색하는 단순한 문제가 아닙니다. 이것은 의존성과 결합도의 문제입니다. 그리고 테스트의 문제이기도 합니다. 데이터의 변경으로 인한 영향은 데이터를 직접 참조하는 모든 함수로 퍼져나갑니다. 스파게티처럼얽히고 설킨 대규모 시스템에서 데이터를 참조하는 함수들을 찾아 정상적으로 동작하는지 여부를 테스트하는 것은 기술보다는 운의 문제 입니다.
  • 데이터 변겨응로 인한 영향을 최소화하려면 데이터와 함께 변경되는 부분과 그렇지 않은 부분을 명확하게 분리해야 합니다. 이를 위해 데이터와 함께 변경되는 부분을 하나의 구현 단위로 묶고 외부에서는 제공되는 함수만 이용해 데이터에 접근해야 합니다. 즉, 정의된 퍼블릭 인터페이스를 통해 데이터에 대한 접근을 통제해야 하는 것입니다. 이것이 바로 의존성 관리의 핵심입니다. 변경에 대한 영향을 최소화하기 위해 영향을 받는 부분과 받지 않는 부분을 명화고하게 분리하고 잘 정의됭ㄴ 퍼블릭 인터페이스를 통해 변경되는 부분에 대한 접근을 통제합니다. 초기 소프트웨어 개발 분야의 선구자 중 한명인 데이비드 파나스는 기능 분해가 가진 본질적인 문제를 해결하기 위해 이 같은 개념을 기반으로 한 정보 은닉모듈이라는 개념을 제시하기에 이르렀습니다.

언제 하향식 분해가 유용한가?

  • 하향식 아이디어가 매력적인 이유는 설계가 어느 정도 안정화 된 후에는 설계의 다양한 측면을 논리적으로 설명하고 문서화하기에 용이하기 때문입니다. 그러나 문서화하는 데 적절한 방법이 좋은 구조를 설계할 수 있는 방법과 동일한 것은 아닙니다.
  • 하향식은 이미 완전히 이해된 사실을 서술하기에 적합한 방법입니다. 그러나 하향식은 새로운 것을 개발하고, 설계하고, 발견하는 데는 적합한 방법이 아닙니다. 시스템이나 프로그램 개발자가 이미 완료한 결과에 대한 명확한 아이디어를 가지고 있다면 머릿속에 있는 것을 종이에 서술하기 위해 하향식을 사용할 수 있습니다. 이것은 사람들이 하향식 설계나 개발을 할 수 있고, 그렇게 함으로써 성공할 수 있다고 믿게 만드는 이유입니다. 하향싯 단계가 시작될 때 문제는 이미 해결됐고, 오직 해결돼야만 하는 세부사항만이 존재할 뿐입니다.
  • 하향식 분해는 작은 프로그램과 개별 알고리즘을 위해서는 유용한 패러다임으로 남아 있습니다. 특히 프로그래밍 과정에서 이미 해결된 알고리즘을 문서화하고 서술하는 데는 훌륭한 기법입니다. 그러나 실제로 동작하는 커다란 소프트웨어를 설꼐하는 데 적합한 방법은 아닙니다.

정리

  • 하향식 분해 방식으로 설계된 소프트웨어는 하나의 함수에 제어가 집중되기 때문에 확장이 어렵습니다.
  • 하향식 분해는 프로젝트 초기에 설계의 본질적인 측면을 무시하고 사용자 인터페이스 같은 비본질적인 측면에 집중하게 만듭니다.
  • 과도하게 함수에 집중하게 함으로써 소프트웨어의 중요한 다른 측면인 데이터에 대한 영향도를 파악하기 어렵게 만듭니다.
  • 하향식 분해를 적용한 설계는 근본적으로 재사용하기 어렵습니다.

03. 모듈

정보 은닉과 모듈

  • 변경을 관리하는 기본적인 전략은 함께 변경되는 부분을 하나의 구현 단위로 묶고 퍼블릭 인터페이스를 통해서만 접근하도록 만드는 것입니다. 즉, 기능을 기반으로 시스템을 분해하는 것이 아니라 변경의 방향에 맞춰 시스템을 분해하는 것입니다.
  • 정보 은닉(information hiding)은 시스템을 모듈 단위로 분해하기 위한 기본 원리로 시스템에서 자주 변경되는 부분을 상대적으로 덜 변경되는 안정적인 인터페이스 뒤로 가뭐야 한다는 것입니다. 데이비드 파나스는 시스템을 모듈로 분할하는 원칙은 외부에서 유출돼서는 안 되는 비밀의 윤곽을 따라야 한다고 주장합니다.
  • 모듈은 서브 프로그램이라기보다는 책임의 할당입니다. 모듈화는 개별적인 모듈에 대한 작업이 시작되기 전에 정해져야 하는 설계 결정들을 포함합니다. 불한된 모듈은 다른 모듈에 대해 감춰야 하는 설계 결정에 따라 특정지어집니다. 해당 모듈 내부의 작업을 가능한 하 적게 노출하는 인터페이스 또는 정의를 선택합니다. 어려운 설계 결정이나 변화할 것 같은 설계 결정들의 목록을 사용해 설계를 시작할 것을 권장합니다. 이러한 결정이 외부 모듈에 대해 숨겨지도록 각 모듈을 설계해야 합니다.
  • 정보 은닉은 외부에 감춰야 하는 비밀에 따라 시스템을 분할하는 모듈 분할 원리입니다. 모듈은 변경될 가능성이 있는 비밀을 내부로 감추고, 잘 정의되고 쉽게 변경되지 않으 퍼블릭 인터페이스를 외부에 제공해서 내부의 비밀에 함부로 접근하지 못하게 합니다.
  • 모듈과 기능 분해는 상호 배타적인 관계가 아닙니다. 시스템을 모듈로 분해한 후에는 각 모듈 내부를 구현하기 위해 기능 분해를 적용할 수 있습니다. 기능 분해가 하나의 기능을 구현하기 위해 필요한 기능들을 순차적으로 찾아가는 탐색의 과정이라면 모듈 분해는 감춰야 하는 비밀을 선택하고 비밀 주변에 안정적인 보호막을 설치하는 보존의 과정입니다. 비밀을 결정하고 모듈을 분해한 후에는 기능 분해를 이용해 모듈에 필요한 퍼블릭 인터페이스를 구현할 수 있습니다.
  • 시스템이 감춰야 하는 비밀을 찾아서 외부에서 내부의 비밀에 접근하지 못하도록 커다란 방어막을 쳐서 에워쌉니다. 이 방어막이 바로 퍼블릭 인터페이스가 됩니다.
  • 모듈은 비밀을 감춰야 합니다.
    • 복잡성 : 모듈이 너무 복잡한 경우 이해하고 사용하기가 어렵습니다. 외부에 모듈을 추상화할 수 있는 간단한 인터페이스를 제공해서 모듈의 복잡도를 낮춥니다.
    • 변경 가능성 : 변경 가능한 설계 결정이 외부에 노출될 경우 실제로 변경이 발생했을 때 파급효과가 커집니다. 변경 발생 시 하나의 모듈만 수정하면 되도록 변경 가능한 설계 결정을 모듈 내부로 감추고 외부에는 쉽게 변경되지 않을 인터페이스를 제공합니다.
  • 데이터와 메서드를 하나의 단위로 통합하고 퍼블릭 메서드(public method)를 통해서만 접근하도록 허용하는 방법을 데이터 캡슐화(data encapsulation)라고 합니다. 정보 은닉과 데이터 캡슐화는 동일한 개념이 아닙니다. 변경과 관련된 비밀을 감춘다는 측면에서 정보 은닉과 캡슐화는 동일 개념을 가리키는 두 가지 다른 용어지만 데이터 캡슐화는 비밀의 한 종류인 데이터를 감추는 캡슐화의 한 종류일 뿐입니다.
  • 시스템의 가장 일반적인 비밀은 데이터 입니다. 이 관점이 데이터 캡슐화와 정보 은닉을 혼동스럽게 만드는 것으로 보입니다. 비밀이 반드시 데이터일 필요는 없으며 복잡한 로직이나 변경 간으성이 큰 자료 구조일 수도 있습니다. 그럼에도 변경 시 시스템을 굴복시키는 대부분의 경우는 데이터가 변경되는 경우입니다.

모듈의 장점과 한계

  • 모듈은 기능이 아니라 변경의 정도에 따라 시스템을 분해하게 합니다. 각 모듈은 외부에 감춰야 하는 비밀과 관련성 높은 데이터와 함수의 집합입니다. 따라서 모듈 내부는 높은 응집도를 유지합니다. 모듈과 모듈 사이에는 퍼블릭 인터페이스를 통해서만 통신해야 합니다. 따라서 낮은 결합도를 유지합니다.
  • 모듈이 정보 은닉이라는 개념을 통해 데이터라는 존재를 설계의 중심 요소로 부각시켰습니다. 모듈에 있어서 핵심은 데이터입니다. 메인 함수를 정의하고 필요에 따라 더 세부적인 함수로 분해하는 하향식 기능 분해와 달리 모듈은 감춰야 할 데이터를 결정하고 이 데이터를 조작하는 데 필요한 함수를 결정합니다. 다시 말해서 기능이 아니라 데이터를 중심으로 시스템을 분해하는 것입니다. 모듈은 데이터와 함수가 통합된 한 차원 높은 추상화를 제공하는 설계 단위입니다.
  • 모듈이 프로시저 추상화보다는 높은 추상화 개념을 제공하지만 태생적으로 변경을 관리하기 위한 구현 기법이기 때문에 추상화 관점에서의 한계점이 명확합니다. 모듈의 가장 큰 단점은 인터페이스의 개념을 제공하지 않는다는 점입니다. 그리고 이를 만족시키기 위해 등장한 개념이 바로 추상 데이터 타입입니다.

모듈 내부의 변수가 변경되더라도 모듈 내부에만 영향을 미칩니다.

  • 모듈을 사용하면 모듈 내부에 정의된 변수를 직접 참조하는 코드의 위치를 모듈 내부로 제한할 수 있습니다. 이제 어떤 데이터가 변경됐을 때 영향을 받는 함수를 찾기 위해 해당 데이터를 정의한 모듈만 검색하면 됩니다. 더 이상 전체 함수를 일일이 분석할 필요가 없습니다. 모듈은 데이터 변경으로 인한 파급효과를 제어할 수 있기 때문에 코드를 수정하고 디버깅하기가 더 용이합니다.

비즈니스 로직과 사용자 인터페이스에 대한 관심사를 분리합니다.

  • 사용자 입력과 화면 출력을 모듈이 아닌 외부에 둡니다. 수정된 코드에서 모듈은 비즈니스 로직과 관련된 감심사만을 담당하며 인터페이스와 관심사는 모두 모듈을 사용하는 main 함수쪽에 위치합니다. 이제 다른 형식의 사용자 인터페이스를 추가하더라도 모듈에 포함된 비즈니스 로직은 변경되지 않습니다.

전역 변수와 전역 함수를 제거함으로써 네임스페이스 오염(namespace pollution)을 방지합니다.

  • 모듈의 한 가지 용도는 네임스페이스를 제공하는 것입니다. 변수와 함수를 모듈 내부에 포함시키기 때문에 다른 모듈에서도 동일한 이름을 사용하 수 있게 됩니다. 따라서 모듈은 전역 네임스페이스의 오염을 방지하는 동시에 이름 충돌(name collision)의 위험을 완화합니다.

04. 데이터 추상화와 추상 데이터 타입

추상 데이터 타입

  • 프로그래밍 언어에서 타입(type)이란 변수에 저장할 수 있는 내용물의 종류와 변수에 적용될 수 있는 연산의 가짓수를 의미합니다. 정수 타입의 변수를 선언하는 것은 프로그램 내에서 변수명을 참조할 때 해당 변수를 임의의 정숫값으로 간수하라고 말하는 것과 같습니다. 타입은 저장된 값에 대해 수행될 수 있는 연산의 집합을 결정하기 때문에 변수의 값이 어떻게 행동할 것이라는 것을 에측할 수 있게 합니다. 정수 타입의 변수는 덧셈 연산을 이용해 값을 더할 수 있고 문자열 타입의 변수는 연결 연산을 이용해 두 문자열을 하나로 합칠 수 있습니다.
  • 프로그래밍 언어는 다양한 형태의 내장 타입(built-in type)을 제공합니다. 기능 분해의 시대에 사용되단 절차형 언어들은 적은 수의 내장 타입만을 제공했으며 설상가상으로 타입을 추가하는 것이 불가능하거나 제한적이었습니다. 이 시대의 프로그램에서 사용하는 주된 추상화는 프로시저 추상화였습니다. 시간이 흐르면서 사람들은 프로시저 추상화로는 프로그램의 표현력을 향상시키는 데 한계가 있다는 사실을 반결했습니다. 프로시저 추상화를 보완하기 위해 데이터 추상화(data abstraction)의 개념을 제안합니다.
  • 프로시저만으로 충분히 풍부한 추상화의 어휘집을 제공할 수 없습니다. 이것은 언어 설계에서 가장 중요한 추상 데이터 타입(Abstract Data Type)의 개념으로 우리를 인도했습니다. 추상 데이터 타입은 추상 객체의 클래스를 정의한 것으로 추상 객체에 사용할 수 있는 오퍼레이션을 이용해 규정됩니다. 추상 데이터 객체를 사용할 때 프로그래머는 오직 객체가 외부에 제공하는 행위에만 관심을 가지며 행위가 구현되는 세부적인 사항에 대해서는 무시합니다. 객체가 저장소 내에서 어떻게 표현되는지와 같은 구현 정보는 오직 오퍼레이션을 어떻게 구현할 것인지에 집중할 때만 필요합니다. 객체의 사용자는 이 정보를 알거나 제공받을 필요가 없습니다.
  • 추상 데이터 타입은 프로시저 추상화 대신 데이터 추상화를 기반으로 소프트웨어를 개발하게 한 최초의 발걸음 입니다.
  • 추상 데이터 타입을 구현하려면 다음과 같은 특성을 위한 프로그래밍 언어의 지원이 필요합니다.
    • 타입 정의를 선언할 수 있어야 합니다.
    • 타입의 인스턴스를 다루기 위해 사용할 수 있는 오퍼레이션의 집합을 정의할 수 있어야 합니다.
    • 제공된 오퍼레이션을 통해서만 조작할 수 있도록 데이터를 외부로부터 보호할수 있어야 합니다.
    • 타입에 대해 여러 개의 인스턴스를 생성할 수 있어야 합니다.
  • 추상 데이터 타입을 정의하기 위해 제시한 언어적인 메커니즘을 오퍼레이션 클러스터(operation cluster)라고 불렀습니다.
  • 추상 데이터 타입을 구현할 수 있는 언어적인 장치를 제공하지 않는 프로그래밍 언어에서도 추상 데이터 타입을 구현하는 것은 가능합니다. 실제로 과거의 많은 프로그래머들은 모듈의 개념을 기반으로 추상 데이터 타입을 구현해 왔습니다. 그러나 언어 차원에서 추상 데이터 타입을 지원하는 것과 관습과 약속, 기법을 통해 추상 데이터 타입을 모방하는 것은 완전히 다른 이야기 입니다. 이것은 객체지향 언어를 사용하지 않아도 객체지향 프로그래밍을 할 수 있다는 낭설과도 유사합니다.
  • 내부에 캡슐화할 데이터를 결정했다면 추상 데이터 타입에 적용할 수 있는 오퍼레이션을 결정해야 합니다.
  • 추상 데이터 타입은 사람들이 세상을 바라보는 방식에 좀 더 근접해지도록 추상화 수준을 향상 시킵니다.
  • 추상 데이터 타입 정의를 기반으로 객체를 생성하는 것은 가능하지만 여전히 데이터와 기능을 분리해서 바라봅니다. 추상 데이터 타입은 말 그대도 시스템의 상태를 저장할 데이터를 표현합니다. 추상 데이터 타입으로 표현된 데이터를 이용해서 기능을 구현하는 핵심 로직은 추상 데이터 타입 외부에 존재합니다. 추상 데이터 타입은 데이터에 대한 관점을 설계의 표면으로 끌어올리기는 하지만 여전히 데이터와 기능을 분리하는 절차적인 설계의 틀에 같혀 있는 것입니다.
  • 추상 데이터 타입의 기본 의도를 프로그래밍 언어가 제공하는 타입처럼 동작하는 사용자 정의 타입을 추가할 수 있게 하는 것입니다. 프로그래밍 언어의 관점에서 추상 데이터 타입은 프로그래밍 언어의 내장 데이터 타입과 동잉합니다. 단지 타입을 개발자가 정의할 수 있다는 점이 다를 뿐입니다. 추상 데티터 타입에 대한 위와 같은 관점은 종종 객체지향 프로그래머들을 혼돈으로 몰아 갑니다.

05. 클래스

클래스는 추상 데이터 타입인가?

  • 대부분의 프로그래밍 서적은 클래스를 추상 데이터 타입으로 설명합니다. 클래스와 추상 데이터 타입 모두 데이터 추상화를 기반으로 시스템을 분해하기 때문에 이런 설명이 꼭 틀린 것만은 아닙니다. 두 메커니즘 모두 외부에서는 객체의 내부 속성에 접근할 수 없으며 오직 퍼블릭 인터페이스를 통해서만 외부와 의사소통할 수 있습니다.
  • 명확한 의미에서 추상 데이터 타입과 클래스는 동일하지 않습니다. 가장 핵심적인 차이는 클래스는 상속과 다형성을 지원하는 데 비해 추상 데이터 타입은 지원하지 못한다는 점입니다. 상속과 다형성일 지원하는 객체지향 프로그래밍(Object-Oriented Programming)과 구분하기 위해 상속과 다형성을 지원하지 않는 추상 데이터 타입 기반의 프로그래밍 패러다임을 객치기반 프로그래밍(Object-Based Programming)이라고 부르기도 합니다.
  • 추상 데이터 타입은 타입을 추상화한 것(type abstraction)이고 클래스는 절차를 추상화한 것(procedural abstraction)입니다.
  • 하나의 대표적인 타입이 다수의 세부적인 타입을 감추는기 때문에 이를 타입 추상화라고 부릅니다. 타입 추상화는 개별 오퍼레이션이 모든 개념적인 타입에 대한 구현을 포괄하도록 함으로써 하나의 물리적인 타입 안에 전체 타입을 감춥니다. 따라서 타입 추상화는 오퍼레이션을 기준으로 타입을 통합하는 데이터 추상화 기법입니다.
  • 타입 추상화를 기반으로 하는 대표적인 기법이 바로 추상 데이터 타입 입니다. 추상 데이터 타입은 오퍼레이션 기준으로 타입을 묶습니다.
  • 추상 데이터 타입이 오퍼레이션을 기준으로 타입을 묶는 방법이라면 객체지향은 타입을 기준으로 오퍼레이션을 묶습니다. 두 가지 클래스로 분리할 경우 공통 로직을 어디에 둘 것인지가 이슈가 됩니다. 공통 로직을 제공할 수 있는 가장 간단한 방법은 공통 로직을 포함할 부모 클래스를 정의하고 부모 클래스를 상속 받는 것입니다. 이제 클라이언트는 부모 클래스의 참조자에 대해 메시지를 전송하면 실제 클래스가 무엇인가에 따라 적절한 절차가 싱행됩니다. 즉, 동일한 메시지에 대해 서로 다르게 반응힙니다. 이것이 바로 다형성입니다.
  • 실제로 내부에서 수행되는 절차는 다르지만 클래스를 이용한 다형성은 절차에 대한 차이점을 줍니다. 다시 말해 객체지향은 절차 추상화(procedural abstraction)입니다.
  • 추상 데이터 타입은 오퍼레이션 기준으로 타입들을 추상화 합니다. 클래스는 타입을 기준으로 절차들을 추상화 합니다. 이것이 추상화와 분해의 관점에서 추상 데이터 타입과 클래스의 다른 점압니다.

추상 데이터 타입에서 클래스로 변경하기

  • 인스턴스를 명시적으로 생성합니다.
  • 객체를 생성하고 나면 객체의 클래스가 무엇인지는 중요하지 않습니다. 클라이언트 입장에서는 인스터스를 모두 부모 클래스의 인스턴스인 것처럼 다룰 수 있습니다. 클라이언트는 메시지를 수신할 객체의 구체적인 클래스에 관해 고민할 필요가 없습니다. 그저 수신자가 이해할 것으로 예상되는 메시지를 전송하기만 하면 됩니다.

변경을 기준을 선택하라

  • 단순히 클래스를 구현 단위로 사용한다는 것이 객체지향 프로그래밍을 한다는 것을 의미하지는 않습니다. 타입을 기준으로 절차를 추상화하지 않았다면 그것은 객체지향 분해가 아닙니다. 비록 클래스를 사용하고 있더라도 말입니다.
  • 클래스가 추상 데이터 타입의 개념을 따르는지를 확인할 수 있는 가장 간단한 방법은 클래스 내부에 인스턴스의 타입을 표현하는 변수가 있는지를 살펴보는 것입니다. 인스턴스 변수에 저장된 값을 기반으로 메서드 내에서 타입을 명시적으로 구분하는 방식은 객체지향을 위반하는 것으로 간주됩니다.
  • 객체지향에서는 타입 변수를 이용한 조건문을 다형성으로 대체합니다. 클라이언트가 객체의 타입을 확인한 후 적절한 메서드를 호출하는 것이 아니라 객체가 메시지를 처리할 적절한 메서드를 선택합니다. 흔히 ‘객체지향이란 조건문을 제거하느 것’이라는 다소 편협한 견해가 널리 퍼진 이유가 바로 이 때문입니다.
  • 모든 설계가 그런 것처럼 조건문을 사용하는 방식을 기피하는 이유 역시 변경 때문입니다. 추상 데이터 타입을 기반으로 한 새로운 타입을 추가하기 위해서는 값을 체크하는 클라이언트의 조건문을 하나씩 다 찾아 수정해야 합니다. 이에 반해 객체지향은 새로운 구현 클래스를 상속 계층에 추가하고 필요한 메서드를 오버라이딩하면 됩니다. 새로 추가된 클래스의 메서드를 실행하기 위한 어떤 코드도 추가할 필요가 없습니다. 이것은 시스템에 새로운 로직을 추가하기 위해 클라이언트 코드를 수정할 필요가 없다는 것을 의미합니다.
  • 기존 코드에 아무런 영향도 미치지 않고 새로운 객체 유형과 행위를 추가할 수 있는 개체지향의 특성을 개방-폐쇄 원칙(Open-Closed Principle, OCP)이라고 부릅니다. 이것이 객체지향 설계가 전통적인 방식에 비해 변경하고 확장하기 쉬운 구조를 설계할 수 있는 이유입니다.
  • 대부분의 객체지향 서적에서는 추상 데이터 타입을 기반으로 애플리케이션을 설계하는 방식을 잘못된 것으로 설명합니다. 설계는 변경과 관련된 것입니다. 설계의 유용성은 변경의 방향성과 발생 빈도에 따라 경정됩니다. 그리고 추상 데이터 타입과 객체지향 설계의 유용성은 설계에 요구되는 변경의 압력이 ‘타입 추가’에 관한 것인지, 아니면 ‘오퍼레이션 추가’에 관한 것인지에 따라 달라집니다.
    • 타입 추가라는 변경의 압력이 더 강한 경우에는 객체지향의 손을 들어줘야 합니다. 추상 데이터 타입의 경우 새로운 타입을 추가하려면 타입을 체크하는 클라이언트 코드를 일일이 찾아 수정한 후 올바르게 작동하는지 테스트해야 합니다. 반면 객체지향의 경우에는 클라이언트 코드를 수정할 필요가 없습니다. 간단하게 새로운 클래스를 상속 계층에 추가하기만 하면 됩니다.
    • 변경의 주된 압력이 오퍼레이션을 추가하는 것이라면 추상 데이터 타입의 승리를 선언해야 합니다. 객체지향의 경우 새로운 오퍼레이션을 추가하기 위해서는 상속 계층에 속하는 모든 클래스를 한번에 수정해야 합니다. 이와 달리 추상 데이터 타입의 경우에는 전체 타입에 대한 구현 코드가 하나의 구현체 내에 포함돼 있기 때문에 새로운 오퍼레이션을 추가하는 작업이 상대적으로 간단합니다.
  • 새로운 타입을 빈번하게 추가해야 한다면 객체지향의 클래스 구조가 더 유용합니다. 새로운 오퍼레이션을 빈번하게 추가해야 한다면 추상 데이터 타입을 선택하는 것이 현명한 판단 입니다. 변경의 축을 찾아야 합니다. 객체지향적인 접근법이 모든 경우에 올바른 해결 방법인 것은 아닙니다.

데이터 주도 설계

  • 추상 데이터 타입의 접근법을 객체지향 설계에 구현한 것을 데이터 주도 설계라고 부릅니다. 책임 주도 설계는 데이터 주도 설계 방법을 개선하고 하는 노력을 산물이었습니다. 모듈과 추상 데이터 타입이 데이터 중심적인 관점(data centered view)을 취하는데 비해 객체지향은 서비스 중심적인 관점(service centered view)을 취한다는 말로 둘 사이의 차이점을 깔끔하게 설명합니다.

협력이 중요하다.

  • 객체지향에서 중요한 것은 역할, 책임, 협력 입니다. 객체지향은 기능을 수행하기 위해 객체들이 협력하는 방식에 집중합니다. 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션의 구현 방식을 타입별로 분해하는 것은 올바른 접근법이 아닙니다.
  • 객체에게 로직을 분배하는 방법에 있어서 추상 데이터 타입과 클래스의 차이를 보여주기 위한 것이지 객체를 설계하는 방법을 설명한 것이 아닙니다. 객체를 설계하는 방법은 3장에서 설명했던 책임 주도 설계의 흐름을 따른다는 점을 기억해야 합니다.
  • 객체가 참여할 협력을 결정하고 협력에 필요한 책임을 수행하기 위해 어떤 객체가 필요한지에 관해 고민합니다. 그 책임을 다양한 방식으로 수행해야 할 때만 타입 계층 안에 각 절차를 추상화합니다. 탕비 계층과 다형성은 협력이라는 문맥 안에서 책임을 수행하는 방법에 관해 고민한 결과물이어야 하면 그 자체가 목적이 되어서는 안 됩니다.

하향식 접근법이란?

객체 분해에서의 하향식 접근법은 전통적으로 문제를 해결하는 방식 때문인데, 분할정복법이 컴공에서의 다재다능한 기본 방법입니다. 하나의 문제를 나누고 작아진 각 문제를 해결한 후에 해결된 결과들을 합쳐 전체의 문제를 해결하는 하향식 기법입니다. 상위 문제의 답이 하위 문제들의 답들로써 도출될 수 있음이 증명되면 쉽게 도입할 수 있는 기법이라 전통적으로 많이 적용되던 기법입니다. 이것을 쓰다보니 하향식접근법이 디폴트인 것입니다.


참고