devseop08 님의 블로그

[오브젝트] 객체 분해 본문

Architecture/객체지향설계

[오브젝트] 객체 분해

devseop08 2025. 7. 6. 18:30
  • 실제적 문제 해결을 위해 인간이 사용하는 기억 저장소는 장기 기억이 아닌 단기 기억
  • 문제 해결을 위해 필요한 정보들을 먼저 단기 기억 안으로 불러들여야 하는데 단기 기억은 시간적 공간적 제약을 갖는다
  • 문제 해결에 필요한 요소의 수가 단기 기억의 용량을 초과하는 순간 문제 해결 능력 급감
  • 이런 현상을 인지 과부화라고 한다.
  • 인지 과부화를 방지하기 위해선 단기 기억 안에 보관할 정보의 양을 조절해야 한다.
  • 한 번에 다뤄야 하는 정보의 수를 줄이기 위해 불필요한 정보를 제거하고 현재 문제 해결에 필요한 핵심만 남기는 작업을 추상화라고 한다.
  • 일반적인 추상화 방법은 한 번에 다뤄야 하는 문제의 크기를 줄이는 것
  • 큰 문제를 해결 가능한 작은 문제로 나누는 작업을 분해라고 한다.
  • 분해의 목적은 큰 문제를 인지 과부화의 부담없이 단기 기억 안에서 한 번에 처리할 수 있는 규모의 문제로 나누는 것
  • 한 번에 단기 기억에 담을 수 있는 추상화의 수에는 한계가 있지만 추상화를 더 큰 규모의 추상화로 압축시킴으로써 단기 기억의 한계를 초월 가능
  • 인류 상 가장 복잡한 문제 분야인 소프트웨어 개발 영역에서의 문제 해결을 위해 추상화와 분해가 사용될 수 있다.

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

  • 프로그래밍 패러다임이란 적절한 추상화의 윤곽을 따라 시스템을 어떤 식으로 나눌 것인지를 결정하는 원칙과 방법의 집합
  • 모든 프로그래밍 패러다임은 추상화와 분해의 관점에서 설명할 수 있다.
  • 현대의 프로그래밍 패러다임들은 프로시저 추상화데이터 추상화를 중심으로 시스템 분해 방법을 설명
  • 프로시저 추상화는 소프트웨어가 무엇을 해야하는지를 추상화, 데이터 추상화는 소프트웨어가 무엇을 알아야 하는지를 추상화
  • 프로시저 추상화를 중심으로 시스템을 분해하기로 결정했다면 기능 분해의 길로 들어서는 것
  • 데이터 추상화를 중심으로 시스템을 분해하기로 결정했다면 두 가지 중 선택
      1. 추상 데이터 타입 : 데이터를 중심으로 타입을 추상화
      1. 객체지향 : 데이터를 중심으로 프로시저를 추상화
  • 객체지향 패러다임은 역할과 책임을 수행하는 자율적인 객체들의 협력 공동체를 구축하는 것
  • '역할과 책임을 수행하는 자율적인 객체'가 바로 객체지향 패러다임이 이용하는 추상화
  • '협력 공동체'를 구축하도록 기능을 객체들로 나누는 것이 객체지향 패러다임에서의 분해를 의미
  • 프로그래밍 언어의 관점에서 객체지향이란 데이터를 중심으로 한 데이터 추상화와 프로시저 추상화를 통합한 객체를 이용해 시스템을 분해하는 방법
  • 객체를 구현하기 위해 객체지향 언어는 클래스라는 도구를 제공

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

메인 함수로서의 시스템

  • 기능 분해의 관점에서 추상화의 단위는 프로시저이며 시스템은 프로시저를 단위로 분해된다.
  • 프로시저 중심의 기능 분해 관점에서 시스템은 입력 값을 계산해서 출력 값을 반환하는 수학의 함수와 동일
  • 시스템은 필요한 더 작은 작업으로 분해될 수 있는 하나의 커다란 메인 함수
  • 전통적인 기능 분해 방법은 하향식 접근법을 따른다.
  • 하향식 접근법이란 시스템을 구성하는 최상위 기능을 정의하고 최상위 기능을 좀 더 작은 단계의 하위 기능으로 분해해 나가는 방법
  • 상위 기능은 하나 이상의 더 간단하고 더 구체적이며 덜 추상적인 하위 기능의 집합으로 분해\

급여 관리 시스템

  • 급여 관리 시스템을 구현하기 위해 기능 분해 방법을 이용해보겠다.
급여 = 기본급 - (기본급 * 소득세율)
  • 최상위의 추상적인 함수 정의는 시스템의 기능을 표현하는 하나의 문장으로 나타내고, 이 문자을 구성하는 좀 더 세부적인 단계의 문장으로 분해해 나가는 방식
  • 급여 관리 시스템에 대한 최상위 문장
직원의 급여를 계산한다.
  • 직원 정보는 프로시저의 인자로 전달받고 소득세율은 사용자로부터 직접 입력받기로 결정

직원의 급여를 계산한다
    사용자로부터 소득세율을 입력받는다
    직원의 급여를 계산한다
    양식에 맞게 결과를 출력한다
  • 각 정제 단계는 이전 문장의 추상화 수준을 감소시켜야 한다.
  • 개발자는 각 단계에서 불완전하고 좀 더 구체화될 수 있는 문장들이 남아있는지 검토
직원의 급여를 계산한다
    사용자로부터 소득세율을 입력받는다
        "세율을 입력하세요: "라는 문장을 화면에 출력한다
        키보드를 통해 세율을 입력받는다
    직원의 급여를 계산한다
        전역 변수에 저장된 직원의 기본급 정보를 얻는다
        급여를 계산한다
    양식에 맞게 결과를 출력한다
        "이름: {직원명}, 급여: {계산된 금액}" 형식에 따라 출력 문자열을 생성한다.
  • 기능 분해를 위한 하향식 접근법은 먼저 필요한 기능을 생각하고 이 기능을 분해하고 정제하는 과정에서 필요한 데이터의 종류와 저장 방식을 식별
  • 이런 방식은 유지 보수에 다양한 문제를 야기
  • 실제 애플리케이션을 코드로 구현하면서 문제점을 이해해보도록 한다.

급여 관리 시스템 구현

직원의 급여를 계산한다
/* Ruby */

def main(name)
end
직원의 급여를 계산한다
    사용자로부터 소득세율을 입력받는다
    직원의 급여를 계산한다
    양식에 맞게 결과를 출력한다
def main(name)
    taxRate = getTaxRate()
    pay = calculatePayFor(name, taxRate)
    puts(decribeResult(name, pay))
end
직원의 급여를 계산한다
    사용자로부터 소득세율을 입력받는다
        "세율을 입력하세요: "라는 문장을 화면에 출력한다
        키보드를 통해 세율을 입력받는다
    직원의 급여를 계산한다
    양식에 맞게 결과를 출력한다
def getTaxRate()
    print("세율을 입력하세요: ")
    return gets().chomp().to_f()
end
직원의 급여를 계산한다
    사용자로부터 소득세율을 입력받는다
    직원의 급여를 계산한다
        전역 변수에 저장된 직원의 기본급 정보를 얻는다
        급여를 계산한다
    양식에 맞게 결과를 출력한다
  • 급여를 계산하기 위해서는 애플리케이션 내부에 직원 목록과 기본급에 대한 정보를 유지
$employees = ["직원A", "직원B", "직원C"]
$basePays = [400, 300, 250]
def calculatePayFor(name, taxRate)
    index = $employees.index(name)
    basePay = $basePays[index]
    return basePay - (basePay * taxRate)
end
직원의 급여를 계산한다
    사용자로부터 소득세율을 입력받는다
    직원의 급여를 계산한다
    양식에 맞게 결과를 출력한다
        "이름: {직원명}, 급여: {계산된 금액}" 형식에 따라 출력 문자열을 생성한다.
def decribeResult(name, pay)
    return "이름: #{name}, 급여: #{pay}"
end
main("직원C") 

하향식 기능 분해의 문제점

하나의 메인 함수라는 비현실적인 아이디어
  • 대부분의 시스템에서 하나의 메인 기능이란 개념은 존재하지 않는다
  • 모든 기능들은 규모라는 측면에서 차이가 있을 수는 있겠지만
  • 기능성의 측면에서는 동등하게 독립적이고 완결된 하나의 기능을 표현한다.
메인 함수의 빈번한 재설계
  • 시스템 안에는 여러 개의 정상이 존재하기 때문에 결과적으로 하나의 메인 함수를 유일한 정상으로 간주하는 하향식 기능 분해의 경우에는 새로운 기능을 추가할 때마다 매번 메인 함수를 수정해야 한다.
  • 급여 관리 시스템에 회사에 속한 모든 직원들의 기본급의 총합을 구하는 기능 추가
def sumOfBasePays()
    result = 0 
    for basePay in $basePays
        result += basePay
    end
    puts(result)
end
  • 기존의 메인 함수에 sumOfBasePays가 들어설 자리가 마땅치 않다.
  • sumOfBasePays 함수와 개별 직원의 급여를 계산하는 main 함수는 개념적으로 동등한 수준의 작업을 수행 => 현재의 main 함수 안에서 sumOfBasePays 함수를 호출 불가
  • main 함수 안의 로직을 새로운 calculatePay 함수로 옮겨서 두 함수를 main 함수에서 호출
def calculatePay(name)
    taxRate = getTaxRate()
    pay = calculatePayFor(name, taxRate)
    puts(decribeResult(name, pay))
end
def main(operation, args={})
    case(operation)
    when :pay then calculatePay(args[:name])
    when :basePays then sumOfBasePays()
    end
end
main(:basePays)
main(:pay, name:"직원A")
비즈니스 로직과 사용자 인터페이스의 결합
  • 하향식 접근법은 비즈니스 로직을 설계하는 초기 단계부터 입력 방법과 출력 양식을 함께 고민하도록 강요한다.
  • 코드 안에서 비즈니스 로직과 사용자 인터페이스 로직이 밀접하게 결합된다.
  • 비즈니스 로직과 사용자 인터페이스가 변경되는 빈도가 다르다는 것이 문제다
  • 하향식 접근법은 사용자 인터페이스 로직과 비즈니스 로직을 한데 섞기 때문에 사용자 인터페이스를 변경하는 경우 비즈니스 로직까지 변경에 영향을 받는다.
  • 하향식 접근법은 기능을 분해하는 과정에서 사용자 인터페이스의 관심사와 비즈니스 로직의 관심사를 동시에 고려하도록 강요하기 때문에 관심사의 분리라는 아키텍처 설계의 목적을 달성하기 어렵다.
성급하게 결정된 실행 순서
  • 하향식으로 기능을 분해하는 과정은 분해된 함수들의 실행 순서를 결정
  • 이는 설계를 시작하는 시점부터 시스템이 무엇을 해야 하는지가 아니라 어떻게 동작해야하는지 집중하도록 만든다.
  • 하향식 접근법의 설계는 처음부터 구현을 염두에 두기 때문에 자연스럽게 함수들의 실행 순서를 정의하는 시간 제약을 강조
  • 모든 중요한 제어 흐름의 결정이 상위 함수에서 이뤄지고 하위 함수는 상위 함수의 흐름에 따라 적절한 시점에 호출
  • 기능을 추가하거나 변경하는 작업은 매번 기존에 결정된 함수의 제어 구조를 변경하도록 만든다.
  • 햐향식 접근법을 통해 분해한 함수들은 재사용하기도 어렵다
  • 하향식 설계와 관련된 모든 문제의 원인은 결합도 때문이다.
  • 함수는 상위 함수가 강요하는 문맥에 강하게 결합, 전체 시스템의 핵심적인 구조를 결정하는 함수들이 데이터와 결합
데이터 변경으로 인한 파급효과
  • 하향식 기능 분해의 가장 큰 문제점은 어떤 데이터를 어떤 함수가 사용하고 있는지를 추적하기 어렵다는 것이다.
  • 데이터 변경으로 인해 어떤 함수가 영향을 받을지 예상하기 어렵다.
  • 데이터 변경으로 인한 영향을 최소화하려면 데이터와 함께 변경되는 부분과 그렇지 않은 부분을 명확하게 분리해야 한다.
  • 잘 정의된 퍼블릭 인터페이스를 통해 데이터에 대한 접근을 통제해야 하는 것이다.
  • 정보 은닉과 모듈

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

  • 하향식 분해는 작은 프로그램과 개별 알고리즘을 위해서는 유요한 패러다임으로 남아 있다.
  • 이미 해결된 알고리즘을 문서화하고 서술하는 데는 훌륭한 기법이나 실제로 동작하는 커다란 소프트웨어를 설계하는 데 적합한 방법은 아니다.

3. 모듈

정보 은닉과 모듈

  • 정보 은닉은 시스템을 모듈 단위로 분해하기 위한 기본 원리
  • 시스템에서 자주 변경되는 부분을 상대적으로 덜 변경되는 안정적인 인터페이스 뒤로 감춰야 한다.
  • 모듈과 기능 분해는 상호 배타적인 관계가 아니다.
  • 시스템을 모듈로 분해한 후에는 각 모듈 내부를 구현하기 위해 기능 분해 적용 가능
  • 시스템을 모듈 단위로 분해하기 위해선 시스템이 감춰야 하는 비밀을 찾아야 한다.
  • 외부에서 내부의 비밀에 접근하지 못하도록 하는 방어막이 퍼블릭 인터페이스가 된다.
  • 모듈은 다음과 같은 두 가지 비밀을 감춰야 한다.
      1. 복잡성 : 외부에 모듈을 추상화할 수 있는 간단한 인터페이스를 제공해서 모듈의 복잡도 를 낮추도록 한다.
      1. 변경 가능성: 변경 발생 시 하나의 모듈만 수정하면 되도록 변경 가능한 설계 결정을 모 듈 내부로 감추도록 한다.
  • 감춰야 하는 비밀이 반드시 데이터일 필요는 없다. 복잡한 로직이나 변경 가능성이 큰 자료 구조일 수도 있다.
  • 전체 직원에 관한 처리를 Employees 모듈로 캡슐화
module Employees
    $employees = ["직원A", "직원B", "직원C", "직원D", "직원E", "직원F"]
    $basePays = [400, 300, 250, 1, 1, 1.5]
    $hourlys = [false, false, false, true, ture, ture]
    $timeCards = [0, 0, 0, 120, 120, 120]

    def Employees.calculatePay(name, taxRate)
        if (Employees.hourly?(name)) then
            pay = Employees.calculateHourlyPayFor(name, taxRate)
        else 
            pay = Employees.calculatePayFor(name, taxRete)
        end
    end

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

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

    def Employees.calculatePayFor(name, taxRate)
        return basePay - (basePay * taxRate)
    end

    def Employees.sumOfBasePay(){
        result = 0
        for name in $employees
            if(not Employees.hourly?(name)) then
                result += $basePays[$employees.index(name)]
            end
        end
        return result
    end
end
    }
  • 이제 모듈 외부에서는 직원 정보를 관리하는 데이터에 직접 접근할 수 없다.
  • 외부에서는 Employees 모듈이 제공하는 퍼블릭 인터페이스에 포함된 calculatePay, hourly?, calculateHourlyPayFor, calculatePayFor, sumOfBasePay 함수를 통해서만 내부 변수를 조작할 수 있다.
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.sumOfBasePays())
end

모듈의 장점과 한계

  • 모듈의 장점
      1. 모듈 내부의 변수가 변경되더라도 모듈 내부에만 영향을 미친다.
      1. 비즈니스 로직과 사용자 인터페이스에 대한 관심사를 분리
      1. 전역 변수와 전역 함수를 제거함으로써 네임스페이스 오염을 방지
  • 모듈은 기능이 아니라 변경의 정도에 따라 시스템을 분해하게 한다.
  • 모듈 내부는 높은 응집도를 유지하고
  • 모듈과 모듈 사이에는 낮은 결합도를 유지한다.
  • 모듈의 가장 큰 단점은 인스턴스의 개념을 제공하지 않는다는 점이다.
  • 이 점을 개선하기 위해 등장한 개념이 바로 추상 데이터 타입이다.

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

추상 데이터 타입

  • 프로그래밍 언어에서 타입이란 변수에 저장할 수 있는 내용물의 종류와 변수에 적용될 수 있는 연산의 가짓수를 의미
  • 타입은 저장된 값에 대해 수행될 수 있는 연산의 집합을 결정, 변수의 값이 어떻게 행동할 것이라는 것을 예측할 수 있게 한다.
  • 기능 분해 시대의 절차형 언어들은 적은 수의 내장 타입만을 제공했으며 새로운 타입을 추가하는 것이 불가능하거나 제한적
  • 이렇게 부족한 내장 타입을 가지고는 프로시저 추상화로써 시스템을 추상화하고 분해하는 것에 있어서 표현적인 측면에서 명확한 한계를 갖게 될 수 밖에 없다.
  • 이런 프로시저 추상화의 한계를 극복하고 보완하기 위해 데이터를 추상화하자는 아이디어가 등장, 데이터 추상화의 개념이 제안되었다.
  • 추상 데이터 타입은 추상 객체의 클래스를 정의한 것으로 추상 객체에 사용할 수 있는 오퍼레이션을 이용해 규정된다.
  • 이는 오퍼레이션을 이용해 추상 데이터 타입을 정의할 수 있음을 의미
  • 추상 데이터 객체를 사용할 때 프로그래머는 오직 객체가 외부에 제공하는 행위에만 관심을 두고 행위가 구현되는 세부적인 사항에 대해서는 무시
  • 이러한 추상 데이터 타입은 프로시저 추상화 대신 데이터 추상화를 기반으로 소프트웨어를 개발하게 한 최초의 발걸음이다.
  • 추상 데이터 타입을 구현하려면 프로그래밍 언어가 다음과 같은 특성들을 위한 지원을 제공해야 한다.
      1. 타입 정의를 선언할 수 있어야 한다.
      1. 타입의 인스턴스를 다루기 위해 사용할 수 있는 오퍼레이션의 집합을 정의할 수 있어야 한다.
      1. 제공된 오퍼레이션을 통해서만 조작할 수 있도록 데이터를 외부로부터 보호할 수 있어야 한다.
      1. 타입에 대해 여러 개의 인스턴스를 생성할 수 있어야 한다.
  • 추상 데이터 타입을 구현할 수 있는 언어적인 장치를 제공하지 않는 프로그래밍 언어에서도 추상 데이터 타입을 구현하는 것이 가능하다.
  • 루비 언어는 추상 데이터 타입을 흉내낼 수 있는 Struct라는 구성 요소를 제공한다.

추상 데이터 타입을 이용해 급여 관리 시스템 개선하기

  • 직원 추상화
Employee = Struct.new(:name, :basePay, :hourly, :timeCard) do
End
  • 내부에 캡슐화할 데이터를 결정했다면 추상 데이터 타입에 적용할 수 있는 오퍼레이션을 결정해야 한다.
  • 모듈 방식에서는 외부에서 전달받던 인자가 추상 데이터 타입에서는 추상 데이터 타입 내부에 포함되기 때문에 모듈 방식의 오퍼레이션보다 추상 데이터 타입에서 정의된 오퍼레이션의 시그니처가 더 간단하다.
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
  • 특정 직원의 급여를 계산하는 것은 Employee 인스턴스를 찾은 후 calculatePay 오퍼레이션을 호출하는 것
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
  • 추상 데이터 타입 정의를 기반으로 객체를 생성하는 것 가능하지만 여전히 데이터와 기능을 분리해서 바라본다.
  • 추상 데이터 타입으로 표현된 데이터를 이용해서 기능을 구현하는 핵심 로직은 추상 데이터 타입 외부에 존재한다. 여전히 데이터와 기능을 분리하는 절차적인 설계의 틀에 갇혀 있는 것
  • 추상 데이터 타입의 본래 기본 의도는 프로그래밍 언어가 제공하는 타입처럼 사용자 정의 타입을 추가할 수 있게 하는 것
  • 프로그래밍 언어의 관점에서 추상 데이터 타입은 해당 프로그래밍 언어의 내장 타입과 동일클래스

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

  • 추상 데이터 타입과 클래스 모두 데이터 추상화를 기반으로 시스템을 분해하기 때문에 이 둘이 동일하다고 할 수 있을 것 같지만 명확한 의미에서 추상 데이터 타입과 클래스는 동일하지 않다.
  • 클래스는 상속과 다형성을 지원하는 데 비해 추상 데이터 타입은 지원하지 못한다
  • 추상 데이터 타입은 타입을 추상화한 것이고 클래스는 절차를 추상화한 것
  • 하나의 대표적인 타입이 다수의 세부적인 타입을 감추는 것이 타입 추상화
  • 타입 추상화는 개별 오퍼레이션이 모든 개념적인 타입에 대한 구현을 포괄하도록 함으로써 하나의 물리적인 타입 안에 전체 타입을 감춘다
  • 타입 추상화오퍼레이션을 기준으로 타입을 통합하는 데이터 추상화 기법이다.
  • 타입 추상화를 기반으로 하는 대표적인 기법이 바로 추상 데이터 타입이다.
  • 추상 데이터 타입이 오퍼레이션을 기준으로 타입을 묶는 방법이라면 객체지향은 타입을 기준으로 오퍼레이션을 묶는다.
  • 객체지향은 절차 추상화다.
  • 추상 데이터 타입은 오퍼레이션을 기준으로 타입들을 추상화한다. 객체지향은 타입을 기준으로 절차들을 추상화한다.
  • 객체지향에서는 추상 데이터 타입과 달리 타입들이 통합되지 않고 분리되는데 이 분리된 클래스들의 공통 로직(공통 절차)을 제공하는 가장 간단한 방법은 이 공통 로직을 포함할 부모 클래스를 선언하고 분리된 클래스들이 부모 클래스를 상속받게 하는 것이다.
  • 클라이언트는 분리된 클래스들의 부모 클래스의 참조자에 대해 메시지를 전송하고, 이 때 실제 클래스가 무엇인가에 따라 적절한 절차가 실행되는데 이것이 바로 객체지향의 다형성이다.

추상 데이터 타입에서 클래스로 변경하기

  • 클래스를 이용해 급여 관리 시스템 구현
  • 클래스를 이용한 구현에서는 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
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
class HourlyEmployee < Employee
    attr_reader :timeCard
    def initialize(name, basePay, timeCard)
        super(name, basePay)
        @timeCard = timeCard
    end

    def calculatePay(taxRate)
        (basePay * timeCard) - (basePay * timeCard) * taxRate
    end

    def monthlyBasePay()
        return 0
    end
end 
  • 모든 직원 타입에 대해 Employee의 인스턴스를 생성해야 했던 추상 데이터 타입의 경우와 달리 클래스를 이용해서 구현한 코드의 경우엔 클라이언트가 원하는 직원의 타입에 해당하는 클래스의 인스턴스를 명시적으로 지정할 수 있다.
$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)
]
  • 클라이언트는 메시지를 수신할 객체의 구체적인 클래스에 관해 고민할 필요가 없다.
def sumOfBasePays()
    result = 0
    for each in $employees
        result += each.monthlyBasePay()
    end
    puts(result)
end

변경을 기준으로 선택하라

  • 단순히 클래스룰 구현 단위로 사용한다는 것이 객체지향 프로그래밍을 한다는 것을 의미하지는 않는다.
  • 비록 클래스를 사용하고 있더라도 타입을 기준으로 절차를 추상화하지 않았다면 그것은 객체지향 분해가 아니다.
  • 클래스가 추상 데이터 타입의 개념을 따르는지를 확인할 수 있는 가장 간단한 방법은 클래스 내부에 인스턴스의 타입을 표현하는 변수가 있는지를 살펴보는 것이다.
  • 인스턴스 변수에 저장된 값을 기반으로 메서드 내에서 타입을 명시적으로 구분하는 방식은 객체지향을 위반하는 것으로 간주한다.
  • 객체지향에서는 타입 변수를 이용한 조건문을 다형성으로 대체한다.
  • 클라이언트가 객체의 타입을 확인한 후 적절한 메서드를 호출하는 것이 아니라 객체가 메시지를 처리할 적절한 메서드를 선택해야 한다.
  • 추상 데이터 타입 기반의 Employee에 새로운 직원 타입을 추가하기 위해서는 hourly의 값을 체크하는 클라이언트의 조건문을 하나씩 다 찾아 수정해야 한다.
  • 이에 비해 객체지향은 새로운 직원 유형을 구현하는 클래스 Employee 상속 계층에 추가하고 필요한 메서드를 오버라이딩하면 된다.
  • 시스템에 새로운 로직을 추가하기 위해 클라이언트의 코드를 수정할 필요가 없다는 것
  • 이처럼 기존 코드에 아무런 영향도 미치지 않고 새로운 객체 유형과 행위를 추가할 수 있는 객체지향의 특성을 개방-폐쇄 원칙(Open-Closed Principle, OCP)이라고 부른다.
  • 설계는 변경과 관련된 것인데 추상 데이터 타입과 객체지향 설계의 유용성은 설계에 요구되는 변경의 압력이 '타입 추가'에 관한 것인지, 아니면 '오퍼레이션 추가'에 관한 것인지에 따라 달라진다.
  • 타입 추가라는 변경의 압력이 더 강한 경우에는 객체지향 설계가 더 유용하다. 이런 경우 객체지향의 상속과 다형성을 이용하면 된다.
  • 오퍼레이션 추가라는 변경의 압력이 더 강한 경우에는 추상 데이터 타입이 더 유용하다.
  • 추상 데이터 타입은 전체 타입에 대한 구현 코드가 하나의 구현체 내에 포함돼 있기 때문에 새로운 오퍼레이션을 추가하는 작업이 상대적으로 간단하다.