7징 객체 분해

오브젝트의 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)는 시스템을 구성하는 하나의 프로시저를 의미하고 한 노드의 자식 노드는 부모 노드를 구현하는 절차중의 한 단계를 의미한다.

하향식 기능 분해의 문제점

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

모듈

정보 은닉과 모듈

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

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

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

  • 복잡성 : 모듈이 너무 복잡한 경우 이해하고 사용하기 어렵다. 외부에 모듈을 추상화할 수 있는 간단한 인터페이스를 제공해서 모듈의 복잡도를 낮춘다.

  • 변경 가능성 : 변경 가능한 설계 결정이 외부에 노출될 경우 실제로 변경이 발생했을 때 파급효과가 커진다. 변경 가능한 설계 결정을 모듈 내부로 감추고 외부에는 쉽게 변경되지 않을 인터페이스를 제공한다.

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

추상 데이터 타입

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

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

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

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

  • 타입 정의를 선언할 수 있어야 한다.

  • 타입의 인스턴스를 다루기 위해 사용할 수 있는 오퍼레이션의 집합을 정의할 수 있어야 한다.

  • 제공된 오퍼레이션을 통해서만 조작할 수 있도록 데이터를 외부로부터 보호할 수 있어야 한다.

  • 타입에 대해 여러 개의 인스턴스를 생성할 수 있어야 한다.

클래스

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

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

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

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

  • 개별 오퍼레이션이 모든 개념적인 타입에 대한 구현을 포괄 하도록 함으로써 하나의 물리적인 타입 안에 전체 타임을 감춘다.

  • 타입 추상화는 오퍼레이션을 기준으로 타입을 통합하는 데이터 추상화 기법이다.

  • 타입을 기준으로 오퍼레이션을 묶는다.

  • 두 가지 이상의 클래스로 분리할 경우 공통로직을 어디에 둘 것인지가 이슈

    • 공통 로직을 제공하기 위한 간단한 방법은 공통 로직을 포함할 부모 클래스를 정의하고 상속 시킨다.

  • 클라이언트는 부모 클래스 참조자에 대해 메세지를 전송하면 실제 클래스가 무엇인지에 따라 다른 메소드가 실행된다.

  • 실제로 내부에서 수행되는 절차는 다르지만 클래스를 이용한 다형성은 절차에 대한 차이점을 감춘다.

    • 따라서 객체지향은 절차 추상화(procedural abstraction)이다

변경을 기준으로 선택하라

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

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

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

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

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

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

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

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

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

Last updated