[Object] 7장

7. 객체 분해

불필요한 정보를 제거하고 현재의 문제 해결에 필요한 핵심만 남기는 작업을 추상화라고 부른다.

가장 일반적인 추상화 방법은 한 번에 다뤄야 하는 문제의 크기를 줄이는 것이다. 큰 문제를 해결 가능한 작은 문제로 나누는 작업을 분해(decomposition) 라고 부른다.


분해의 목적은 큰 문제를 인지 과부하의 부담 없이 단기 기억 안에서 한 번에 처리할 수 있는 규모의 문제로 나누는 것이다.

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

모든 프로그래밍 패러다임은 추상화와 분해의 관점에서 설명할 수 있다. 현대적인 프로그래밍 언어를 특정 짓는 중요한 두 가지 추상화 매커니즘은 프로시저 추상화(procedure abstraction)데이터 추상화(data abstraction) 다.

프로시저 추상화는 소프트웨어가 무엇을 해야 하는지를 추상화한다. 데이터 추상화는 소프트웨어가 무엇을 알아야 하는지를 추상화한다. 소프트웨어는 데이터를 이용해 정보를 표현하고 프로시저를 이용해 데이터를 조작한다.

프로그래밍 패러다임이란 적절한 추상화의 윤곽을 따라 시스템을 어떤 식으로 나눌 것인지를 결정하는 원칙과 방법의 집합이다.

프로시저 추상화를 중심으로 시스템을 분해하기로 결정했다면 기능 분해(functional decomposition) 의 길로 들어서는 것이다. 기능 분해는 알고리즘 분해(algorithmic decomposition) 라고 부르기도 한다.

데이터를 중심으로 시스템을 분해하기로 결정했다면 다시 두 가지 중 하나를 선택해야 한다. 하나는 데이터를 중심으로 타입을 추상화(type abstraction) 하는 것이고 다른 하나는 데이터를 중심으로 프로시저를 추상화(procedure abstraction) 하는 것이다. 전자를 추상 데이터 타입(Abstract Data Type) 이라고 부르고 후자를 객체지향(Object-Oriented) 이라고 부른다.

역할과 책임을 수행하는 객체가 바로 객체지향 패러다임이 이용하는 추상화다. 기능을 협력하는 공동체를 구성하도록 객체들을 나누는 과정이 바로 객체지향 패러다임에서의 분해를 의미한다.

프로그래밍 언어의 관점에서 객체지향이란 데이터를 중심으로 데이터 추상화프로시저 추상화를 통합한 객체를 이용해 시스템을 분해하는 방법이다. 따라서 프로그래밍 언어적인 관점에서 객체지향을 바라보는 일반적인 관점은 데이터 추상화와 프로시저 추상화를 함께 포함한 클래스를 이용해 시스템을 분해하는 것이다.

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

메인 함수로서의 시스템

기능은 오랜 시간 동안 시스템을 분해하기 위한 기준으로 사용됐으며, 이 같은 시스템 분해 방식을 알고리즘 분해 또는 기능 분해라고 부른다. 기능 분해의 관점에서 추상화의 단위는 프로시저이며 시스템은 프로시저를 단위로 분해된다.

프로시저는 반복적으로 실행되거나 거의 유사하게 실행되는 작업들을 하나의 장소에 모아놓음으로써 로직을 재사용하고 중복을 방지할 수 있는 추상화 방법이다. 프로시저를 추상화라고 부르는 이유는 내부의 상세한 구현 내용을 모르더라도 인터페이스만 알면 프로시저를 사용할 수 있기 때문이다.

전통적인 기능 분해 방법은 하향식 접근법(Top-Down Approach) 으 따른다. 하향식 접근법이란 시스템을 구성하는 가장 최상위(topmost) 기능을 정의하고, 이 최상위 기능을 좀 더 작은 단계의 하위 기능으로 분해해 나가는 방법을 말한다. 분해는 세분화된 마지막 하위 기능이 프로그래밍 언어로 구현 가능한 수준이 될 때까지 계속된다.

급여 관리 시스템

기능 분해 방법을 이용하여 급여 관리 시스템을 구현한다. 전통적으로 기능 분해 방법은 하향식 접근법을 따르며 단계적인 정제 절차를 따라 시스템을 구축한다. 기능 분해의 초점은 하나의 문장으로 표현된 기능을 여러 개의 더 작은 기능으로 분해하는 것이다.

직원의 급여를 계산한다

좀 더 세분화된 절차로 구체화해야 한다.

직원의 급여를 계산한다
  사용자로부터 소득세율을 입력받는다
  직원의 급여를 계산한다
  양식에 맞게 결과를 출력한다

모든 문장이 정제 과정을 거치면서 하나 이상의 좀 더 단순하고 구체적인 문장들의 조합으로 분해돼야 한다. 만약 좀 더 정제 가능한 문장이 존재하면 동일한 과정을 거쳐 구현이 가능할 정도로 충분히 저수준의 문장이 될 때까지 기능을 분해해야 한다.

직원의 급여를 계산한다
  사용자로부터 소득세율을 입력받는다
    "세율을 입력하세요: "라는 문장을 화면에 출력한다
    키보드를 통해 세율을 입력받는다
  직원의 급여를 계산한다
    전역 변수에 저장된 직원의 기본급 정보를 얻는다
    급여를 계산한다
  양식에 맞게 결과를 출력한다
    "이름: {직원명}, 급여: {계산된 금액}" 형식에 따라 출력 문자열을 생성한다

기능 분해의 결과는 최상위 기능을 수행하는 데 필요한 절차들을 실행되는 시간 순서에 따라 나열한 것이다.

기능 분해 방법에서는 기능을 중심으로 필요한 데이터를 결정한다. 먼저 필요한 기능을 생각하고 이 기능을 분해하고 정제하는 과정에서 필요한 데이터의 종류와 저장 방식을 식별한다. 하지만 이 방법은 유지보수에 다양한 문제를 야기한다.

급여 관리 시스템 구현

하향식 기능 분해 방식으로 설계한 시스템은 메인 함수를 루트로 하는 트리(tree) 로 표현할 수 있다. 트리에서 각 노드(node)는 시스템을 구성하는 프로시저를 의미하고 한 노드의 자식 노드는 부모 노드를 구현하는 절차 중의 한 단계를 의미한다. 7.1.png 그림 7.1 트리 형태로 표현한 급여 관리 시스템의 기능 분해 구조

하향식 기능 분해의 문제점

하향식 기능 분해 방법을 실제 설계에 적용하다 보면 다음과 같은 다양한 문제에 직면한다.

  • 시스템은 하나의 메인 함수로 구성돼 있지 않다.
  • 기능 추가나 요구사항 변경으로 인해 메인 함수를 빈번하게 수정해야 한다.
  • 비즈니스 로직이 사용자 인터페이스와 강하게 결합된다.
  • 하향식 분해는 너무 이른 시기에 함수들의 실행 순서를 고정시키기 때문에 유연성과 재사용성이 저하된다.
  • 데이터 형식이 변경될 경우 파급효과를 예측할 수 없다.

하향식 접근법과 기능 분해가 가지는 근본적인 문제점은 변경에 취약한 설계를 낳는다.

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

어떤 시스템도 최초에 릴리스됐던 당시의 모습을 그대로 유지하지는 않는다. 어느 시점에 이르면 유일한 메인 함수라는 개념은 의미가 없어지고 시스템은 여러 개의 동등한 수준의 함수 집합으로 성장하게 된다. 대부분의 시스템에서 하나의 메인 기능이란 개념은 존재하지 않는다.

메인 함수의 빈번한 재설계

하향식 기능 분해의 경우에는 새로운 기능을 추가할 때마다 매번 메인 함수를 수정해야 한다. 기존 코드를 수정하는 것은 항상 새로운 버그를 만들어낼 확률을 높인다는 점에 주의하라. 기존 코드의 빈번한 수정으로 인한 버그 발생 확률이 높아지기 때문에 시스템은 변경에 취약해질 수밖에 없다.

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

하향식 접근법은 비즈니스 로직을 설계하는 초기 단계부터 입력 방법과 출력 양식을 함께 고민하도록 강요한다. 결과적으로 코드 안에서 비즈니스 로직과 사용자 인터페이스 로직이 밀접하게 결합된다.

문제는 비즈니스 로직과 사용자 인터페이스가 변경되는 빈도가 다르다는 것이다. 하향식 접근법에서 사용자 인터페이스를 변경하는 경우 비즈니스 로직까지 변경에 영향을 받게 된다. 하향식 접근법은 “관심사의 분리” 라는 아키텍처 설계의 목적을 달성하기 어렵다.

성급하게 결정된 실행 순서

하향식으로 기능을 분해하는 과정은 하나의 함수를 더 작은 함수로 분해하고, 분해된 함수들의 실행 순서를 결정하는 작업으로 요약할 수 있다. 이것은 설계하는 시점부터 시스템이 무엇(what) 을 해야 하는지가 아니라 어떻게(how) 동작해야 하는지에 집중하도록 만든다.

하향식 접근법의 설계는 처음부터 구현을 염두에 두기 때문에 자연스럽게 함수들의 실행 순서를 정의하는 시간 제약(temporal constraint) 을 강조한다. 메인 함수가 작은 함수들로 분해되기 위해서는 우선 함수들의 순서를 결정해야 한다.

기능 분해 방식은 중앙집중 제어 스타일(centralized control style) 의 형태를 띨 수밖에 없다. 결과적으로 모든 중요한 제어 흐름의 결정이 상위 함수에서 이뤄지고 하위 함수는 상위 함수의 흐름에 따라 적절한 시점에 호출된다.

문제는 중요한 설계 결정사항인 함수의 제어구조가 빈번한 변경의 대상이라는 점이다.

이를 해결하기 위해서는 자주 변경되는 시간적인 제약에 대한 미련을 버리고 좀 더 안정적인 논리적 제약(logical constraint) 을 설계의 기준으로 삼는 것이다. 객체지향은 함수 간의 호출 순서가 아니라 객체 사이의 논리적인 관계를 중심으로 설계를 이끌어간다. 결과적으로 여러 객체들 사이로 제어 주체가 분산된다.

하향식 접근법을 통해 분해한 함수들은 재사용하기도 어렵다. 모든 함수는 상위 함수가 강요하는 문맥(context) 안에서만 의미를 가지기 때문이다. 하향식 접근법을 따를 경우 분해된 함수는 항상 상위 함수보다 문맥에 더 종속적이다.

하향식 설계와 관련된 모든 문제의 원인은 결합도다. 강한 결합도는 시스템을 변경에 취약하게 만들고 이해하기 어렵게 만든다. 가장 큰 문제는 전체 시스템의 핵심적인 구조를 결정하는 함수들이 데이터와 강하게 결합된다는 것이다.

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

하향식 기능 분해의 가장 큰 문제점은 어떤 데이터를 어떤 함수가 사용하고 있는지를 추적하기 어렵다는 것이다. 따라서 데이터 변경으로 인해 어떤 함수가 영향을 받을지 예상하기 어렵다. 어떤 데이터가 어떤 함수에 의존하고 있는지를 파악하는 것은 어려운 일이다. 이것은 의존성과 결합도 문제이다.

데이터 변경으로 인해 발생하는 함수에 대한 영향도를 파악하는 것이 생각보다 쉽지 않다. 코드가 성장하고 라인 수가 증가할수록 전역 데이터를 변경하는 것은 악몽으로 변해간다.

데이터 변경으로 인한 영향을 최소화하려면 데이터와 함께 변경되는 부분과 그렇지 않은 부분을 명확하게 분리해야 한다. 잘 정의된 퍼블릭 인터페이스를 통해 데이터에 대한 접근을 통제해야 한다.

이것이 의존성 관리의 핵심이다. 변경에 대한 영향을 최소화하기 위해 영향을 받는 부분과 받지 않는 부분을 명확하게 분리하고 잘 정의된 퍼블릭 인터페이스를 통해 변경되는 부분에 대한 접근을 통제하라. 데이비드 파나스(David Parnas)는 이 같은 개념을 기반으로 한 정보 은닉모듈이라는 개념을 제시하였다.

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

하향식 아이디어가 매력적인 이유는 설계가 어느 정도 안정화된 후에는 설계의 다양한 측면을 논리적으로 설명하고 문서화하기에 용이하기 때문이다. 하향식은 이미 완전히 이해된 사실을 서술하기에 적합한 방법이다.

모듈

정보 은닉과 모듈

기능을 기반으로 시스템을 분해하는 것이 아니라 변경에 방향에 맞춰 시스템을 분해해야 한다.

정보 은닉(information hiding)시스템을 모듈 단위로 분해하기 위한 기본 원리로 시스템에서 자주 변경되는 부분을 상대적으로 덜 변경되는 안정적인 인터페이스 뒤로 감춰야 한다는 것이 핵심이다.

모듈은 서브 프로그램이라기보다는 책임의 할당이다. 모듈화는 개별적인 모듈에 대한 작업이 시작되기 전에 정해져야 하는 설계 결정들을 포함한다. 어려운 설계 결정이나 변화할 것 같은 설계 결정들의 목록을 사용해 설계를 시작할 것을 권장한다.

정보 은닉은 외부에 감춰야 하는 비밀에 따라 시스템을 분할하는 모듈 분할 원리다. 모듈은 변경될 가능성이 있는 비밀을 내부로 감추고, 쉽게 변경되지 않을 퍼블릭 인터페이스를 외부에 제공해서 내부의 비밀에 함부로 접근하지 못하게 한다.

기능 분해가 하나의 기능을 구현하기 위해 필요한 기능들을 순차적으로 찾아가는 탐색의 과정이라면 모듈 분해는 감춰야 하는 비밀을 선택하고 비밀 주변에 안정적인 보호막을 설치하는 보존의 과정이다.

모듈은 다음과 같은 두 가지 비밀을 감춰야 한다.

  • 복잡성 : 모듈이 너무 복잡한 경우 이해하고 사용하기 어렵다. 외부에 모듈을 추상화할 수 있는 간단한 인터페이스를 제공해서 모듈의 복잡도를 낮춘다.
  • 변경 가능성 : 변경 발생 시 하나의 모듈만 수정하면 되도록 변경 가능한 설계 결정을 모듈 내부로 감추고 외부에는 쉽게 변경되지 않을 인터페이스를 제공한다.

모듈의 장점과 한계

모듈의 장점은 다음과 같다.

모듈 내부의 변수가 변경되더라도 모듈 내부에만 영향을 미친다 모듈을 사용하면 모듈 내부에 정의된 변수를 직접 참조하는 코드의 위치를 모듈 내부로 제한할 수 있다. 모듈은 데이터 변경으로 인한 파급효과를 제어할 수 있기 때문에 코드를 수정하고 디버깅하기가 더 용이하다.

비즈니스 로직과 사용자 인터페이스에 대한 관심사를 분리한다 다른 형식의 사용자 인터페이스를 추가하더라도 모듈에 포함된 비즈니스 로직은 변경되지 않는다.

전역 변수와 전역 함수를 제거함으로써 네임스페이스 오염(namespace pollution)을 방지한다 모듈은 네임스페이스의 오염을 방지하는 동시에 이름 충돌(name collision)의 위험을 완화한다.

모듈은 기능이 아니라 변경의 정도에 따라 시스템을 분해하게 한다. 따라서 모듈 내부는 높은 응집도를 유지한다. 모듈과 모듈 사이에는 퍼블릭 인터페이스를 통해서만 통신해야 한다. 따라서 낮은 결합도를 유지한다.

하향식 기능 분해와 달리 모듈은 감춰야 할 데이터를 결정하고 데이터를 조작하는 데 필요한 함수를 결정한다. 다시 말해 기능이 아니라 데이터를 중심으로 시스템을 분해한다. 모듈은 데이터와 함수가 통합된 한 차원 높은 추상화를 제공하는 설계 단위다.

모듈이 프로시저 추상화보다는 높은 추상화 개념을 제공하지만 추상화 관점에서의 한계점이 명확하다. 모듈의 가장 큰 단점은 인스턴스의 개념을 제공하지 않는다는 점이다.

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

추상 데이터 타입

프로그래밍 언어에서 타입(type) 이란 변수에 저장할 수 있는 내용물의 종류와 변수에 적용될 수 있는 연산의 가짓수를 의미한다.

바바라 리스코프(Barbara Liskov)는 프로시저 추상화를 보완하기 위해 데이터 추상화(data abstraction) 의 개념을 제안했다. 추상 데이터 타입은 추상 객체의 클래스를 정의한 것으로 추상 객체에 사용할 수 있는 오퍼레이션을 이용해 규정된다. 추상 데이터 객체를 사용할 때 프로그래머는 오직 객체가 외부에 제공하는 행위에만 관심을 가지며 행위가 구현되는 세부적인 사항에 대해서는 무시한다.

추상 데이터 타입은 말 그대로 시스템의 상태를 저장할 데이터를 표현한다. 데이터에 대한 관점을 설계 표면으로 끌어올리기는 하지만 여전히 데이터와 기능을 분리하는 절차적인 설계의 틀에 갇혀 있다.

클래스

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

클래스와 추상 데이터 타입의 가장 핵심적인 차이는 클래스는 상속과 다형성을 지원하는 데 비해 추상 데이터 타입은 지원하지 못한다는 점이다. 상속과 다형성을 지원하는 객체지향 프로그래밍(Object-Oriented Programming) 과 구분하기 위해 상속과 다형성을 지원하지 않는 추상 데이터 타입 기반의 프로그래밍 패러다임을 객체기반 프로그래밍(Object-Based Programming) 이라고 부른다.

윌리엄 쿡(William Cook)은 추상 데이터 타입은 타입을 추상화한 것(type abstraction)이고 클래스는 절차를 추상화한 것(procedural abstraction)이라고 정의하였다. 윌리엄 쿡은 하나의 대표적인 타입이 다수의 세부적인 타입을 감추기 때문에 이를 타입 추상화라고 불렀다. 타입 추상화는 개별 오퍼레이션이 모든 개념적인 타입에 대한 구현을 포괄하도록 함으로써 하나의 물리적인 타입 안에 전체 타입을 감춘다. 따라서 타입 추상화오퍼레이션을 기준으로 타입을 통합하는 데이터 추상화 기법이다.

객체지향은 타입을 기준으로 오퍼레이션을 묶는다. 클래스의 공통 로직은 부모 클래스를 정의하고 하위 클래스가 부모 클래스를 상속받게 한다. 즉, 동일한 메시지에 대해 서로 다르게 반응한다. 이것이 바로 다형성이다. 클래스를 이용한 다형성은 절차에 대한 차이점을 감춘다. 다시 말해 객체지향은 절차 추상화(procedural abstraction) 다.

추상 데이터 타입오퍼레이션을 기준으로 타입들을 추상화한다. 클래스타입을 기준으로 절차들을 추상화한다.

변경을 기준으로 선택하라

클래스가 추상 데이터 타입의 개념을 따르는지를 확인할 수 있는 가장 간단한 방법은 클래스 내부에 인스턴스의 타입을 표현하는 변수가 있는지를 살펴보는 것이다. 인스턴스 변수에 저장된 값을 기반으로 메서드 내에서 타입을 명시적으로 구분하는 방식은 객체지향을 위반하는 것으로 간주된다.

객체지향에서는 타입 변수를 이용한 조건문을 다형성으로 대체한다. 조건문을 사용하는 방식을 기피하는 이유는 변경때문이다. 객체 지향은 새로운 유형을 구현하는 클래스를 상속 계층에 추가하고 필요한 메서드를 오버라이딩하면 된다. 이것은 시스템에 새로운 로직을 추가하기 위해 클라이언트 코드를 수정할 필요가 없다는 것을 의미한다.

기존 코드에 아무런 영향도 미치지 않고 새로운 객체 유형과 행위를 추가할 수 있는 객체지향의 특성을 개방-폐쇄 원칙(Open-Closed Principle, OCP) 이라고 부른다. 이것이 객체지향 설계가 전통적인 방식에 비해 변경하고 확장하기 쉬운 구조로 설계할 수 있는 이유다.

설계는 변경과 관련된 것이다. 설계의 유용성은 변경의 방향성발생 빈도에 따라 결정된다. 설계에 요구되는 변경의 압력이 타입 추가에 관한 것인지, 아니면 오퍼레이션 추가에 관한 것인지에 따라 달라진다.

타입 추가라는 변경의 압력이 더 강한 경우에는 객체지향의 손을 들어줘야 한다. 이에 반해 변경의 주된 압력이 오퍼레이션을 추가하는 것이라면 추상 데이터 타입의 승리를 선언해야 한다.

협력이 중요하다

설계할 때 객체가 참여할 협력을 결정하고 협력에 필요한 책임을 수행하기 위해 어떤 객체가 필요한지에 관해 고민하라. 그 책임을 다양한 방식으로 수행해야 할 때만 타입 계층 안에 각 절차를 추상화하라. 타입 계층과 다형성은 협력이라는 문맥 안에서 책임을 수행하는 방법에 관한 고민한 결과물이어야 한다.

Leave a comment