문제 해결을 위해 필요한 정보들을 먼저 단기 기억 안으로 불러들여야 하는데 단기 기억은 시간적 공간적 제약을 갖는다
문제 해결에 필요한 요소의 수가 단기 기억의 용량을 초과하는 순간 문제 해결 능력 급감
이런 현상을 인지 과부화라고 한다.
인지 과부화를 방지하기 위해선 단기 기억 안에 보관할 정보의 양을 조절해야 한다.
한 번에 다뤄야 하는 정보의 수를 줄이기 위해 불필요한 정보를 제거하고 현재 문제 해결에 필요한 핵심만 남기는 작업을 추상화라고 한다.
일반적인 추상화 방법은 한 번에 다뤄야 하는 문제의 크기를 줄이는 것
큰 문제를 해결 가능한 작은 문제로 나누는 작업을 분해라고 한다.
분해의 목적은 큰 문제를 인지 과부화의 부담없이 단기 기억 안에서 한 번에 처리할 수 있는 규모의 문제로 나누는 것
한 번에 단기 기억에 담을 수 있는 추상화의 수에는 한계가 있지만 추상화를 더 큰 규모의 추상화로 압축시킴으로써 단기 기억의 한계를 초월 가능
인류 상 가장 복잡한 문제 분야인 소프트웨어 개발 영역에서의 문제 해결을 위해 추상화와 분해가 사용될 수 있다.
1. 프로시저 추상화와 데이터 추상화
프로그래밍 패러다임이란 적절한 추상화의 윤곽을 따라 시스템을 어떤 식으로 나눌 것인지를 결정하는 원칙과 방법의 집합
모든 프로그래밍 패러다임은 추상화와 분해의 관점에서 설명할 수 있다.
현대의 프로그래밍 패러다임들은 프로시저 추상화나 데이터 추상화를 중심으로 시스템 분해 방법을 설명
프로시저 추상화는 소프트웨어가 무엇을 해야하는지를 추상화, 데이터 추상화는 소프트웨어가 무엇을 알아야 하는지를 추상화
프로시저 추상화를 중심으로 시스템을 분해하기로 결정했다면 기능 분해의 길로 들어서는 것
데이터 추상화를 중심으로 시스템을 분해하기로 결정했다면 두 가지 중 선택
추상 데이터 타입 : 데이터를 중심으로 타입을 추상화
객체지향 : 데이터를 중심으로 프로시저를 추상화
객체지향 패러다임은 역할과 책임을 수행하는 자율적인 객체들의 협력 공동체를 구축하는 것
'역할과 책임을 수행하는 자율적인 객체'가 바로 객체지향 패러다임이 이용하는 추상화
'협력 공동체'를 구축하도록 기능을 객체들로 나누는 것이 객체지향 패러다임에서의 분해를 의미
프로그래밍 언어의 관점에서 객체지향이란 데이터를 중심으로 한 데이터 추상화와 프로시저 추상화를 통합한 객체를 이용해 시스템을 분해하는 방법
객체를 구현하기 위해 객체지향 언어는 클래스라는 도구를 제공
2. 프로시저 추상화와 기능 분해
메인 함수로서의 시스템
기능 분해의 관점에서 추상화의 단위는 프로시저이며 시스템은 프로시저를 단위로 분해된다.
프로시저 중심의 기능 분해 관점에서 시스템은 입력 값을 계산해서 출력 값을 반환하는 수학의 함수와 동일
시스템은 필요한 더 작은 작업으로 분해될 수 있는 하나의 커다란 메인 함수
전통적인 기능 분해 방법은 하향식 접근법을 따른다.
하향식 접근법이란 시스템을 구성하는 최상위 기능을 정의하고 최상위 기능을 좀 더 작은 단계의 하위 기능으로 분해해 나가는 방법
상위 기능은 하나 이상의 더 간단하고 더 구체적이며 덜 추상적인 하위 기능의 집합으로 분해\
급여 관리 시스템
급여 관리 시스템을 구현하기 위해 기능 분해 방법을 이용해보겠다.
급여 = 기본급 - (기본급 * 소득세율)
최상위의 추상적인 함수 정의는 시스템의 기능을 표현하는 하나의 문장으로 나타내고, 이 문자을 구성하는 좀 더 세부적인 단계의 문장으로 분해해 나가는 방식
급여 관리 시스템에 대한 최상위 문장
직원의 급여를 계산한다.
직원 정보는 프로시저의 인자로 전달받고 소득세율은 사용자로부터 직접 입력받기로 결정
직원의 급여를 계산한다
사용자로부터 소득세율을 입력받는다
직원의 급여를 계산한다
양식에 맞게 결과를 출력한다
각 정제 단계는 이전 문장의 추상화 수준을 감소시켜야 한다.
개발자는 각 단계에서 불완전하고 좀 더 구체화될 수 있는 문장들이 남아있는지 검토
직원의 급여를 계산한다
사용자로부터 소득세율을 입력받는다
"세율을 입력하세요: "라는 문장을 화면에 출력한다
키보드를 통해 세율을 입력받는다
직원의 급여를 계산한다
전역 변수에 저장된 직원의 기본급 정보를 얻는다
급여를 계산한다
양식에 맞게 결과를 출력한다
"이름: {직원명}, 급여: {계산된 금액}" 형식에 따라 출력 문자열을 생성한다.
기능 분해를 위한 하향식 접근법은 먼저 필요한 기능을 생각하고 이 기능을 분해하고 정제하는 과정에서 필요한 데이터의 종류와 저장 방식을 식별
이런 방식은 유지 보수에 다양한 문제를 야기
실제 애플리케이션을 코드로 구현하면서 문제점을 이해해보도록 한다.
급여 관리 시스템 구현
직원의 급여를 계산한다
/* Ruby */
def main(name)
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. 모듈
정보 은닉과 모듈
정보 은닉은 시스템을 모듈 단위로 분해하기 위한 기본 원리
시스템에서 자주 변경되는 부분을 상대적으로 덜 변경되는 안정적인 인터페이스 뒤로 감춰야 한다.
모듈과 기능 분해는 상호 배타적인 관계가 아니다.
시스템을 모듈로 분해한 후에는 각 모듈 내부를 구현하기 위해 기능 분해 적용 가능
시스템을 모듈 단위로 분해하기 위해선 시스템이 감춰야 하는 비밀을 찾아야 한다.
외부에서 내부의 비밀에 접근하지 못하도록 하는 방어막이 퍼블릭 인터페이스가 된다.
모듈은 다음과 같은 두 가지 비밀을 감춰야 한다.
복잡성 : 외부에 모듈을 추상화할 수 있는 간단한 인터페이스를 제공해서 모듈의 복잡도 를 낮추도록 한다.
변경 가능성: 변경 발생 시 하나의 모듈만 수정하면 되도록 변경 가능한 설계 결정을 모 듈 내부로 감추도록 한다.
감춰야 하는 비밀이 반드시 데이터일 필요는 없다. 복잡한 로직이나 변경 가능성이 큰 자료 구조일 수도 있다.
전체 직원에 관한 처리를 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
모듈의 장점과 한계
모듈의 장점
모듈 내부의 변수가 변경되더라도 모듈 내부에만 영향을 미친다.
비즈니스 로직과 사용자 인터페이스에 대한 관심사를 분리
전역 변수와 전역 함수를 제거함으로써 네임스페이스 오염을 방지
모듈은 기능이 아니라 변경의 정도에 따라 시스템을 분해하게 한다.
모듈 내부는 높은 응집도를 유지하고
모듈과 모듈 사이에는 낮은 결합도를 유지한다.
모듈의 가장 큰 단점은 인스턴스의 개념을 제공하지 않는다는 점이다.
이 점을 개선하기 위해 등장한 개념이 바로 추상 데이터 타입이다.
데이터 추상화와 추상 데이터 타입
추상 데이터 타입
프로그래밍 언어에서 타입이란 변수에 저장할 수 있는 내용물의 종류와 변수에 적용될 수 있는 연산의 가짓수를 의미
타입은 저장된 값에 대해 수행될 수 있는 연산의 집합을 결정, 변수의 값이 어떻게 행동할 것이라는 것을 예측할 수 있게 한다.
기능 분해 시대의 절차형 언어들은 적은 수의 내장 타입만을 제공했으며 새로운 타입을 추가하는 것이 불가능하거나 제한적
이렇게 부족한 내장 타입을 가지고는 프로시저 추상화로써 시스템을 추상화하고 분해하는 것에 있어서 표현적인 측면에서 명확한 한계를 갖게 될 수 밖에 없다.
이런 프로시저 추상화의 한계를 극복하고 보완하기 위해 데이터를 추상화하자는 아이디어가 등장, 데이터 추상화의 개념이 제안되었다.
추상 데이터 타입은 추상 객체의 클래스를 정의한 것으로 추상 객체에 사용할 수 있는 오퍼레이션을 이용해 규정된다.
이는 오퍼레이션을 이용해 추상 데이터 타입을 정의할 수 있음을 의미
추상 데이터 객체를 사용할 때 프로그래머는 오직 객체가 외부에 제공하는 행위에만 관심을 두고 행위가 구현되는 세부적인 사항에 대해서는 무시
이러한 추상 데이터 타입은 프로시저 추상화 대신 데이터 추상화를 기반으로 소프트웨어를 개발하게 한 최초의 발걸음이다.
추상 데이터 타입을 구현하려면 프로그래밍 언어가 다음과 같은 특성들을 위한 지원을 제공해야 한다.
타입 정의를 선언할 수 있어야 한다.
타입의 인스턴스를 다루기 위해 사용할 수 있는 오퍼레이션의 집합을 정의할 수 있어야 한다.
제공된 오퍼레이션을 통해서만 조작할 수 있도록 데이터를 외부로부터 보호할 수 있어야 한다.
타입에 대해 여러 개의 인스턴스를 생성할 수 있어야 한다.
추상 데이터 타입을 구현할 수 있는 언어적인 장치를 제공하지 않는 프로그래밍 언어에서도 추상 데이터 타입을 구현하는 것이 가능하다.
루비 언어는 추상 데이터 타입을 흉내낼 수 있는 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의 인스턴스를 생성해야 했던 추상 데이터 타입의 경우와 달리 클래스를 이용해서 구현한 코드의 경우엔 클라이언트가 원하는 직원의 타입에 해당하는 클래스의 인스턴스를 명시적으로 지정할 수 있다.