오브젝트; 하향식 기능 분해

🗓️

  • STM
  • LTM
  • 인지 과부하
  • 추상화
  • 분해

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

시스템을 분해하는 방법

  • 프로시저 추상화 Procedure abstraction : 소프트웨어가 무엇을 해야 하는지 추상화
    • 기능 분해 Functional decomposition 또는 알고리즘 분해 Algorithm decomposition
  • 데이터 추상화 Data abstraction : 소프트웨어가 무엇을 알아야 하는지 추상화
    • 데이터를 중심으로 타입을 추상화 Type abstraction : 추상 데이터 타입 Abstract data type
    • 데이터를 중심으로 프로시저를 추상화 Procedure abstraction : 객체지향 Object-Oriented

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

메인 함수로서의 시스템

시스템 분해 방식

  • 기능 분해 or 알고리즘 분해
  • 기능 분해 관점에서 추상화의 단위는 프로시저.

기능 분해 방법

  • 하향식 Top-down 접근법 : 시스템을 구성하는 가장 최상위 기능을 정의하고, 이 최상위 기능을 좀 더 작은 단계의 하위 기능으로 분해해 나가는 방법.
  • 마지막 하위 기능이 프로그래밍 언어로 구현 가능한 수준이 될 때 까지 분해함.
  • 하위 기능은 상위 기능보다 덜 추상적이여야 함.

예제

최상위 문장

직원의 급여를 계산한다.

첫번째 세분화

직원의 급여를 계산한다.
    사용자로부터 소득세율을 입력받는다.
    직원의 급여를 계산한다.
    양식에 맞게 결과를 출력한다.
public static void main(String[] args) {
    String name = "아이유";
    double taxRate = getTaxRate();
    double pay = calculatePayFor(name, taxRate);
    System.out.println(describeResult(name, pay));
}

getTaxRate()의 세부 분해

직원의 급여를 계산한다.
    사용자로부터 소득세율을 입력받는다.
        "세율을 입력하세요: "라는 문장을 화면에 출력한다
        키보드를 통해 세율을 입력 받는다.
    직원의 급여를 계산한다.
    양식에 맞게 결과를 출력한다.
private static double getTaxRate() {
    System.out.print("세율을 입력하세요: ");
    Scanner scanner = new Scanner(System.in);
    double taxRate = scanner.nextDouble();
    scanner.close();
    return taxRate;
}

급여 계산의 분해

직원의 급여를 계산한다.
    사용자로부터 소득세율을 입력받는다.
        "세율을 입력하세요: "라는 문장을 화면에 출력한다
        키보드를 통해 세율을 입력 받는다.
    직원의 급여를 계산한다.
        전역 변수에 저장된 직원의 기본급 정보를 얻는다
        급여를 계산한다
    양식에 맞게 결과를 출력한다.
private static String[] employees = {"아이유", "이지금", "이지동"};
private static double[] basePays = {400, 300, 250};

private static double calculatePayFor(String name, double taxRate) {
    int index = Arrays.asList(employees).indexOf(name);
    double basePay = basePays[index];
    return basePay - (basePay * taxRate);
}

양식의 세분화

직원의 급여를 계산한다.
    사용자로부터 소득세율을 입력받는다.
        "세율을 입력하세요: "라는 문장을 화면에 출력한다
        키보드를 통해 세율을 입력 받는다.
    직원의 급여를 계산한다.
        전역 변수에 저장된 직원의 기본급 정보를 얻는다
        급여를 계산한다
    양식에 맞게 결과를 출력한다.
        "이름: {직원명}, 급여: {계산된 금액}" 형식에 따라 출력 문자열을 생성한다.
private static String describeResult(String name, double pay) {
    return "이름: " + name + ", 급여: " + pay + "원";
}
  • 하향식 기능 분해는 논리적이고 체계적인 시스템 개발 절차를 제시한다.
  • 그러나 현실은 완벽히 체계적이거나 이상적이지 않다.

하향식 기능 분해의 문제점

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

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

실제 시스템에 top이란 존재하지 않는다.

  • 대부분의 시스템은 하나의 메인기능이란 개념은 존재하지 않는다.
  • 하향식 접근법은 하나의 알고리즘을 구현하거나 배치 처리를 구현하기에는 적합하지만 현대적인 상호작용 시스템에는 적합하지 않다.
  • 현대적인 시스템은 동등한 수준의 다양한 기능으로 구성된다.

메인 함수의 빈번한 재설계

기본급의 총합을 계산하는 기능이 추가되었을때 다음과 같이 main()을 수정해야 한다

public enum OperationType {
        PAY, BASE_PAY
}

private static String[] employees = {"아이유", "이지금", "이지동"};

public static void main(String[] args) {
    Map<String, Object> arguments = new HashMap<>();
    arguments.put("name", "이지금"); // 예시로 사용된 이름, 실제로는 적절한 값으로 설정해야 합니다.

    OperationType operation = OperationType.PAY;
    switch (operation) {
        case PAY:
            String name = (String) arguments.get("name");
            calculatePay(name);
            break;
        case BASE_PAY:
            sumOfBasePays();
            break;
        default:
            System.out.println("유효하지 않은 연산입니다.");
    }
}
  • 변경에 취약한 코드.

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

  • 하향식 접근법은 처리 과정과 결과를 하나의 양식으로 강제하는 경우가 있음
  • 비즈니스 로직과 사용자 인터페이스 간 “관심사의 분리”가 어려움

성급하게 결정된 실행 순서

  • 하향식 관점으로 분해를 하면 하면 시스템이 무엇을 해야 하는지가 아니라 어떻게 동작해야 하는지 집중하게 만든다.
  • 시간 제약 : 구현부터 먼저 생각하기 때문에 순서를 강제하게 한다. 기능 분해 방식은 중앙집중 제어의 스타일을 띈다.
  • 이렇게 분해된 함수들은 상위 함수에 대해 의존, 즉 결합도가 매우 높기 때문에 재사용이 어렵다.

이를 해결하기 위해 시간 제약에 대한 관점을 버리고 논리적 제약을 설계 기준으로 감으면 된다.

  • 객체지향은 함수간 호출 순서가 아니라 객체 사이의 논리적인 관계를 중심으로 설계해야 한다.

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

  • 어떤 데이터가 어떤 함수를 사용하고 있는지 추적이 어렵다.
  • 데이터 변경으로 인한 파급효과를 예측하기 어렵다.
  • 이것 역시 의존성과 결합도의 문제.

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

  • 이미 완전히 이해된 사실을 서술할 때
  • 완료된 결과에 대한 명확한 아이디어가 있을 때
  • 세부사항만 정하면 될 때

모듈

정보 은닉과 모듈

시스템을 모듈로 분할하는 원칙은 외부에 유출돼서는 안되는 비밀의 윤곽을 따라야 한다.

  • 정보은닉 : 외부에 감춰야 하는 비밀에 따라 시스템을 분할하는 모듈 분할 원리

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

  • 복잡성 : 모듈이 너무 복잡한 경우 이해하고 사용하기가 어렵다. 외부에 모듀을 추상화할 수 있는 간단한 인터페이스를 제공해서 모듈의 복잡도를 낮춘다.
  • 변경 가능성 : 변경 가능한 설계 결정이 외부에 노출될 경우 실제로 변경이 발생했을 때 파급효과가 커진다. 변경 발생 시 하나의 모듈만 수정하면 되도록 변경 가능한 설계 결정을 모듈 내부로 감추고 외부에는 쉽게 변경되지 않을 인터페이스를 제공한다.
public class Employees {
    private static String[] employees = {"아이유", "이지금", "이지동"};
    private static double[] basePays = {400, 300, 250};
    private static boolean[] hourlys = {false, false, false};
    private static int[] timeCards = {0, 120, 250};

    public static double calculatePay(String name, double taxRate) {
        double pay;
        if (hourlys(name)) {
            pay = calculateHourlyPayFor(name, taxRate);
        } else {
            pay = calculatePayFor(name, taxRate);
        }
        return pay;
    }

    private static boolean hourlys(String name) {
        int index = Arrays.asList(employees).indexOf(name);
        return hourlys[index];
    }

    private static double calculateHourlyPayFor(String name, double taxRate) {
        int index = Arrays.asList(employees).indexOf(name);
        double basePay = basePays[index] * timeCards[index];
        return basePay - (basePay * taxRate);
    }

    private static double calculatePayFor(String name, double taxRate) {
        int index = Arrays.asList(employees).indexOf(name);
        double basePay = basePays[index];
        return basePay - (basePay * taxRate);
    }

    public static double sumOfBasePays() {
        double result = 0;
        for (String name : employees) {
            if (!hourlys(name)) {
                int index = Arrays.asList(employees).indexOf(name);
                result += basePays[index];
            }
        }
        return result;
    }
}

모듈의 장점과 한계

  • 모듈 내부의 변수가 변경되더라도 모듈 내부에만 영향을 미친다.
    • 데이터가 변경됐을때 영향을 받는 함수를 찾기 위해 해당 데이터를 정의한 모듈만 검색하면 됨.
  • 비즈니스 로직과 사용자 인터페이스에 대한 관심사를 분리한다.
  • 전역 변수와 전역 함수를 제거함으로써 네임스페이스 오염을 방지한다.
    • 이름 충돌 name collision 의 방지

모듈에 있어 핵심은 데이터다. 메인 함수를 정의하고 필요에 따라 더 세부적인 함수로 분해하는 하향식 기능 분해와 달리 모듈은 감춰야 할 데이터를 결정하고 이 데이터를 조작하는 데 필요한 함수를 결정한다. 모듈은 데이터와 함수가 통합된 한 차원 높은 추상화를 제공하는 설계 단위다.

모듈은 인스턴스의 개념을 제공하지 않는다. 좀 더 높은 수준의 추상화를 위해 직원 전체가 아니라 개별 직원을 독립적인 단위로 다룰 수 있어야 한다.