[오브젝트] #7. 객체 분해

bien·2024년 10월 30일
0

오브젝트

목록 보기
7/13
  • 기억의 종류
    • 장기기억; 매우 큰 저장용량 보유
    • 단기기억; 정보 보관의 속도와 공간에 제약 있음
      • 주요 연구:
        • 조지 밀러: 매직넘버 7 (7 ± 2) - 사람이 기억할 수 있는 정보의 개수
        • 하버트 사이먼: 새로운 정보를 받아들이는 데 걸리는 시간 약 5초
  • 문제 해결에 주로 사용되는 저장소: 단기기억
    • 인지 과부하(Cognitive Overload)
      • 문제 해결에 필요한 요소의 수가 단기기억의 용량을 초과할 때 문제 해결 능력의 급격한 저하를 보이는 것.
  • 인지 과부하 방지 전략; 단기기억에 보관할 정보의 양을 조절
    • 추상화 (Abstraction)
      • 불필요한 정보를 제거하고 문제 해결에 필요한 핵심만 남기는 작업
  • 일반적인 추상화 방법: 한 번에 다뤄야 하는 문제의 크기를 줄이는 것.
    • 분해 (Decomposition)
      • 큰 문제를 해결 가능한 작은 문제들로 나누는 것.
        • 이는 조지밀러가 이야기한 단기기억의 한계 용량인 매직넘버 7이 청크(chunk)를 단위로 한다는 점에서 유의미해진다.
  • 청크 (Chunk)
    • 하나의 단위로 취급될 수 있는 논리적 단위
    • 특징:
      • 더 작은 청크를 포함할 수 있음
      • 연속적으로 분해 가능
  • 결론
    • 단기기억의 한계는 추상화를 기반으로 큰 규모의 문제를 압축함으로써 극복 가능
    • 이를 통해 더 복잡한 문제도 효과적으로 해결할 수 있게 됨

이 추상화와 분해라는 기능을 통해 복잡한 기능을 청크 단위로 이해할 수 있다.
소프트웨어 개발 영역에서 어떻게 추상화와 분해를 이용해 복잡한 현실세계의 문제들을 해결해왔는지 알아보자.

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

거대하고 복잡한 문제를 해결하기 위해 추상화 기법이 이용된다.
추상화란; 거대하고 식별하기 어려운 문제를, 응집성있는 모듈화를 통한 분해로, 인식하기 쉽게 하려는 노력이다.
따라서 추상화를 위해서는 어떻게 분해할 것인지, 어떻게 응집할 것인지를 고려해야 한다.

  • 프로그래밍 언어
    • 정의; 효과적인 추상화를 이용해 복잡성을 극복하려는 개발자들의 노력에서 출발
    • 발전과정
      1. 기계어: 숫자로만 이루어진 저수준 언어
      2. 어셈블리어: 기계어에 인간이 이해할 수 있는 상징을 부여
      3. 고수준 언어: 기계 독립적이고 의미있는 추상화 제공
  • 프로그래밍 패러다임
    • 정의; 적절한 추상화의 윤곽을 따라 시스템을 어떤 식으로 나눌지를 결정하는 원칙과 방법의 집합
    • 구성요소
      1. 프로그래밍을 구성하기 위해 사용되는 추상화의 종류
        • 어떻게 응집할 것인지
      2. 추상화를 이용한 소프트웨어 분해 방법
        • 어떻게 분해할 것인지
  • 현대적 프로그래밍 언어의 추상화 메커니즘
    1. 프로시저 추상화: 소프트웨어의 행동(무엇을 해야할지)을 추상화
    2. 데이터 추상화: 소프트웨어의 지식(무엇을 알아야할지)을 추상화

시스템 분해방법

  1. 추상화의 방식 결정
    • 프로시저 추상화 또는 데이터 추상화 중 선택
  2. 프로시저 추상화 기반 분해
    • 기능분해(functional decomposition) (= 알고리즘 분해(algorithmic decomposition))
    • 각 기능을 독립적인 프로시저로 분리하여 시스템을 구성
    • 예시: 은행 계좌 관리 시스템
은행계좌관리()
├── 계좌개설()
├── 입금처리()
├── 출금처리()
├── 잔액조회()
└── 계좌해지()
  1. 데이터 추상화 기반 분해

    • 추상 데이터 타입(Abstraction Data Type)

      • 데이터를 중심으로 데이터의 타입을 추상화(type abstraction)
      • 데이터와 그 데이터를 다루는 연산을 하나의 단위로 묶음.
      • 예시: 스택(Stack) ADT
        Stack ADT
        ├── 데이터: 요소들의 집합
        └── 연산:
            ├── push(element)
            ├── pop()
            ├── peek()
            └── isEmpty()
    • 객체지향(Object-Oriented)

      • 데이터를 중심으로 프로시저를 추상화(procedure abstraction)

      • 실세계 객체를 모델링하여 시스템을 구성

      • 예시: 은행 계좌 시스템의 객체지향 설계

        class 계좌 {
          속성:
            - 계좌번호
            - 소유자
            - 잔액
        
          메서드:
            - 입금(금액)
            - 출금(금액)
            - 잔액조회()
        }

객체지향의 경우

  • 객체지향 패러다임이 이용하는 추상화; 역할과 책임을 수행하는 객체
  • 객체지향 패러다임의 분해; 기능을 협력하는 공동체를 구성하도록 객체들로 나누는 과정
  • 프로그래밍 언어에서의 객체지향;
    • 데이터 추상화와 프로시저 추상화를 함께 포함한 클래스를 이용해 시스템을 분해하는 것.

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

시스템 분해 중 더 이전부터 오랜시간 사용되던 기준은 기능 분해였다.

프로시저 추상화; 메인 함수로서의 시스템

  • 알고리즘 분해 (=기능 분해)
    • 시스템을 기능을 기준으로 분해하는 방식.
    • 추상화의 단위; 프로시저
      • (시스템은 프로시저를 단위로 분해된다)
  • 프로시저 (추상화)
    • 반복적으로 실행되거나 거의 유사하게 실행되는 작업들을 하나의 장소에 모아놓음으로써 로직을 재사용하고 중복을 방지할 수 있는 추상화 방법
      • 내부의 상세한 구현 내용을 모르더라도 인터페이스만 알면 프로시저 사용 가능
      • 프로시저는 잠재적으로 정보은닉(information hiding)의 가능성을 제시한다.
    • 프로시저 기능 분해 관점에서 시스템
      • = 수학의 함수
        • 입력 값을 계산 해 출력값을 반환
      • = 필요한 더 작은 작업으로 분해될 수 있는 하나의 커다란 메인 함수
  • 하향식 접근법(Top-Down Approach)
    • 전통적 기능 분해 방법이 따르는 방식
    • 시스템을 구성하는 가장 최상위(topmost) 기능을 정의하고, 이 최상위 기능을 좀 더 작은 단계의 하위 기능으로 분해해 나가는 방법
    • 분해는 세분화된 마지막 하위 기능이 프로그래밍 언어로 구현 가능한 수준이 될 때까지 반복.
    • 정제된 기능은 자신의 바로 상위 기능보다 덜 추상적이어야 한다.

🧾 예제; 급여 관리 시스템

요구사항

  • 매달 지급해야 하는 기본급에 대해 직원과 협의하여 이 금액을 12개월 동안 동일하게 직원들에게 지급한다.
  • 회사는 급여 지급 시 소득 세율에 따라 일정 금액의 소금을 공제한다.
  • 실제로 지급받는 급여는 다음과 같다.
    • 급여 = 기본급 - (기본급 * 소득세율)

기능 분해 방법 적용

  • 하향식 접근법을 따른다.
    1. 최상위의 추상적인 함수 정의
      • 시스템을 표현하는 하나의 문장을 종의한다.
    2. 단계적인 정제 절차를 따라 시스템 구축
      • 좀 더 세부적인 단계의 문장으로 분해

1. 최상위 문장 정의

  • 최상위 문장; 시스템을 시작하는 메인 프로시저로 구현된다.
    • 직원의 급여를 계산한다.

2. 세부적 절차

  • 이전 문장의 추상화 수준을 감소시킨다.
    • 모든 문장이 정제 과정을 거치며 단순하고 구체적인 조합으로 분해되어야 한다.
  • 개발자는 각 단계에서 불완전하고 좀 더 구체화될 수 있는 문장들이 남아있는지 검토한다.
직원의 급여를 계산한다.
   사용자로부터 소득세율을 입력받는다.
   직원의 급여를 계산한다.
   양식에 맞게 결과를 출력한다.

3. 최종 절차

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

💻 구현; 급여 관리 시스템

루비 언어를 통해 구현을 해보자!

1. 최상위 문장

  • 직원의 급여를 계산한다.
    • 하나의 메인 함수로 매핑된다.
def main(name)
end

2. 세부적 절차

직원의 급여를 계산한다.
   사용자로부터 소득세율을 입력받는다.
   직원의 급여를 계산한다.
   양식에 맞게 결과를 출력한다.
  • 위의 세 단계는 더 작은 세부적인 단계로 분해 가능하므로, 각 단계를 프로시저를 호출하는 명령문으로 변환할 수 있다.
def main(name)
	taxRate = getTaxRate()
    pay = calculatePayFor(name, taxRate)
    puts(describeResult(name, pay))
end    

3. 최종 절차

  • 루비에서 전역변수의 이름은 반드시 $로 시작해야 한다.
   사용자로부터 소득세율을 입력받는다.
      "세율을 입력하세요:"라는 문장을 화면에 출력한다.
      키보드를 통해 세율을 입력받는다.
   직원의 급여를 계산한다.
      전역 변수에 저장된 직원의 기본급 정보를 얻는다.
      급여를 계산한다.
   양식에 맞게 결과를 출력한다.
      "이름: {직원명}, 급여: {계산된 금액}" 형식에 따라 출력 문자열을 생성한다.
def getTaxRate()
	print("세율을 입력하세요: ")
    return gets().chomp().to_f()
end    

# 직원의 이름과 급여를 동일한 인덱스에 저장
$employees = ["직원A", "직원B", "직원C"]
$basePays = [400, 300, 250]

# 급여 계산 절차
def calculatePayFor(name, taxRate)
	index = $emplyees.index(name)
    basePay = $basePays[index]
    return basePay - (basePay * taxRate)
end

def describeResult(name, pay)
	return "이름: #{name}, 급여: #{pay}"
end
  • 이름이 "직원C"인 직원의 급여 계산
main("직원C")

하향식 기능 분해의 특징

  • 하향식 기능 분해
    1. 최상위의 가장 추상적인 메인함수 정의
    2. 메인 함수를 구현 가능한 수준까지 세부적인 단계로 분해
  • 하향식 기능 분해를 적용한 시스템
    • 메인 함수를 루트로 하는 트리(tree)로 표현 가능
    • 각 노드(node)는 시스템을 구성하는 프로시저를 의미.
    • 자식 노드는 부모 노드의 구현 절차 중 한 단계를 의미한다.

  • 논리적으고 체계적인 시스템 개발 절차를 제시한다.
    • 그러나 실세계의 개발은 불규칙적이고 불완전하다.

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

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

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

  • 어떤 시스템도 최초 배포 당시의 모습을 유지하지 않으며, 지속적으로 새로운 기능을 추가한다.
    • 이 점이 하나의 메인함수라는 개념과 완전히 상충된다.
  • 대부분의 시스템에서 하나의 메인기능이라는 개념은 존재하지 않는다.
    • 모든 기능들은 기능성의 측면에서 동등하게 독립적이고 완결된 하나의 기능을 표현한다.
  • 하향식 접근법은 하나의 알고리즘 혹은 배치 처리에는 적합하나, 현대적인 상호작용 시스템 개발에는 적합하지 않다.
  • 실제 시스템에 정상(top)이란 존재하지 않는다. -버트란드 마이어
    • 정상(top); 분해가 시작되는 메인함수
  • 훌륭한 프로그램은 많은 정상(top)을 명확하게 제시하고, 훌륭한 아키텍처는 정상들을 우아하게 표현한다. -제임스 코플리엔

2. 메인 함수의 빈번한 재설계

  • 시스템 내부에 여러 개의 정상이 존재하므로, 하나의 메인함수를 유일한 정상으로 간주하는 하향식 기능 분해의 경우, 새로운 기능이 추가될 때마다 메인 함수를 수정해야 한다.
    • 기존 코드의 수정은 버그 발생 확률을 높인다.

모든 직원들의 기본급 총합을 구하는 기능 추가.

  • $basePays에 저장돼 있는 직원들의 모든 급여를 더하기
def sumOfBasePayse()
	result = 0
    for basePay in $basePays
    	result += basePay
    end
    puts(result)
end
  • 기존의 메인 함수는 직원 각각의 급여 계산이 목적이므로, 추가된 기능이 들어설 자리가 마땅찮다.
    • 따라서, 기존의 main 함수 안의 로직을 새로운 함수 calculatePay함수로 옮긴다.
    • 동등한 수준의 작업을 수행하는 calculatePaysumOfBasePays를 선택적으로 호출하도록 수정한다.
def calculatePay(name)
	taxRate = getTaxRate()
    pay = calculatePayFor(name, taxRate)
    puts(describeResult(name, pay))
end

def main(operation, args={})
	case(operation)
    when :pay then calculatePay(args[:name])
    when :basePays then sumOfBasePayse()
    end
end

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

  • 하향식 접근법
    • 설계 초기부터 입력방식출력 양식을 함께 고민하게 한다.
    • 사용자로부터 소득세율을 입력받아 급여를 계산한 후 계산된 결과를 화면에 출력한다.
      • 비즈니스 로직과사용자 인터페이스 로직이 밀접하게 결합된다.
  • 비즈니스 로직과 사용자 인터페이스는 변경 빈도가 다르다.
    • 사용자 인터페이스는 자주 변경되는 부분이다.
      • 따라서 비즈니스 로직과 결합도가 높은 경우, 변경이 불안정한 아키텍처가 설계된다.
  • 현재 예세에서 사용자 인터페이스가 GUI로 변경된다면,
    • 현 설계에서는 비즈니스 로직과 사용자 인터페이스 로직이 main함수 안에 뒤섞여 있다.
      • 따라서 사용자 인터페이스 변경의 유일한 방법은 시스템 재설계 뿐이다.

4. 성급하게 결정된 실행 순서

  • 하향식 기능 분해; 함수 분해 + 순서 결정의 과정
    • 설계 시작부터 무엇(what)을 해야하는지가 아니라 어떻게(how) 동작해야 하는가에 집중하게 한다.
  • 처음부터 구현을 염두에 두어, 함수의 실행 순서를 정의하는 시간 제약(temporal constraint)이 강조된다.
    • 메인함수의 분해를 위해
  • 중앙집중 제어 스타일(centralized control style)
  • 하향식 접근법
    • 특징1. 시간 제약(temporal constraint)이 강조됨
      • 시간제약: 함수들의 실행 순서를 정의
      • 처음부터 구현을 염두에 두고 설계하여 유발되는 특징
      • 메인함수의 분해는 실행순서를 우선적으로 결정해야만 가능하다.
    • 특징2. 중앙집중 제어 스타일(centralized control style)
      • 중앙집중 제어스타일; 모든 중요한 제어 흐름의 결정이 상위함수에서 이루어지고, 하위함수는 적절한 시점에 호출됨.
      • 제어구조(실행순서, 조건, 반복 등)를 미리 결정해야만 분해가 가능하다는 점에서 유발되는 특징
    • 문제점1. 함수의 제어구조는 빈번하게 변경된다.
      • 기능 추가 및 변경 시 기존의 제어구조가 변경됨.
    • 해결법. 시간적 제약을 버리고, 안정적인 논리적 제약(logical constraint)을 따르는 것.
      • 객체지향은 함수간의 호출 순서가 아니라, 객체 사이의 논리적 관계를 중심으로 설계를 이끌어 나간다.
      • 따라서, 전체적인 시스템의 제어구조가 한 구성요소로 집중되지 않고, 여러 객체들 사이로 제어가 분산된다.
    • 문제점2. 함수의 낮은 재사용성
      • 모든 함수가 상위함수의 분해 과정에서 필요가 식별된다.
        • 즉, 상위 함수가 강요하는 문맥(context) 안에서만 의미를 가진다.
      • 재사용성 = 일반성이란 의미를 내포
        • 함수가 재사용 가능하려면, 상위 함수보다 더 일반적이어야 한다.
        • 하향식 접근법에서, 분해한 하위 함수는 항상 상위함수보다 문맥에 종속적이다.
          • 정확히 재사용성과 반대되는 개념.
  • 하향식 설계와 관련된 모든 문제의 원인은 결합도다.
    • 상위 함수의 문맥에 대한 강한 결합
    • 함께 절차를 구현하는 다른 함수와의 시간적 결합
  • 현재의 문맥에 강하게 결합된 시스템은 다른 문맥으로 옮겨갔을 때 재사용이 어렵다.

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

  • 가장 큰 문제점; 어떤 데이터를 어떤 함수가 사용하고 있는지 추적하기 어렵다.
    • 즉, 데이터 변경으로 영향을 받게 될 함수를 예상하기 어렵다.
    • 함수가 어떤 데이터를 사용하는지 파악: 쉬움 (함수 몸체를 확인하면 됨)
    • 데이터를 어떤 함수가 사용하는지 파악: 매우 어려움 (모든 함수를 열어 확인해야 함)
  • 데이터 추적의 어려움 = 의존성 & 결합도 / 테스트의 문제
    • 이터 변경이 데이터를 참조하는 모든 함수로 전파됨
      • 스파게티처럼 얽히고 설킨 대규모 시스템에서 데이터 참조 함수를 모두 찾아 정상 동작 여부를 체크하는 것은 기술보다는 운의 영역이다. 한발 삐끗하는 순간 수많은 버그와 장애가 유발된다.

💻 새로운 기능 & 기능변경 예시코드

  • 기능추가) 아르바이트 직원에 대한 개발 급여 계산 기능
    • 아르바이트 직원은 고정 급여가 아니라, 일한 시간 * 시급의 금액을 지급받는다.
$employees = ["직원A", "직원B", "직원C", "아르바이트D", "아르바이트E", "아르바이트F"] # 직원 이름
$basePays = [400, 300, 250, 1, 1, 1.5] # 기본급여
$hourlys = [false, false, false, true, true, true] # 아르바이트 직원 여부
$timeCards = [0, 0, 0, 120, 120, 120] # 업무 누적 시간(정규직원은 0)
  • 애플리케이션 안의 데이터가 변경되었다.
    • 정규직원 정보를 관리하던 $employees$basePays가 알바 직원 정보도 함께 관리하도록 수정되었다.
    • 새롭게 $houlyees$timeCards정보가 추가되었다.
  • 변경된 데이터를 사용하는 모든 함수를 편집해야 한다.
    • $employees$basePays를 사용하는 모든 함수를 함께 처리하도록 변경해야 한다.
    • $houlyees$timeCards의 값도 함께 사용하도록 변경해야 한다.
def calculatePay(name)
	taxRate = getTaxRate()
    if (hourly?(name)) then
    	pay = calculateHourlyPayFor(name, taxRate)
    else
    	pay = calculatePayFor(name, taxRate)
    end
    puts(describeResult(name, pay))
end

def calculatehourlyPayFor(name, taxRate)
	index = $employees.index(name)
    basePay = $basePays[index] * $timeCards[index]
    return basePay - (basePay * taxRate)
end

def hourly?(name)
	return $hourlys[$employees.index(name)]
end

💣 subOfBasePays 함수로의 side effect

  • 요청사항에 맞추어 claculatePay함수를 변경했으나, 예상치 못하게 sumOfBasePays에서 결과가 이상이라는 버그가 발생했다.
    • $basePays$employees에 알바 정보를 추가하면서 영향이 퍼지게 된 것이다.
  • 현재 $basePays에서는 알바 직원의 시급도 저장되어 있으므로, 시급을 총합에서 제외하는 기능을 추가해야 한다.
def sumOfBasePays()
	result = 0
    for name in $employees
    	if (not hourly?(name)) then
        	result += $basePayse[$employees.index(hame)]
        end
    end
    puts(result)
end    

이처럼 데이터의 변경으로 인해 발생하는 함수에 대한 영향도는 파악하기 어렵다.
코드가 성장하고 라인 수가 증가할 수록 전역 데이터를 변경하는 것은 더 어려워진다.

⭐️ 결론

  • 데이터 변경으로 인한 영향의 최소화
    • 데이터로 인해 변경되는 부분과, 그렇지 않은 부분명확히 분리
    • 데이터로 함께 변경되는 부분하나의 구현으로 묶고, 외부에서는 제공되는 함수만 이용해 데이터에 접근해야 한다.
      • 즉, 잘 정의된 퍼블릭 인터페이스를 통해 데이터 접근을 제한해야 한다.
  • 의존성 관리의 핵심;
    • 변경에 영향을 받는 부분과 받지 않는 부분의 명확한 분리
    • 변경되는 부분의 통제 (퍼블릭 인터페이스를 통한)

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

  • 하향식 분해 = 설계가 어느정도 안정화된 이후에 유용
    • 설계의 다양한 측면을 논리적으로 설명하고 문서화하기에 용이하기 때문
  • 하향식은 이미 완전히 이해된 사실을 서술하기에 적합한 방법이다. 그러나 새로운 것을 개발하고, 설계하고, 발견하는 데는 적합한 방법이 아니다.
  • 시스템이나 프로그램 개발자가 이미 완료한 결과에 대한 명확한 아이디어를 가지고 있다면 머릿속에 있는 것을 종이에 서술하기 위해 하향식을 사용할 수 있다.
  • 하향식 단계가 시작될 때 문제는 이미 해결됐고, 오직 해결돼야만 하는 세부사항만이 존재할 뿐이다.[Jackson83].

📌 결론

  • 하향식 설계의 문제
    1. 하나의 함수에 제어가 집중 => 확장의 어려움
    2. 프로젝트 초기 설계에 본질적인 측면보다 (사용자 인터페이스와 같은) 비본질적인 것에 집중해야 함
    3. 데이터에 대한 영향도를 파악하기 어려움
    4. 재사용의 어려움
  • 이미 이해한 알고리즘의 문서화 & 서술에 적합한 기법
  • 실제 동작하는 거대한 소프트웨어 설계에 적합한 방식은 아님

03. 모듈

모듈이란? (ft. 정보은닉)

  • 시스템 변경 관리의 기본 전략
    • 함께 변경되는 부분은 하나의 구현으로 묶기 + 퍼블릭 인터페이스로 접근 제한
      • 기능이 아니라 변경의 방향에 맞춘 시스템 분해
  • 정보 은닉(information hiding)
    • 시스템을 모듈 단위로 분해하기 위한 기본 원리
    • 시스템에서 자주 변경되는 부분을 상대적으로 더 변경되는 안정적인 인터페이스 뒤로 감추는 것
      • 시스템 분할 원칙 = 외부에 감춰야 하는 비밀에 따라 시스템을 분할

모듈은 서브 프로그램이라기 보다는 책임의 할당이다. 모듈화는 개별적인 모듈에 대한 작업이 시작되기 전에 정해져야 하는 설계 결정들을 포함한다. (...) 분할된 모듈은 다른 모듈에 대해 감춰야 하는 설계 결정에 따라 특징지어진다. 해당 모듈 내부의 작업을 가능한 한 적게 노출하는 인터페이스 또는 정의를 선택한다. (...) 어려운 설계 결,ㅡ정이나 변화할 것 같은 설계 결정들의 목록을 사용해 설계를 시작할 것을 권장한다. 이러한 결정이 외부 몯류에 대해 숨겨지도록 각 모듈을 설계해야 한다.[Parnas72].

1. 모듈과 기능분해

  • 모듈과 기능분해는 상호 배타적인 관계가 아니다.
    • 기능 분해;
      • 하나의 기능을 구현하기 위해 필요한 기능들을 순차적으로 찾아가는 탐색의 과정
    • 모듈 분해;
      • 감춰야 하는 비밀을 선택하고, 비밀 주변에 안정적인 보호막을 설치하는 보존의 과정
  • 시스템을 모듈로 분해한 후에는 각 모듈 내부를 구현하기 위해 기능 분해를 적용할 수 있다.
    1. 비밀을 결정하고,
    2. 모듈을 분해한 후에는,
    3. 기능 분해를 이용해 모듈에 필요한 퍼블릭 인터페이스를 구현할 수 있다.

2. 모듈 분해 방법; 비밀

  • 시스템을 어떻게 모듈 단위로 분해해야 할까?
    • 시스템이 감춰야 하는 비밀을 찾아라.
      • 외부에서 내부의 비밀에 접근하지 못하도록 커다란 방어벽을 에워싸야 한다.
      • 이 방어막이 바로 퍼블릭 인터페이스가 된다.
  • 모듈이 감춰야 하는 2가지 비밀
    1. 복잡성
      • 모듈이 너무 복잡한 경우 이해하고 사용하기가 어렵다. 외부에 모듈을 추상화할 수 있는 간단한 인터페이스를 제공해서 모듈의 복잡도를 낮춘다.
    2. 변경 가능성
      • 변경 가능한 설계 결정이 외부에 노출도리 경우 실제로 변경이 발생했을 때 파급효과가 커진다. 변경 발생 시 하나의 모듈만 수정하면 되도록 변경 가능한 설계 결정을 모듈 내부로 감추고 외부에는 쉽게 변경되지 않을 인터페이스를 제공한다.

시스템의 가장 일반적인 비밀은 데이터다. 이 관점에서 데이터 캡슐화와 정보 은닉이 혼동스럽게 느껴질 수 있다. 비밀이 반드시 데이터일 필요는 없으며 복잡한 로직이나 변경 가능성이 큰 자료 구조일수도 있다. 그럼에도 변경 시 가장큰 위험요소는 대부분 데이터가 변경되는 경우다.

3. 모듈 구현

  • 모듈은 키워드의 지원여부와 관겨없이 적용할 수 있는 논리적인 개념이다.
    • 과거에 모듈로 시스템을 분할하는 방법은 개별 모듈을 별도의 물리적인 파일로 변경하는 것이었다.
      • C언어; 파일단위로 모듈 관리
      • 자바; 패키지(packae)
      • C++. C#; 네임스페이스(namespace)

💻 모듈 캡슐화 예시; Employees 모듈

moduel Employees
	$employees = ["직원A", "직원B", "직원C", "아르바이트D", "아르바이트E", "아르바이트F"] # 직원 이름
    $basePays = [400, 300, 250, 1, 1, 1.5] # 기본급여
    $hourlys = [false, false, false, true, true, true] # 아르바이트 직원 여부
    $timeCards = [0, 0, 0, 120, 120, 120] # 업무 누적 시간(정규직원은 0)

    def calculatePay(name)
        taxRate = getTaxRate()
        if (hourly?(name)) then
            pay = calculateHourlyPayFor(name, taxRate)
        else
            pay = calculatePayFor(name, taxRate)
        end
        puts(describeResult(name, pay))
    end

    def calculatehourlyPayFor(name, taxRate)
        index = $employees.index(name)
        basePay = $basePays[index] * $timeCards[index]
        return basePay - (basePay * taxRate)
    end

    def hourly?(name)
        return $hourlys[$employees.index(name)]
    end	

    def sumOfBasePays()
        result = 0
        for name in $employees
            if (not hourly?(name)) then
                result += $basePayse[$employees.index(hame)]
            end
        end
        puts(result)
    end    
  • 오직 모듈에 제공하는 퍼블릭 인터페이스를 통해서만 내부 변수를 조작할 수 있다.
    • 모듈 외부에서는 모듈 내부의 데이터에 직접 접근할 수 없다.
    • 심지어 모듈 외부에서는 모듈 내부에 어떤 데이터가 존재하는지 조차 알 수 없다.

💻 모듈 사용 예시

def main(operation, args={})
	case(operation)
    when: pay then calculatePay(args[:name])
    when: basePays then sumOfBasePays()
    end
end

def calculatePay(name)
	taxRate = getTaxRate()
    pay = Employees.calculatePay(name, taxRate)
    puts(describeResult(name, pay))
end

def getTaxRate()
	print("세율을 입력하세요: ")
    return gets().chomp().to_f()
end    

def describeResult(name, pay)
	return "이름: #{name}, 급여: #{pay}"
end    

def sumOfBasePays()
	puts(Employees.sumOfBasePayse())
end    

모듈의 장점과 한계

모듈의 장점

  • 모듈 내부의 변수가 변경되더라도 모듈 내부에만 영향을 미친다.
    • 모듈 내부에 정의된 변수를 직접 참조하는 코드가 모듈 내부로 제한된다.
    • 특정 데이터 변경 시 영향을 받는 함수를 찾기 위해 해당 데이터를 정의한 모듈만 검색하면 된다.
      • 전체 함수를 일일이 분석 및 비교할 필요가 없다.
    • 모듈은 데이터 변경으로 인한 파급효과를 제어하므로, 코드 수정 및 디버깅이 용이하다.
  • 비즈니스 로직과 사용자 인터페이스에 대한 관심사를 분리한다.
    • 수정된 코드에서 Employees 모듈은 비즈니스 로직과 관련된 관심사만을 담당한다.
    • 사용자 인터페이스와 관련된 관심사는 모두 Employees 모듈을 사용하는 main 함수 쪽에 위치한다.
    • GUI와 같은 다른 형식의 사용자 인터페이스가 추가되더라도 Employees 모듈에 포함된 비즈니스 로직은 변경되지 않는다.
  • 전역 변수와 전역 함수를 제거함으로써 네임스페이스 오염(namespace pollution)을 방지한다.
    • 모듈; 네임스페이스 제공
      • 네임스페이스 = 변수가 유효한 범위 한정. 변수의 소속
      • 변수와 함수 모두 모듈 내부에 포함되므로, 다른 모듈에서도 동일한 이름을 사용할 수 있다.
      • 따라서 모듈은 전역 네임 스페이스의 오염 방지 & 이름 충돌의 위험을 완화한다.

모듈의 의의

  • 모듈은 정보 은닉이라는 개념을 통해 데이터라는 존재를 설계의 중심 요소로 부각시킨다.
    • 모듈의 핵심은 데이터다.
    • 모듈은 감춰야 할 데이터를 결정하고, 이 데이터를 조작하는데 필요한 함수를 결정한다.
      • 즉, 기능이 아닌 데이터를 중심으로 시스템을 분해한다.
  • 모듈은 데이터와 함수가 통일한 차원 높은 추상화를 제공하는 설계 단위이다.

모듈의 한계

  • 모듈이 프로시저 추상화 보다는 높은 단계의 추상화 개념을 제공하지만, 태생적으로 변경을 관리하기 위한 구현 기법이기에 추상화 관점에서 한계점이 명확하다.
  • 모듈의 가장 큰 단점; 인스턴스의 개념을 제공하지 않는다는 점.
    • Employees 모듈은 단지 회사에 속한 모든 직원 정보를 가지고 있는 모듈일 뿐이다.
    • 좀 더 높은 수준의 추상화를 위해서는 전체 직원이 아니라 개별 직원독립적인 단위로 다룰 수 있어야 한다.
      • 즉, 다수의 직원 인스턴스가 존재하는 추상화 메커니즘이 필요하다.
      • 이를 만족시키기 위해 등장한 개념이 추상 데이터 타입이다.

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

추상 데이터 타입

타입 Type

  • 타입(type)
    • 변수에 저장할 수 있는 내용물의 종류와 변수에 적용될 수 있는 연산의 가짓수
  • 타입은 저장된 값에 대해 수행할 수 있는 연산의 집합을 결정하므로, 변수의 값이 어떻게 행동할지 예측할 수 있다.
    • 정수 타입의 변수 선언 = 프로그램 내에서 변수 선언 시 해당 변수를 임의의 정숫값으로 간주하라는 의미
    • 정수 타입 변수의 덧셈연산 = 값 더하기
    • 문자열 타입 변수의 연결연산 = 문자열 합치기

이 시대의 프로그램에서 사용하는 주된 추상화는 프로시저 추상화였다.
시간이 흐르면서 사람들은 프로시저 추상화로는 프로그램의 표현력을 향상시키는 데 한계가 있다는 사실을 발견했다.

데이터 추상화 (data abstraction)

  • 데이터 추상화 (data abstraction)
    • 바바라 리스코프(Barbara Liskov)에 의해 제안됨
    • 프로시저 추상화를 보완하기 위해 제안됨
      • 사람은 직원의 급여를 계산한다라는 하나의 커다란 절차를 이용한 사고보다(프로시저 추상화) 직원,과 급여라는 추상적인 개념들을 머릿속에 떠올린 후 이들을 이용해 계산에 필요한 절차를 생각하는 데(데이터 추상화) 익숙하다.
      • 추상 데이터 타입은프로시저 추상화 대신 데이터 추상화를 기반으로 소프트웨어를 개발하게 한 최초의 발걸음이다.

안타깝게도 프로시저만으로는 충분히 풍부한 추상화의 어휘집을 제공할 수 없다. (...) 이것은 언어 설계에서 가장 중요한 추상 데이터 타입(Abstract Data Type) 개념으로 우리를 인도했다. 추상 데이터 타입은 추상 객체의 클래스를 정의한 것으로 추상 객체에 사용할 수 있는 오퍼레이션을 이용해 규정된다.(데이터 추상화, 데이터 캡슐화) 이것은 오퍼레이션을 이용해 추상 데이터 타입을 정의할 수 있음을 의히만다. (...) 추상 데이터 객체를 사용할 때 프로그래머는 오직 객체가 외부에 제공하는 행위에만 관심을 가지며 행위가 구현되는 세부적인 사항에 대해서는 무시한다.(인터페이스-구현 분리) 객체가 저장소 내에서 어떻게 표현되는지와 같은 구현 정보는 오직 오퍼레이션을 어떻게 구현할 것인지에 집중할 때만 필요하다. 객체의 사용자는 이 정보를 알거나 제공받을 필요가 없다.(정보은닉)[Liskov74].

위 인용문은 데이터 추상화, 정보 은닉, 데이터 캡슐화, 인터페이스-구현 분리의 개념들이 모두 다 녹아들어있다.

추상 데이터 타입 구현에 필요한 프로그래밍 언어의 지원

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

💻 급여 관리 시스템 개선 예시코드; by 추상 데이터 타입

내부 캡슐화 할 데이터

  • $employees; 직원의 이름
  • $basePays; 기본급
  • $hourlys; 아르바이트 직원 여부
  • $timeCards; 아르바이트 직원일 경우 한달 간 작업시간
Employee = Struct.new(:name, :basePay, :houlry, :timeCard) do
End

적용할 오퍼레이션 목록

  1. claculatePay; 직원의 유형에 따른 급여계산
    • 외부로부터 전달받던 직원의 이름이 Employee타입 내부에 포함돼 인자로 받을 필요가 없다.
Employee = Struct.new(:name, :basePay, :hourly, :timeCard) do
	def calculatePay(taxRate)
    	if (hourly) then
        	return calculateHourlyPay(taxRate)
        end
        return calculateSalariedPay(taxRate)
    end
    
private
	def calculateHourlyPay(taxRate)
        return (basePay * timeCard) - (basePay * timeCard) * taxRate
    end
    def calculateSalariedPay(taxRate)
    	return basePay - (basePay * taxRate)
    end
end     
  1. monthlyBasePay; 개별직원의 기본급 계산
    • 정규직의 경우 기본급 반환
    • 아르바이트 직원인 경우 기본급 개념이 없으므로 0을 반환
Employee = Struct.new(:name, :basePay, :hourly, :timeCard) do
	def monthlyBasePay()
    	if (hourly) then return 0 end
        return basePay
    end
end

클라이언트 코드

  1. 필요한 직원들의 인스턴스 준비
$employees = [
	Employee.new("직원A", 400, false, 0),
    Employee.new("직원B", 300, false, 0),
    Employee.new("직원C", 250, false, 0),
    Employee.new("아르바이트D", 1, true, 120),
    Employee.new("아르바이트E", 1, true, 120),
    Employee.new("아르바이트F", 1, true, 120),
  1. 특정 직원의 급여계산
def calculatePay(name)
	taxRate = getTaxRate()
    for each in $employees
    	if (each.name == name) then employee = each; break end
    end
    pay = employee.calculatePay(taxRate);
    puts(describeResult(name, pay))
end
  1. 정규직원 전체에 대한 기본급 총합 구하기
def sumOfBasePays()
	result = 0
    for each in $employees
    	result += each.mounthlyBasePay()
    end
    puts(result)
end

추상 데이터 타입 특징

  • 추상 데이터 타입은 사람들이 세상을 바라보는 방식에 더 근접해지도록 추상화 수준을 향상시킨다.
    • 일상 생활에서 Employee는 상태와 행위를 가지는 독립적인 객체의 의미를 가진다.
      • 따라서, 개별 인스턴스를 생성할 수있는 추상 데이터 타입은 전체 직원을 캡슐화하는 모듈보다 더 사람의 사고방식에 가깝다.

추상 데이터 타입 한계

  1. 데이터와 기능의 분리
    • 타입 정의를 기반으로 객체 생성이 가능하나, 여전히 데이터와 기능을 분리해서 바라본다.
    • 데이터에 대한 관점을 설계의 표면으로 끌어올리기는 하지만, 여전히 데이터와 기능을 분리하는 절차지향적 설계의 틀에 갇혀있다.
  2. 제한된 데이터 중심 설계
    • 추상 데이터 타입은 시스템의 상태를 저장할 데이터를 표현한다.
    • 데이터를 이용해 기능을 구현하는 핵심 로직은 추상데이터 타입 외부에 존재한다.
  3. 프로그래밍 언어 수준의 한계
    • 추상데이터 타입의 기본 의도;
      • 프로그래밍 언어가 제공하는 타입처럼 동작하는 사용자 정의 타입 추가 기능
    • 프로그래밍 언어 관점에서 추상 데이터 타입은 프로그래밍언어의 내정 언어 타입과 동일하다.

05. 클래스

그렇다면 클래스는 추상 데이터 타입인가?

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

  • 공통점
    • 데이터 추상화를 기반으로 시스템을 분해한다.
    • 외부에서는 객체의 내부 속성에 직접 접근할 수 없으며 오직 퍼블릭 인터페이스를 통해서만 외부와 의사소통할 수 있다.
  • 차이점
    • 클래스는 상속과 다형성을 지원한다. (cf. 추상 데이터 타입은 지원하지 못한다)
      • 객체지향 프로그래밍(Object-Oriented Programming)
        • 상속과 다형성 지원
      • 객체기반 프로그래밍(Object-Based Programming)
        • 상속과 다형성을 지원하지 않는 추상데이터 타입 기반의 프로그래밍 패러다임
    • 프로그래밍 언어에서의 차이점. (by 윌리엄 쿡)
      • 추상 데이터 타입; 타입을 추상화 한 것(type abstraction)
      • 클래스; 절차를 추상화한 것(procedural abstraction)

예시로 알아보는 차이점; 급여 관리 시스템

추상 데이터 타입

  • Employee 타입
    • 물리적; 하나의 타입
    • 개념적; 2개의 개별적인 개념을 포괄 (정규직원 + 아르바이트 직원)
  • Employee가 제공하는 2개의 퍼블릭 인터페이스; calcualtePay, monthlyBasePay
    • 정규직원과 아르바이트 직원의 경우 다른 금액을 제공

타입 추상화

  • 하나의 타입으로 보이는 Employee내부에 두개의 타입이 공존한다.
    • 설계의 관점에서 Employee타입은 구체적인 직원 타입을 외부에 캡슐화하고 있다.
    • 이처럼 하나의 대표 타입에 세부적인 타입을 감추기 때문에, 이를 타입 추상화라고 부름. (by. 윌리엄 쿡)
      • 개별 오퍼레이션이 모든 개념적 타입에 대한 구현을 포괄하도록 함으로써 하나의 물리적 타입 안에 전체 타입을 감춘다.
      • 타입 추상화는 오퍼레이션을 기준으로 타입을 통합하는 데이터 추상화 기법이다. (오퍼레이션 안에 타입이 감춰져있다)
  • 예시에서 Employee를 사용하는 클라이언트는 calcualtePaymonthlyBasePay를 호출할 수 있으나, 정규 직원이나 아르바이트 직원이 있다는 사실은 알 수 없다. (오퍼레이션은 알 수 있으나 타입은 알 수 없다)
    • 두 직원은 Employee 내부에 감춰져 있으며 암묵적이다.

객체지향

  • 타입을 기준으로 오퍼레이션을 묶는다.
    • 정규 직원, 아르바이트 직원이라는 두 개의 타입을 명시적으로 정의한다.
    • 두 직원 유형과 관련된 오퍼레이션의 실행 절차를 두 타입에 분배한다.
      • 정규 직원과 아르바이트 직원 각각에 대한 클래스가 정의되고, 각 클래스들이 calculatePaymonthlyBasePay 오퍼레이션을 적절하게 구현한다.
  • 두 가지 클래스로 분리될 경우, 공통 로직을 어디에 둘 것인지가 문제가 된다.
    • 공통 로직을 제공할 수 있는 가장 간단한 방법은, 공통 로직을 포함할 부모 클래스를 정의하고 두 직원 유형이 부모 클래스를 상속받는 것이다.
    • 클라이언트는 부모 클래스의 참조자에 대해 메시지를 전송하면 실제 클래스가 무엇인가에 따라 적절한 절차가 실행된다.
      • 즉, 동일한 메시지에 대해 서로 다르게 반응한다.
        • 이것이 바로 다형성이다.
  • 클라이언트 관점에서 두 클래스의 인스턴스는 동일하게 보인다.
    • 내부에서 수행되는 절차는 다르지만, 클래스를 이용한 다형성은 절차에 대한 차이점을 감춘다. (타입안에 오퍼레이션의 차이점을 감춘다)
      • 다시 말해 객체지향은 절차 추상화(procedural abstraction)이다.

📌 결론

  • 추상화와 분해의 관점에서 차이점
    • 추상 데이터 타입; 오퍼레이션을 기준으로 타입들을 추상화한다.
    • 클래스; 타입을 기준으로 절차들을 추상화한다.

💻 급여 관리 시스템 개선 예시코드; by 클래스

  • 클래스를 이용하는 객체지향 버전
    • 각 직원 타입을 독립적인 클래스로 구현하여, 두 개의 타입이 존재함을 명시적으로 표현
    • 클래스를 이용한 구현; Employee 추상 데이터 타입에 구현되어 있던 타입별 코드가 두 개의 클래스로 분배
  • ruby에서는 항상 인스턴스 변수명이 @로 시작해야 한다.

Employee; 직원

  • 정규직원과 아르바이트 직원이 공통적으로 가져야 하는 속성과 메서드 시그니처만 정의
class Employee
	attr_reader :name, :basePay
    
    def initialize(name, basePay)
    	@name = name
        @basePay = basePay
    end
    
    def calculatePay(taxRate)
    	raise NotImplementedError
    end
    
    def monthlyBasePay()
    	raise NotImplementedError
    end
end

SalariedEmployee; 정규직원

class SalariedEmployee < Employee
	def initialize(name, basePay)
    	super(name, basePay)
    end
    
    def calculatePay(taxRate)
    	return basePay - (basePay * taxRate)
    end
    
    def monthlyBasePay()
    	return basePay
    end
end

HourlyEmployee; 아르바이트 직원

  • 루비에서의 상속관계; 자식클래스 < 부모클래스
class HourlyEmployee < Employee
	attr_reader :timeCard
    def initialize(name, basePay, timeCard)
    	super(name, basePay)
        @timeCard = timeCard
    end
    
    def calculatePay(taxRate)
    	return (basePay * timeCard) - (basePay * timeCard) * taxRate
    end
    
    def monthlyBasePay()
    	return 0
    end
end

인스턴스 생성

  • 클라이언트가 원하는 직원 타입에 해당하는 클래스의 인스턴스를 명시적으로 지정
  • 객체가 생성된 이후에는, 객체의 클래스가 무엇인지는 중요하지 않다.
    • 클라이언트는 메시지 수신 객체의 구체적인 클래스에 관해 고민할 필요가 없다.
      • 그저 수신자가 이해할 것으로 예상되는 메시지를 전송하기만 하면 된다.
$employees = [
	SalariedEmployee.new("직원A", 400),
    SalariedEmployee.new("직원B", 300),
    SalariedEmployee.new("직원C", 250),
    HourlyEmployee.new("아르바이트D", 1, 120),
    HourlyEmployee.new("아르바이트E", 1, 120),
    HourlyEmployee.new("아르바이트F", 1, 120),
]

sumOfBasePays

  • 구체적인 객체는 고려하지 않고, monthlyBasePay 메시지만 전송하고 있다.
    • 메시지를 수신한 객체가, 서로다른 자신만의 메서드를 이용해, 적절하게 반응한다.
def sumOfBasePay()
	result = 0
    for each in $employees
    	result += each.monthlyBasePay()
    end
    puts(result)
end

변경 기준으로 선택하라

객체지향 vs 추상데이터타입

  • 객체지향 분해
    • 타입을 기준으로 절차를 추상화하는 것.
      • 이를 준수하지 않으면, 클래스를 사용하더라도 객체지향 분해가 아니다.
  • 추상 데이터 타입 분해
    • 클래스 내부에 인스턴스의 타입을 표현하는 변수가 존재한다.
      • 추상 데이터 타입으로 구현된 Employee에는 hourly라는 인스턴스 변수를 통해 직원의 유형을 저장하고 있다.
    • 인스턴스 변수에 저장된 값을 기반으로 메서드 내에서 타입을 명시적으로 구분하는 방식

추상데이터 타입과 비교하여, 객체지향이 가지는 강점

  • 객체지향; 타입변수를 이용한 조건문을 다형성으로 대체한다.
    • 타입을 나타내는 코드를 다형성으로 바꾸는 리팩터링 = Replace Type Code with Class (by.마틴 파울러)
  • 즉, 클라이언트가 객체의 타입을 확인한 후 적절한 메서드를 호출하는 것이 아니라, 객체가 메시지를 처리할 적절한 메서드를 선택한다.
    • 클라이언트가 호출하지 않고, 객체가 선택한다.
    • 이 점이 '객체지향이란 조건문을 제거하는 것' 이라는 다소 편협한 견해가 널리퍼진 이유다.

왜 조건문 사용을 기피할까? 여타 다른 설계 문제처럼, 변경 때문이다.

  • Employee에 새로운 직원 타입을 추가하는 경우
    • 추상 데이터 타입
      • hourl의 값을 체크하는 클라이언트의 조건문을 전부 다 찾아 수정해야 한다.
    • 객체지향
      • 새로운 직원 유형을 구현하는 클래스를 Employee 상속 계층에 추가하고, 필요한 메서드를 오버라이딩 한다.
      • 클라이언트에서, 새로 추가된 클래스의 메서드를 실행하기 위한 어떤 코드도 추가할 필요가 없다.
        • 즉, 시스템에 새로운 로직을 추가하기 위해 클라이언트 코드를 수정할 필요가 없다는 것을 의미한다.

  • 개방-폐쇄 원칙(Open-Closed Principle, OCP)
    • 기존 코드에 아무런 영향도 미치지 않고, 새로운 객체 유형과 행위를 추가할 수 있는 객체지향의 특성
    • 객체지향 설계가 전통적인 방식에 비해 변경하고 확장하기 쉬운 구조를 설계할 수 있는 이유

선택기준; 추상데이터타입 vs 객체지향

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

설계의 이유는 변경이고, 따라서 설계의 유용성은 변경의 방향성과 발생 빈도에 달려있다.
추상 데이터 타입객체지향의 선택 기준은, 현재 요구되는 문제의 변경 방향성과 발생 빈도에 따라 결정된다.

변경의 압력 = 타입추가

  • 추상 데이터 타입
    • 타입을 체크하는 모든 클라이언트 코드를 일일이 찾아 수정한 후 올바른 작동 여부를 테스트해야 한다.
  • 객체지향
    • 클라이언트 코드의 수정이 필요없고, 간단히 새로운 클래스를 상속계층에 추가하기만 하면 된다.

따라서, 변경의 압력이 타입추가인 경우, 객체지향을 선택하면 된다.

변경의 압력 = 오퍼레이션 추가

  • 객체지향
    • 새로운 오퍼레이션 추가를 위해 상속 계층에 속하는 모든 클래스를 한 번에 수정해야 한다.
  • 추상 데이터 타입
    • 전체 타입에 대한 코드가 하나의 구현체에 포함돼있기 때문에, 새로운 오퍼레이션 추가 작업이 상대적으로 간단하다.

따라서, 변경의 압력이 오퍼레이션 추가인 경우, 추상 데이터 타입을 선택하면 된다.

📌 결론

  • 변경의 축 찾기
    • 새로운 타입을 빈번하게 추가; 객체지향의 클래스 구조
    • 새로운 오퍼레이션을 빈번하게 추가; 추상 데이터 타입

데이터-주도 설계

  • 추상 데이터 타입의 접근법을 객체지향 설계에 구현한 것; 데이터 주도 설계 (by. 레베카 워프스브록)
    • 책임 주도 설계 = 데이터 주도 설계 방법을 개선하고자 하는 노력
  • 모듈과 추상 데이터 타입이 데이터 중심적인 관점(data centered view)을 취하는데 비해 객체지향서비스 중심적인 관점(service entered view)을 취한다 (by. 티모시 버드)

⭐️ 협력이 중요하다

클래스 계층에 오퍼레이션의 구현 방법이 분배된다고 해서 객체지향적인 애플리케이션을 설계한 것은 아니다.
객체지향에서 가장 중요한 것은 역할, 책임, 협력이다.
협력이라는 문맥을 고려하지 않고 객체를 고립시킨채 오퍼레이션의 구현 방식을 타입별로 분배하는 것은 올바른 접근법이 아니다.

객체가 참여할 협력을 결정하고, 협력에 필요한 책임을 수행하기 위해 어떤 객체가 필요한지에 관해 고민해라.
그 책임을 다양한 방식으로 수행해야 할 때만 타입 계층 안에 각 절차를 추상화하라.
타입 계층과 다형성은 협력이라는 문맥 안에서 책임을 수행하는 방법에 관해 고민한 결과물이어야 하며 그 자체가 목적이 되어서는 안된다.


Reference

  • 오브젝트 | 조영호
profile
Good Luck!

0개의 댓글