# 7징 객체 분해

한 번에 다뤄야 하는 정보의 수를 줄이기 위해 본질적인 정보만 남기고 불필요한 세부 사항을 걸러내면 문제를 단순화할 수 있을 것이다. 이처럼 불필요한 정보를 제거하고 현재의 문제 해결에 필요한 핵심만 남기는 작업을 `추상화`라고 부른다.

사람들은 한 번에 해결하기 어려운 `커다란 문제`에 맞닥뜨릴 경우 해결 가능한 작은 문제로 나누는 경향이 있다. 이처럼 큰 문제를 해결 가능한 작은 문제로 나누는 작업을 `분해`라고 부른다.

따라서 `추상화`와 `분해`가 인류가 창조한 가장 복잡한 분야의 문제를 해결하기 위해 사용돼 왔다고 해도 놀랍지 않을 것이다. 그 분야는 바로 `소프트웨어 개발 영역`이다.

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

### 추상화 메커니즘

* 프로시저 추상화 : 소프트웨어가 무엇을 해야 하는지
  * 기능 분해(알고리즘 분해)
* 데이터 추상화 : 소프트웨어가 무엇을 알아야 하는지
  * 타입 추상화
  * 프로시저 추상화

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

### 메인 함수로서의 시스템

전통적인 기능 분해 방법은 `하향식 접근법(Top-Down Approach)`을 따른다. 하향식 접근법이란 시스템을 구성하는 가장 최상위(topmost) 기능을 정의하고 이 최상위 기능을 좀 더 작은 단계의 `하위 기능으로 분해`해 나가는 방법을 말한다. 각 세분화 단계는 바로 위 단계보다 더 구체적이어야 한다.

### 급여 관리 시스템

급여 관리 시스템을 구현하기 위해 기능 분해 방법을 사용해보자

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

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

![https://i.ibb.co/Z8QLgqp/9.png](https://i.ibb.co/Z8QLgqp/9.png)

### 하향식 기능 분해의 문제점

실제로 설계에 적용하다 보면 다음과 같은 다양한 문제에 직면한다.

#### 1. 시스템은 하나의 메인 함수로 구성돼 있지 않다.

시간이 지나고 사용자를 만족시키기 위한 새로운 요구사항을 도출해 나가면서 지속적으로 `새로운 기능`을 추가하게 된다. 모든 기능들은 규모라는 측면에서 차이가 있을 수는 있겠지만 가능성의 측면에서는 `동등`하게 독립적이고 완결된 하나의 기능을 표현한다.

#### 2. 기능 추가나 요구사항 변경으로 인해 메인 함수를 빈번하게 수정해야 한다.

기존 로직과는 아무런 상관이 없는 새로운 함수의 적절한 위치를 확보해야 하기 때문에 `메인 함수의 구조`를 급격하게 변경할 수밖에 없을 것이다. 기존 코드를 수정하는 것은 항상 새로운 버그를 만들어낼 확률을 높인다는 점에 주의하라.

#### 3. 비즈니스 로직이 사용자 인터페이스와 강하게 결합된다.

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

문제는 비즈니스 로직과 사용자 인터페이스가 변경되는 `빈도`가 다르다는 것이다. 따라서 사용자 인터페이스를 변경하는 경우 비즈니스 로직 까지 변경에 영향을 받게 된다. 따라서 하향식 접근법은 근본적으로 `변경에 불안정한 아키텍처`를 낳는다.

#### 4. 하향식 분해는 너무 이른 시기에 함수들의 실행 순서를 고정시키기 때문에 유연성과 재사용성이 저하된다.

하향식 설계는 시작하는 시점부터 시스템이 `무엇을 해야 하는지`가 아니라 `어떻게 동작`해야 하는지에 집중하도록 만든다. 그렇기 때문에 함수들의 실행 순서를 정의하는 시간 제약을 강조한다.

결과적으로 기능을 추가하거나 변경하는 작업은 매번 기존에 결정된 함수의 `제어 구조를 변경`하도록 만든다.

하향식 접근법을 통해 분해한 함수들은 `재사용하기도 어렵다.` 모든 함수는 상위 함수를 분해하는 과정에서 필요에 따라 식별되며, 그에 따라 상위 함수가 강요하는 문맥 안에서만 의미를 가지기 때문이다.

#### 5. 데이터 형식이 변경될 경우 파급효과를 예측할 수 없다.

하향식 기능 분해의 가장 큰 문제점은 어떤 데이터를 어떤 함수가 사용하고 있는지를 `추적하기 어렵다`는 것이다. 데이터의 영향 범위를 파악하기 위해서는 모든 함수를 열어 데이터를 사용하고 있는지를 모두 확인해봐야 하기 때문이다. 이를 해결하기 위해서는 `변경에 대한 영향을 최소화`하기 위해 영향을 받는 부분과 받지 않는 부분을 명확하게 분리하고 잘 정의된 `퍼블릭 인터페이스`를 통해 변경되는 부분에 대한 `접근을 통제`해야 한다.

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

하향식 아이디어가 매력적인 이유는 설계가 어느 정도 안정화된 후에는 설계의 다양한 측면을 논리적으로 설명하고 `문서화` 하기에 용이하기 때문이다. 그러나 설계를 문서화 하는 데 적절한 방법이 좋은 구조를 설계할 수 있는 방법과 동일한 것은 아니다.

## 모듈

#### 정보 은닉과 모듈

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

시스템을 모듈 단위로 어떻게 분해할 것인가? 시스템이 감춰야 하는 비밀을 찾아라. 외부에서 내부의 비밀에 접근하지 못하도록 커다란 방어막을 쳐서 에워쏴라. 이 방어막이 바로 `퍼블릭 인터페이스`가 된다.

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

* 복잡성 : 모듈이 너무 복잡한 경우 이해하고 사용하기 어렵다. 외부에 모듈을 `추상화`할 수 있는 `간단한 인터페이스`를 제공해서 모듈의 복잡도를 낮춘다.
* 변경 가능성 : 변경 가능한 설계 결정이 외부에 노출될 경우 실제로 변경이 발생했을 때 `파급효과`가 커진다. 변경 가능한 설계 결정을 `모듈 내부`로 감추고 외부에는 쉽게 변경되지 않을 인터페이스를 제공한다.

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

#### 추상 데이터 타입

타입은 저장된 값에 대해 수행될 수 있는 `연산의 집합`을 결정하기 때문에 변수의 값이 어떻게 행동할 것이라는 것을 예측할 수 있게 한다.

리스코프는 `프로시저 추상화`의 한계를 인지하고 이를 보완하기 위해 `데이터 추상화`의 개념을 제안했다.

추상 데이터 타입은 추상 객체의 클래스를 정의한 것으로 추상 객체에 사용할 수 있는 `오퍼레이션`을 이용해 규정된다. 추상 데이터 객체를 사용할 때 프로그래머는 오직 객체가 외부에 제공하는 행위에만 관심을 가지며 행위가 구현되는 세부적인 사항에 대해서는 무시한다.

#### 추상 데이터 타입을 구현하려면 다음의 특성을 위한 프로그래밍 언어의 지원이 필요하다.

* `타입 정의`를 선언할 수 있어야 한다.
* 타입의 인스턴스를 다루기 위해 사용할 수 있는 `오퍼레이션의 집합`을 정의할 수 있어야 한다.
* 제공된 오퍼레이션을 통해서만 조작할 수 있도록 데이터를 외부로부터 `보호`할 수 있어야 한다.
* 타입에 대해 여러 개의 인스턴스를 생성할 수 있어야 한다.

### 클래스

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

클래스와 추상 데이터 타입 모두 데이터 추상화를 기반으로 시스템을 분해하기 때문에 이런 설명이 꼭 틀린 것만은 아니다.

그러나 명확한 의미에서 추상 데이터 타입과 클래스는 동일하지 않다. 가장 핵심적인 차이는 클래스는 `상속`과 `다형성`을 지원하는 데 비해 추상 데이터 타입은 지원하지 못한다.

쿡의 정의를 빌리자면 추상 데이터 타입은 타입을 추상화한 것이고 클래스는 절차를 추상화한 것이다.

![https://i.ibb.co/jyqyYdK/10.png](https://i.ibb.co/jyqyYdK/10.png)

* 개별 오퍼레이션이 모든 개념적인 타입에 대한 구현을 포괄 하도록 함으로써 하나의 물리적인 타입 안에 전체 타임을 감춘다.
* 타입 추상화는 오퍼레이션을 기준으로 타입을 통합하는 데이터 추상화 기법이다.

![https://i.ibb.co/HKdzDYD/11.png](https://i.ibb.co/HKdzDYD/11.png)

* 타입을 기준으로 오퍼레이션을 묶는다.
* 두 가지 이상의 클래스로 분리할 경우 공통로직을 어디에 둘 것인지가 이슈
  * 공통 로직을 제공하기 위한 간단한 방법은 공통 로직을 포함할 부모 클래스를 정의하고 상속 시킨다.
* 클라이언트는 부모 클래스 참조자에 대해 메세지를 전송하면 실제 클래스가 무엇인지에 따라 다른 메소드가 실행된다.
* 실제로 내부에서 수행되는 절차는 다르지만 클래스를 이용한 다형성은 절차에 대한 차이점을 감춘다.
  * 따라서 객체지향은 절차 추상화(procedural abstraction)이다

#### 변경을 기준으로 선택하라

단순히 클래스를 구현 단위로 사용한다는 것이 객체지향 프로그래밍을 한다는 것을 의미하지는 않는다. `타입을 기준으로 절차를 추상화하지 않았다면 그것은 객체지향 분해가 아니다.`

클래스가 추상 데이터 타입의 개념을 따르는지를 확인할 수 있는 가장 간단한 방법은 클래스 내부에 인스턴스의 타입을 표현하는 변수가 있는지를 살펴보는 것이다.

```java
public Integer calculatePay(taxRate) {
    if(this.houly) return calculateHourlyPay(taxRate);
    return calculateSaliedPay(taxRate);
}
```

객체지향에서는 타입 변수를 이용한 조건문을 `다형성`으로 대체한다. 객체가 메시지를 처리할 적절한 메서드를 선택하게 된다.

![](https://2649832514-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-M5HOStxvx-Jr0fqZhyW%2F-MBgTdjTWo7QrEoE0-RY%2F-MBgVLvye-VXqwhJYv61%2FPolymorphism.jpg?alt=media\&token=5d04b92a-1d55-4eca-ab6f-d49f21e160b8)

이처럼 기존 코드에 아무런 영향도 미치지 않고 새로운 객체 유형과 행위를 추가할 수 있는 객체지향의 특성을 `개방-폐쇄 원칙(Open-Closed Principle, OCP)`이라고 부른다.

그렇다면 항상 절차를 추상화하는 객체지향 설계 방식을 따라야 하는가? 추상 데이터 타입은 모든 경우에 최악의 선택인가?

`새로운 타입을 빈번하게 추가해야 한다면 객체지향의 클래스 구조가 더 유용하지만 새로운 오퍼레이션을 빈번하게 추가해야 한다면 추상 데이터 타입을 선택하는 것이 현명한 판단이다.`

객체지향에서 중요한 것은 역할, 책임, 협력이다. 객체지향은 기능을 행하기 위해 객체들이 협력하는 방식에 집중한다. `협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션의 구현 방식을 타입별로 분배하는 것은 올바른 접근법이 아니다.`

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