7. 객체 분해
문제를 해결하는 좋은 방법은 한 번에 다루어야하는 정보의 수를 줄여서 본질적인 정보만 남기는 것이다.
이처럼 불필요한 정보를 제거하고 현재의 문제 해결에 필요한 핵심만 남기는 작업을 추상화(abstraction) 라고 한다.
가장 일반적인 추상화 방법은 큰 문제를 해결 가능한 크기의 작은 문제로 게속해서 쪼개나가는 것으로 이러한 작업을 분해(decomposition) 이라고 부른다.
이 추상화화 분해는 사람이 문제를 인지하고 해결하기 위한 가장 기본적인 사고 도구이다.
7.1. 프로시저 추상화와 데이터 수상화
현대적인 프로그래밍 언어를 특징 짓는 두 가지 매커니즘은 프로시저 추상화(procedure abstraction) 와 데이터 추상화(data abstractio) 이다.
프로시저 추상화는 소프트웨어가 무엇을 해야하는지 추상화하며, 데이터 추상화는 소프트웨어가 무엇을 알아야하는 지 추상화한다.
결론적으로 소프트웨어는 데이터를 이용해 정보를 표현하고 프로시저를 이용해 데이터를 조작한다.
이제 시스템을 분해 해보도록 하자.
7.1.1. 프로시저 추상화를 중심으로 하는 시스템의 분해
프로시저 추상화는 시스템을 기능을 기준으로 분해한다.
이 기능 분해(functional decompostion) 는 알고리즘 분해(algorithm decomposition) 이라고도 한다.
7.1.2. 데이터 추상화를 중심으로 하는 시스템의 분해
데이터 추상화는 시스템을 데이터를 중심으로 타입 추상화(type abstraction) 을 하거나, 데이터를 중심으로 프로시저 추상화한다.
전자를 추상 데이터 타입, 후자를 객체지향이라고 부른다.
7.2. 프로시저 추상화와 기능 분해
기능은 오랜 시간 동안 시스템을 분해하기 위한 기준으로 사용되었다.
상술했듯 기능 분해의 관점에서 추상화의 단위를 프로시저이며, 시스템은 프로시저를 단위로 분해된다.
프로시저는 반복적으로 실행되거나 거의 유사하게 실행되는 작업들을 하나의 장소에 모아놓음으로써 로직의 재사용 및 중복을 방지할 수 있는 추상화 방법이다.
전통적인 기능분해 방식은 하향식 접근법(Top-Down Approach) 을 따른다.
하향식 접근법이란 시스템을 구성하는 가장 최상위(topmost) 기능을 정의하고, 이 최상위 기능을 좀 더 작은 단계의 하위 기능으로 분해해 나가는 방법을 말한다.
이때 분해는 세분화된 마지막 하위 기능이 프로그래밍 언어로 구현 가능한 수준이 될때까지 계속되며 점진적으로 구체화 된다.
설명만 보면 이상적으로 생갈될 수 있지만, 실제로 적용하다보면 아래와 같은 다양한 문제를 마주치게 된다.
- 시스템은 하나의 메인 함수로 구성되어 있지않다.
- 기능 추가나 요구사항의 변경으로 메인 함수를 빈번하게 수정해야 한다.
- 비즈니스 로직이 사용자 인터페이스와 강하게 결합된다.
- 하향식 분해는 너무 이른 시기에 함수들의 실행 순서를 고정시키기때문에 유연성과 재사용성이 저하된다.
- 데이터 형식이 변경될 경우 파급 효과를 예측할 수 없다.
하나씩 자세하게 살펴보도록 하자.
7.2.1. 시스템은 하나의 메인 함수로 구성되어 있지않다.
대부분의 프로그램은 시간이 지나고 사용자를 만족시키기 위한 새로운 요구사항을 도출해나가면서 지속적으로 새로운 기능을 추가하게 된다.
이는 시스템이 오직 하나의 메인 함수만으로 구현된다는 개념과 완전히 모순된다.
대부분의 시스템에서 하나의 메인 기능이란 개념은 희미하고, 모든 기능들은 기능성의 측면에서 독립적이고 완결된 하나의 기능을 표현하게 되므로,
모든 기능을 자식으로 가지는 루트 기능을 선택하긴 어려워진다.
7.2.2. 기능 추가나 요구사항의 변경으로 메인 함수를 빈번하게 수정해야 한다.
하나의 메인 함수를 유일한 정상으로 간주하는 하향식 기능 분해는 새로운 기능 추가를 위해 매번 메인 함수를 수정해야하는 부작용을 낳는다.
기존 코드의 빈번한 수정은 결국 버그의 발생 확률을 높이는 데 일조하게되고, 이는 변경에 취약한 시스템으로 이어진다.
7.2.3. 비즈니스 로직이 사용자 인터페이스와 강하게 결합된다.
하향식 접근법은 비즈니스 로직의 설계부터 사용자의 입력 방법과 출력 형태를 함게 고민하게 된다.
이는 입력과 출력을 강결합 시키며, 관심사의 분리를 달성하기 어렵게 만든다.
7.2.4. 하향식 분해는 너무 이른 시기에 함수들의 실행 순서를 고정시키기때문에 유연성과 재사용성이 저하된다.
실행 순서나 조건, 반복과 같은 제어 구조를 미리 결정하지않고는 분해를 진행할 수 없기에 기능 분해 방식은 중앙집중 제어 스타일의 형태를 보인다.
이는 모든 중요한 제어 흐름의 결정이 상위 함수에서 이뤄지고, 하위 함수는 상위 함수의 흐름에 따라 적절한 시점에 호출됨을 의미한다.
문제는 이 함수의 제어 구조가 빈번한 변경의 대상이 된다는 점이다.
기능이 추가되거나 변경될 때마다 초기에 결정된 함수들의 제어 구조를 계속해서 변경해야하는 상황이 놓이게 되며,
결정권이 상위 함수에 있기에 분해한 함수들의 재사용 도한 제한된다.
이처럼 하향식 설계와 관련된 모든 문제의 원인은 각 기능들이 가진 결합도이다.
강한 결합도는 시스템을 변경에 취약하게 만들고 이해하기 어렵게 만든다.
7.2.5. 데이터 형식이 변경될 경우 파급 효과를 예측할 수 없다.
하향식 기능 분해의 가장 큰 문제점은 어떤 데이터를 어떤 함수가 사용하고 있는지 추적하기 어렵다는 점이다.
따라서 데이터의 변경이 어떤 함수에 어떤 영향을 미치는 지 파악하기 어려운 상황에 놓인다.
7.3. 모듈
시스템의 변경을 관리하는 기본전인 전략은 변경되는 부분을 하나의 구현 단위로 묶고 퍼블릭 인터페이스를 통해서만 접근하도록 하는 것이다.
즉, 기능이 아닌 변경에 방향에 맞춰 시스템을 분해해야한다.
이때 중요한 원리로 정보 은닉(information hiding) 이 필요하다.
정보 은닉은 시스템을 모듈 단위로 분해하기 위한 기본 원리로 시스템에서 자주 변경되는 부분을 상대적으로 덜 변경되는 안정적인 인터페이스 위로 감춰야한다는 것이다.
이때 모듈 단위를 도입하는 것이 권장되는데,
모듈은 연관있는 부분을 하나의 단위로 묶어서 퍼블릭 인터페이스로 제공하는 것을 말한다.
모듈은 변경될 가능성이 있는 비밀을 내부로 감추고, 잘 정의되고 쉽게 변경되지않을 퍼블릭 인터페이스를 외부로 제공함으로써, 내부의 비밀에 함부로 접근하지 못하게 한다.
모듈은 다음과 같은 두 가지 비밀을 감춰야 한다.
- 복잡성 : 모듈이 너무 복잡한 경우 이해하고 사용하기가 어렵다. 외부에 모듈을 추상화할 수 있는 간단한 인터페이스를 제공해서 모듈의 복잡도를 낮춘다.
- 변경 가능성 : 변경 가능한 설계 결정이 외부에 노출될 경우, 실제로 변경이 발생했을 때 파급효과가 커진다. 변경 발생시 하나의 모듈만 수정하면 되도록 변경 가능한 설계 결정을 모듈 내부로 감춰야 한다.
모듈화를 통해 달성할 수 있는 장점은 다음과 같다.
- 모듈 내부의 변수가 변경되더라도 모듈 내부에만 영향을 미친다.
- 비즈니스 로직과 사용자 인터페이스에 대한 관심사를 분리한다.
- 전역 변수와 전역 함수를 제거함으로써 네임스페이스의 오염을 방지한다.
7.4. 데이터 추상화와 추상 데이터 타입
프로시저 추상화의 한계를 인지한 후 나온 대안이 바로 데이터 추상화이다.
프로그래밍 언어에서 타입(type) 이란 변수에 저장할 수 있는 내용물의 종류와 변수에 적용될 수 있는 연산의 가짓수를 의미한다.
이후 추상 데이터 타입(Abstract Data Type) 의 개념이 나오면서 본격적으로 추상화의 수준이 올라가기 시작하였다.
추상 데이터 타입을 구현하려면 프로그래밍 언어가 아래와 같은 기능을 제공해야 한다.
- 타입 정의를 선언할 수 있어야 한다.
- 타입의 인스턴스를 다루기 위해 사용할 수 있는 오퍼레이션의 집합을 정의할 수 있어야 한다.
- 제공된 오퍼레이션을 통해서만 조작할 수 있도록 데이터를 외부로부터 보호할 수 있어야 한다.
- 타입에 대해 여러 개의 인스턴스를 생성할 수 있어야 한다.
위의 기능들을 보면 간파할 수 있겠지만, 추상 데이터 타입의 구현을 위해서는 데이터의 추상화, 정보의 은닉, 데이터의 캡슐화, 인터페이스와 구현의 분리 등을 모두 필요로 한다.
7.5. 클래스
객체지향 프로그래밍에서 제일 먼저 마주치는 클래스라는 녀석은 추상 데이터 타입일까?
대부분의 프로그래밍 서적에서는 클래스를 추상 데이터 타입이라고 설명한다.
꼭 틀린 것은 아지지만, 명확한 의미에서 클래스와 추상 데이터 타입을 동일하지 않다.
가장 핵심적인 차이는 상속과 다형성이다.
클래스를 상속과 다형성을 지원하지만, 추상 데이터 타입은 지원할 수 없기 때문이다.
따라서 추상 데이터 타입을 클래스로 변경하여 다형성의 개념을 주입한다면 좀 더 유연한 설계를 가질 수 있다.