7장 : 객체 분해
문제를 해결하기 위해서는 필요한 정보들을 먼저 단기 기억 안으로 불러들여야 한다.
한 번에 단기 기억에 담을 수 있는 추상화의 수에는 한계가 있지만 추상화를 더 큰 규모의 추상화로 압축시킴으로써 단기 기억의 한계를 초월할 수 있다.
1. 프로시저 추상화와 데이터 추상화
프로그래밍 언어의 발전은 좀 더 효과적인 추상화를 이용해 복잡성을 극복하려는 개발자들의 노력에서 출발했다. 어셈블리어는 숫자로 뒤범벅이 딘 기계어에 인간이 이해할 수 있는 상징을 부여하려는 노력의 결과다. 고수준 언어는 기게적인 사고를 강요하는 낮은 수준의 명령어들을 탈피해서 인간의 눈높이에 맞는 기계 독립적이고 의미 있는 추상화를 제공하려는 시도의 결과였다.
프로그래밍 패러다임은 프로그래밍을 구성하기 위해 사용하는 추상화의 종류와 이 추상화를 이용해 소프트 웨어를 분해하는 방법의 두 가지 요소로 결정된다. 따라서 모든 프로그래밍 패러다임은 추상화와 분해의 관점에서 설명할 수 있다.
추상화 매커니즘
프로시저 추상화(procedure abstraction)
소프트웨어가 무엇을 해야 하는지를 추상화
기능 분해(functional decomposition)
데이터 추상화(data abstraction)
소프트웨어가 무엇을 알아야 하는지를 추상화
알고리즘 분해(algorithmic decomposition)
데이터 추상화를 중심으로 시스템을 분해하기로 결정했다면 다시 두 가지 중 하나를 선택해야 한다.
데이터를 중심으로 타입을 추상화(type abstraction),
추상 데이터 타입(Abstract Data Type)
데이터를 중심으로 프로시저를 추상화(procedure abstraction),
객체지향(Object-Oriented)
프로그래밍 언어의 관점에서 객체지향을 바라볼 경우, 기능을 구현하기 위해 필요한 객체를 식별하고 협력 가능하도록 시스템을 분해한 후에는 프로그래밍 언어라는 수단을 이용해 실행 가능한 프로그램을 구현해야 한다. 프로그래밍 언어의 관점에서 객체지향이랑 데이터를 중심으로 데이터 추상화와 프로시저 추상화를 통합한 객체를 이용해 시스템을 분해하는 방법이다. 그리고 이런 객체를 구현하기 위해 대부분의 객체지향 언어는 클래스라는 도구를 제공한다.
→ 따라서 프로그래밍 언어적인 관점에서 객체지향을 바라보는 일반적인 관점은 데이터 추상화와 프로시저 추상화를 함께 포함한 클래스를 이용해 시스템을 분해하는 것이다.
2. 프로시저 추상화의 기능 분해
✔️ 메인 함수로서의 시스템
기능은 오랜 시간 동안 시스템을 분해하기 위한 기준으로 사용됐으며, 이 같은 시스템 분해 방식을 알고리즘 분해 또는 기능 분해라고 부른다. 기능 분해의 관점에서 추상화의 단위는 프로시저이며 시스템은 프로시저를 단위로 분해된다.
프로시저는 반복적으로 실행되거나 거의 유사하게 실행되는 작업들을 하나의 장소에 모아놓음으로써 로직을 재사용하고 중복을 방지할 수 있는 추상화 방법이다.
프로시저 중심의 기능 분해 관점에서 시스템은 입력 값을 계산해서 출력 값을 반환하는 수학의 함수와 동일하다. 시스템은 필요한 더 작은 작업으로 분해될 수 있는 하나의 커다란 메인 함수다.
전통적인 기능 분해 방법 **하향식 접근법(Top-Down Approach)**을 따른다.
✔️ 급여 관리 시스템
먼저 급여 관리 시스템에 대한 추상적인 최상위 문장을 기술함으로써 시작하자. 이 문장은 급여 관리 시스템을 시작하는 메인 프로시저로 구현될 것이다.
이제 기능 분해 방법에 따라 이 프로시저를 실제로 급여를 계산하는 데 필요한 좀 더 세분화된 절차로 구체화해야 한다.
각 정제 단계는 이전 문장의 추상화 수준을 감소시켜야 한다.
기능 분해의 결과는 최상위 기능을 수행하는 데 필요한 절차들을 실행되는 시간 순서에 따라 나열한 것이다. 기본적으로 기능 분해는 책의 목차를 정리하고 그 안에 내용을 채워 넣는 것과 유사하다.
기능 분해 방법에서는 기능을 중심으로 필요한 데이터를 결정한다.
이것은 유지보수에 다양한 문제를 야기한다. 하향식 기능 분해 방식이 가지는 문제점을 이해하는 것은 유지보수 관점에서 객체지향의 장점을 이해할 수 있는 좋은 출발점이다.
✔️ 급여 관리 시스템 구현
✔️ 하향식 기능 분해의 문제점
시스템은 하나의 메인 함수로 구성돼 있지 않다
기능 추가나 요구사항 변경으로 인해 메인 함수를 빈번하게 수정해야 한다
비즈니스 로직이 사용자 인터페이스와 강하게 결합된다
하향식 분해는 너무 이른 시기에 함수들의 실행 순서를 고정시키기 때문에 유연성과 재사용성이 저하된다
데이터 형식이 변경될 경우 파급효과를 예측할 수 없다
하나의 메인 함수라는 비현실적인 아이디어
어떤 시스템도 최초에 릴리스됐던 당시의 모습을 그대로 유지하지 않는다. 하향식 접근법은 하나의 알고리즘을 구현하거나 배치 처리를 구현하기에는 적합하지만 현대적인 상호작용 시스템을 개발하는 데는 적합하지 않다. 현대적인 시스템은 동등한 수준의 다양한 기능으로 구성된다. 버트란드 마이어의 말을 인용하자면 “실제 시스템에 정상(top)”이란 존재하지 않는다.
메인 함수의 빈번한 재설계
하향식 기능 분해의 경우에는 새로운 기능을 추가할 때마다 매번 메인 함수를 수정해야 한다. 결과적으로 기존 코드의 빈번한 수정으로 인한 버그 발생 확률이 높아지기 때문에 시스템은 변경에 취약해질 수밖에 없다.
비즈니스 로직과 사용자 인터페이스의 결합
결과적으로 코드 안에서 비즈니스 로직과 사용자 인터페이스 로직이 밀접하게 결합된다. 문제는 비즈니스 로직과 사용자 인터페이스가 변경되는 빈도가 다르다는 것이다. 이는 근본적으로 변경에 불안정한 아키텍처를 낳는다.
성급하게 결정된 실행 순서
하향식 설계와 관련된 모든 문제의 원인은 결합도다. 함수는 상위 함수가 강요하는 문맥에 강하게 결합된다. 함수는 함께 절차를 구성하는 다른 함수들과 시간적으로 강하게 결합돼 있다. 현재의 문맥에 강하게 결합된 시스템은 현재 문맥을 떠나 다른 문맥으로 옮겨갔을 때 재사용하기 어렵다. 가장 큰 문제는 전체 시스템의 핵심적인 구조를 결정하는 함수들이 데이터와 강하게 결합된다는 것이다.
데이터 변경으로 인한 파급효과
하향식 기능 분해의 가장 큰 문제점은 어떤 데이터를 어떤 함수가 사용하고 있는지를 추적하기 어렵다는 것이다. 따라서 데이터 변경으로 인해 어떤 함수가 영향을 받을지 예상하기 어렵다.
변경에 대한 영향을 최소화하기 위해 영향을 받는 부분과 받지 않는 부분을 명확하게 분리하고 잘 정의된 퍼블릭 인터페이스를 통해 변경되는 부분에 대한 접근을 통제하는 것이 의존성 관리의 핵심이다. 초기 소프트웨어 개발 분야의 선구자 중 한 명인 데이비드 파나스는 기능 분해가 가진 본질적인 문제를 해결하기 위해 이 같은 개념을 기반으로 한 정보 은닉과 모듈이라는 개념을 제시하기에 이르렀다.
✔️ 언제 하향식 분해가 유용한가 ?
하향식 아이디어가 매력적인 이유는 설계가 어느 정도 안정화된 후에는 설계의 다양한 측면을 논리적으로 설명하고 문서화하기에 용이하기 때문이다.
지금까지 하향식 설계가 가지는 문제점을 살펴봤다. 하향식 분해 방식으로 설계된 소프트웨어는 하나의 함수에 제어가 집중되기 때문에 확장이 어렵고, 프로젝트 초기에 설계의 본질적인 측면을 무시하고 사용자 인터페이스 같은 비본질적인 측면에 집중하게 만든다. 과도하게 함수에 집중하게 함으로써 소프트웨어의 중요한 다른 측면인 데이터에 대한 영향도를 차악하기 어렵게 만든다. 또한 하향식 분해를 적용한 설계는 근본적으로 재사용하기 어렵다.
3. 모듈
✔️ 정보 은닉과 모듈
시스템의 변경을 관리하는 기본적인 전략은 함께 변경되는 부분을 하나의 구현 단위로 묶고 퍼블릭 인터페이스를 통해서만 접근하도록 만드는 것이다. 즉, 기능을 기반으로 시스템을 분해하는 것이 아니라 변경의 방향에 맞춰 시스템을 분해하는 것이다.
정보 은닉(information hiding)은 시스템을 모듈 단위로 분해하기 위한 기본 원리로 시스템에서 자주 변경되는 부분을 상대적으로 덜 변경되는 안정적인 인터페이스 뒤로 감춰야 한다는 것이 핵심이다.
정보 은닉은 외부에 감춰야 하는 비밀에 따라 시스템을 분할하는 모듈 분할 원리다. 모듈은 변경될 가능성이 있는 비밀을 내부로 감추고, 잘 정의되고 쉽게 변겨오디지 않을 퍼블릭 인터페이스를 외부에 제공해서 내부의 비밀에 함부로 접근하지 못하게 한다.
모듈이 감춰야 하는 두 가지 비밀
복잡성 : 모듈이 너무 복잡한 경우 이해하고 사용하기가 어렵다. 외부에 모듈을 추상화할 수 있는 간단한 인터페이스를 제공해서 모듈의 복잡도를 낮춘다.
변경 가능성 : 변경 가능한 설계 셜정이 외부에 노출죌 경우 실제로 변경이 발생했을 때 파급효과가 커진다. 변경 발생 시 하나의 모듈만 수정하면 되도록 변경 가능한 설계 결정을 모듈 내부로 감추고 외부에는 쉽게 변경되지 않을 인터페이스를 제공한다.
✔️ 모듈의 장점과 한계
모듈의 장점
모듈 내부의 변수가 변경되더라도 모듈 내부에만 영향을 미친다
비즈니스 로직과 사용자 인터페이스에 대한 관심사를 분리한다
전역 변수와 전역 함수를 제거함으로써 네임스페이스 오염(namespace pollution)을 방지한다
모듈은 기능이 아니라 변경의 정도에 따라 시스템을 분해하게 한다. 각 모듈은 외부에 감춰야 하는 비밀과 관련성 높은 데이터와 함수의 집합이다. 따라서 모듈 내부는 높은 응집도를 유지한다. 모듈과 모듈 사이에는 퍼블릭 인터페이스를 통해서만 통신해야 한다. 따라서 낮은 결합도를 유지한다.
모듈의 가장 큰 단점은 인스턴스의 개념을 제공하지 않는다는 점이다. Employees 모듈은 단지 회사에 속한 모든 직원 정보를 가지고 있는 모듈일 뿐이다. 좀 더 높은 수준의 추상화를 위해서는 직원 전체가 아니라 개별 직원을 독립적인 단위로 다룰 수 있어야 한다. 다시 말해서 다수의 직원 인스턴스가 존재하는 추상화 메커니즘이 필요한 것이다. 그리고 이를 만족시키기 위해 등장한 개념이 바로 추상 데이터 타입이다.
4. 데이터 추상화와 추상 데이터 타입
✔️ 추상 데이터 타입
프로그래밍 언어에서 타입(type)이란 변수에 저장할 수 있는 내용물의 종류와 변수에 적용될 수 있는 연산의 가짓수를 의미한다. 정수 타입의 변수를 선언하는 것은 프로그램 내에서 변수명을 참조할 때 해당 변수를 임의의 정숫값으로 간주하라고 말하는 것과 같다. 타입은 저장된 값에 대해 수행될 수 있는 연산의 집합을 결정하기 때문에 변수의 값이 어떻게 행동할 것이라는 것을 예측할 수 있게 한다.
프로그래밍 언어는 다양한 형태의 내장 타입(built-in type)을 제공한다.
리스코프는 <Programming with Abstract Data Types>에서 프로시저 추상화를 보완하기 위해 데이터 추상화(data abstracton)
의 개념을 제안했다.
추상 데이터 타입을 구현하려면 다음과 같은 특성을 위한 프로그래밍 언어의 지원이 필요하다.
타입 정의를 선언할 수 있어야 한다.
타입의 인스턴스를 다루기 위해 사용할 수 있는 오퍼레이션의 집합을 정의할 수 있어야 한다.
제공된 오퍼레이션을 통해서만 조작할 수 있도록 데이터를 외부로부터 보호할 수 있어야 한다.
타입에 대해 여러 개의 인스턴스 생성할 수 있어야 한다.
추상 데이터 타입의 기본 의도는 프로그래밍 언어가 제공하는 타입처럼 동작하는 사용자 정의 타입을 추가할 수 있게 하는 것이다. 프로그래밍 언어의 관점에서 추상 데이터 타입은 프로그래밍 언어의 내장 데이터 타입과 동일하다. 단지 타입을 개발자가 정의ㅣ할 수 있다는 점이 다를 뿐이다.
5. 클래스
✔️ 클래스는 추상 데이터 타입인가 ?
추상 데이터 타입과 클래스 공통점
두 메커니즘 모두 외부에서는 객체의 내부 속성에 직접 접근할 수 없으며 오직 퍼블릭 인터페이스를 통해서만 외부와 의사소통할 수 있다.
추상 데이터 타입과 클래스 차이점
클래스는 상속과
다형성
을 지원하는 데 비해 추상 데이터 타입은 지원하지 못한다는 점이다.
상속과 다형성을 지원하는 객체지향 프로그래밍(Object-Oriented Programming)과 구분하기 위해 상속과 다형성을 지원하지 않는 추상 데이터 타입 기반의 프로그래밍 패러다임을 객체기반 프로그래밍(Object-Based Programming)이라고 부르기도 한다.
추상 데이터 타입은 오퍼레이션을 기준으로 타입들을 추상화한다. 클래스는 타입을 기준으로 절차들을 추상화한다.
✔️ 추상 데이터 타입에서 클래스로 변경하기
✔️ 변경을 기준으로 선택하라
코드에 아무런 영향도 미치지 않고 새로운 객체 유형과 행위를 추가할 수 있는 객체지향의 특성을 개방-폐쇄 원칙(Open-Closed Principle, OCP)이라고 부른다.
새로운 타입을 빈번하게 추가해야 한다면 객체지향의 클래스 구조가 더 유용하다. 새로운 오퍼레이션을 빈번하게 추가해야 한다면 추상 데이터 타입을 선택하는 것이 현명한 판단이다.
✔️ 협력이 중요하다
객체가 참여할 협력을 결정하고 협력에 필요한 책임을 수행하기 위해 어떤 객체가 필요한지에 관해 고민하라. 그 책임을 다양한 방식으로 수행해야 할 때만 타입 계층 안에 각 절차를 추상화하라. 타입 계층과 다형성은 협력이라는 문맥 안에서 책임을 수행하는 방법에 관해 고민한 결과물이어야 하며 그 자체가 목적이 되어서는 안 된다.
Last updated