단기 기억과 장기기억
문제해결에 필요한 요소의 수가 단기 기억의 용량을 초과하는 순간 문제 해결 능력이 급격히 떨어지는것
인지 과부하(cognitive overload) 발생
-> 단기기억안에 보관할 정보의 양을 조절하기위해
불필요한 정보를 제거, 현재 문제 해결에 필요한 핵심 정보만 남기는 작업(추상화)
큰 문제를 한번에 해결 가능한 작은 단위로 나누는 작업 분해(decomposition)
추상화를 더 큰 규모의 추상화로 압축시킴으로써 단기 기억의 한계를 초월할 수 있음.
복잡성의 문제를 추상화와 분해로 해결
01 프로시저 추상화와 데이터 추상화
어셈블리어 - 기계어에 인간이 이해할 수 있는 상징을 부여하려는 노력의 결과
고수준 언어 - 인간의 눈높이에 맞게 의미 있는 추상화를 시도한 결과
프로그래밍을 구성하기 위해 사용하는 추상화의 종류
이 추상화를 이용해 소프트웨어를 분해하는 방법의 두가지 요소로 결정.
프로시저 추상화 | 데이터 추상화 |
기능 분해/ 알고리즘 분해 |
데이터 중심으로 타입을 추상화 (추상 데이터 타입 type abstraction) , 데이터 중심으로 프로시저를 추상화 (객체지향 object-oriented) |
무엇을 해야하는지를 추상화 | 무엇을 알아야 하는지 추상화 |
데이터 추상화와 프로시저 추상화를 함께 포함한 클래스를 이용해 시스템을 분해하는 것
객체지향이 전통적 기능 분해 방법에 비해 효과적 이라고 말하는 이유가 무엇일까?
효과적 이라는 말의 진정한 의미는?
02 프로시저 추상화와 기능 분해
메인 함수로서의 시스템
기능은 오랜시간동안 시스템을 분해하기 위한 기준으로 사용됨. 추상화의 단위. 프로시저.
유사하게 사용되는 작업들은 모아놓아 로직을 재사용 중복방지하는 추상화 방법.
전통적인 기능 분해 방법. 하향식 접근법 (Top-Down Approach)
- 가장 최상위 기능을 정의하고 좀 더 작은 단계의 하위기능으로 분해해 나가는 방법
- 각 세분화 단계는 윗단계보다 더 구체적이어야 함
- 분해는 세분화된 마지막 하위기능이 프로그래밍 언어로 구현 가능한 수준이 될때까지 계속.
급여 관리 시스템
직원들에게 실제로 지급되는 급여를 계산하는 식
급여 = 기본급 - (기본급 * 소득세율)
하나의 문장으로 표현된 기능을 여러개의 더 작은 기능으로 분해하자
먼저 추상적인 최상위 문장으로 시작하자 - 메인 프로시저로 구현
직원의 급여를 계산한다
필요한 정보 - 직원의 이름과 소득세율
이름은 프로시저의 인자로 전달 받고 소득세율을 직업 입력받기로 결정
세부적인 절차로 구체화 하자
직원의 급여를 계산한다
사용자로부터 소득세율을 입력받는다
직원의 급여를 계산한다.
양식에 맞게 결과를 출력한다.
구현이 가능 할 정도로 저수준의 문장이 될때 까지 기능을 분해
직원의 급여를 계산한다
사용자로부터 소득세율을 입력받는다
"세율을 입력하세요: " 라는 문장을 화면에 출력한다.
직원의 급여를 계산한다.
전역변수에 저장된 직원의 기본급 정보를 얻는다
급여를 계산한다
양식에 맞게 결과를 출력한다.
"이름: {직원명}, 급여: {계산된 금액}" 형식에 따라 출력 문자열을 생성한다.
직원정보
> 급여 관리 시스템 -> 급여
소득세율
기능분해 방법에는 기능을 중심으로 데이터를 결정
하향식 기능 분해 관점은 유지보수에서 다양한 문제를 야기
급여 관리 시스템 구현
직원의 급여를 계산한다 사용자로부터 소득세율을 입력받는다 "세율을 입력하세요: " 라는 문장을 화면에 출력한다. 직원의 급여를 계산한다. 전역변수에 저장된 직원의 기본급 정보를 얻는다 급여를 계산한다 양식에 맞게 결과를 출력한다. "이름: {직원명}, 급여: {계산된 금액}" 형식에 따라 출력 문자열을 생성한다. |
def sumOfBasePays()
result = 0
for basePay in $basePays
result += basePay
end
puts(result)
end
def main(main)
taxRate = getTaxRate()
pay = calculatePayFor(name, taxRate)
puts(describeResult(name, pay))
end
def getTaxRate()
print("세율을 입력하세요: ")
return gets().chomp().to_f()
end
$empoyees = ["직원A", "직원B", "직원C"]
$basePays = [400,300,250]
def calculatePayFor(name, taxRate)
index = $employees.index(name)
basePay = $basePays[index]
return basePay - (basePay*taxRate)
end
def describeResult(name, pay)
return "이름: #{name}, 급여: {pay}"
end
메인함수를 루트로 하는 '트리(tree)'
논리적이고 체계적인 시스템 개발 절차를 제시
하향식 기능 분해의 문제점
- 시스템은 하나의 메인함수로 구성되지 않는다.
- 기능 추가나 요구사항 변경으로 인해 메인 함수를 빈번하게 수정해야 한다.
- 비지니스 로직이 사용자 인터페이스와 강하게 결합된다.
- 하향식 분해는 너무 이른시기에 함수들의 실행 순서를 고정시키기 때문에 유연성과 재사용성이 저하됨
- 데이터 형식이 변경될 경우 파급효과를 예측할 수 없다.
셜계가 필요한 이유는 변경에 대비하기 위함 - 소프트웨어는 항상 변경되기 때문에
하향식 접근법의 문제점에서 시작해 기능분해가 가지는 문제점
- 하나의 메인함수라는 비현실적인 아이디어
하향식 접근법은 하나의 알고리즘이나 배치처리를 구현하기에 적합하나, 현대적인 상호작용 시스템을 개발하는 데는 부적합
현대적인 시스템은 동등한 수준의 다양한 기능으로 구성
- 메인함수의 빈번한 재설계
새로운 기능을 추가할 때마다 메인함수를 수정해야하고, 구조를 급격하게 변경해야함
ex) 직원들의 기본급 총합을 계산하는 기능을 추가한다고 할때.
def calculatePay(name)
taxRate = getTaxRate()
pay = calculatePayFor(name, taxRate)
puts(describeResult(name, pay))
end
def main(operation, args={})
case(operation)
when :pay then calculate(args[:name])
when :basePays then sumOfBasePays()
end
end
기본급의 총합을 구할때 main(:basePays)
이름이 "직원A"인 직원의 급여를 계산할 떄 main(:pay, name:"직원A")
- 비지니스 로직과 사용자 인터페이스의 결합
사용자 인터페이스는 시스템 내에서 가장 자주 변경되는 부분
비지니스 로직과 사용자 인터페이스 로직이 섞이면 변경에 불안정한 설계가 됨.
- 성급하게 결정된 실행 순서
하향식 기능 분해 과정은 무엇을 해야하는지가 아닌 어떻게 동작하는지 집중하게 만듬.
실행 순서나 제어구조를 미리 결정하지 않고는 분해를 진행할 수 없음. 중앙집중 제어 스타일(centralized control)
중요한 제어흐름 결정이 상위 함수에서 이루어지고 하위함수는 적절한 시점에 호출됨. 모든 문제의 원인 의존성과 결합도.
->논리적 관계를 중심으로 설계를 이끌어 나가야함.
- 데이터 변경으로 인한 파급효과
데이터 변경으로 어떤 함수가 영향을 받을지 어려움.
ex) 정규직원과 아르바이트 직원 급여계산 기능 추가
$employees = ["직원A", "직원B", "직원C", "아르바이트D", "아르바이트E", "아르바이트F"]
$basePays = [400, 300, 250, 1, 1, 1.5]
$hourlys = [false, false, false, true, true, true]
$timeCard = [0,0,0,120,120,120]
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 calculate(name)
taxRate = getTaxRate()
if(hourly?(name)) then
pay = calculateHourlyPayFor(name, taxRate)
else
pay = calculatePayFor(name, taxRate)
end
puts(describeResult(name, pay))
end
def sumOfBasePays()
result = 0
for name in $employees
if (not hourly?(name)) then
result += $basePays[$employees.index(name)]
end
end
puts(result)
end
언제 하향식 분해가 유용한가?
하향식 분해는 작은 프로그램과 개별 알고리즘을 위해서는 유용한 패러다임.
이미 해결된 알고리즘을 문서화하고 서술하는데 훌륭한 기법 but 커다란 소프트웨어 설계에는 비적합.
03 모듈
정보은닉과 모듈
함께 변경되는 부분을 하나의 구현단위로 묶고 퍼블릭 인터페이스를 통해서만 접근하도록 만드는 것.
기능을 기반으로 분해하는것이 아니라 변경의 방향에 맞춰 시스템을 분해하는 것.
정보은닉(information hiding) - 시스템을 모듈 단위로 분해하기 위한 기본원리
자주 변경되는 부분을 상대적으로 덜 변경되는 안정적인 인터페이스 뒤로 감춰야 한다는 것.
모듈이 감춰야할 두가지
- 복잡성 - 모듈이 너무 복잡하고 이해하기 어려운경우
- 변경가능성 - 변경 가능한 설계 결정이 외부에 노출되 실제 변경이 발생 했을때 파급효과가 커짐.
ex) 급여관리 시스템에서 감춰야하는 비밀 - 직원정보
module Employees
$employees = ["직원A", "직원B", "직원C", "아르바이트D", "아르바이트E", "아르바이트F"]
$basePays = [400, 300, 250, 1, 1, 1.5]
$hourlys = [false, false, false, true, true, true]
$timeCard = [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, taxRate)
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.sumOfBasePays()
result = 0
for name in $employees
if (not Employees.hourly?(name)) then
result += $basePays[$employees.index(name)]
end
end
return result
end
main함수가 Employees 모듈의 기능을 사용하도록 수정
def main(operation, args={})
case(operation)
when :pay then calculatePay(args[:name])
when :basePays then sumOfBasePays()
end
end
def calaulatePay(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
모듈 장점과 한계
장점
- 모듈 내부의 변수가 변경되더라도 모듈 내부에만 영향을 미친다.
- 모듈 내부에 정의된 변수를 직접 참조하는 코드의 위치를 내부로 제한할 수 있음.
- 파급효과 제어
- 비지니스 로직과 사용자 인터페이스에 대한 관심사를 분리한다.
- 비지니스 로직에 영향 최소화
- 전역 변수와 전역 함수를 제거, 네임스페이스 오염을 방지한다.
- 네임스페이스를 제공함으로써 이름 충돌의 위험을 줄임
모듈의 핵심은 데이터. 기능이 아니라 데이터를 중심으로 시스템을 분해
감춰야할 데이터를 결정하고 -> 이 데이터를 조작하는데 필요한 함수를 결정
단점
- 인스턴스의 개념을 제공하지 않음.
-높은 수준의 추상화를 위해서는 개별직원은 독립적으로 다룰수 있어야함.
04 데이터 추상화와 추상 데이터 타입
추상 데이터 타입
타입(type) 이란?
변수에 저장할 수 있는 내용물의 종류와 변수에 적용될 수 있는 연산의 가짓수를 의미
변수값의 행동 양식을 예측할 수 있음.
추상데이터 타입을 구현하는데 필요한 프로그래밍 언어의 특성
- 타입 정의를 선언할 수 있다
- 타입의 인스턴스를 다루기 위해 사용할 수 있는 오퍼레이션 집향을 정의할 수 있다.
- 제공된 오퍼레이션을 통해서만 조작할 수 있도록 데이터를 외부에서 보호할 수 있다.
- 타입 여러개의 인스턴스를 생성할 수 있다.
언어 차원에서 추상 데이터 타입을 지원하는것
그것를 위한 메커니즘 -> 오퍼레이션 클러스터(operation cluster)
루비 Struct 구성을 이용해 추상데이터 타입을 구현
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 = Struct.new(:name, :basePay, :hourly, :timeCard) do
def monthlyBasePay()
if(hourly) then return 0 end
return basePay
end
end
클라이언트 코드 작성
직원들의 인스턴스 준비
$employees = [
Employees.new("직원A", 400, false, 0),
Employees.new("직원B", 300, false, 0),
Employees.new("직원C", 200, false, 0),
Employees.new("아르바이트D", 1, true, 120),
Employees.new("아르바이트E", 1, true, 120),
Employees.new("아르바이트F", 1, true, 120),
]
def calcuatePay(name)
taxRate = getTaxRate()
for each in $employees
if(each.name == name) then employees = each; brake end
end
pay = employees.calculatePay(taxRate)
puts(describeResult(name, pay))
end
def sumOfBasePays()
result = 0
for each in $employees
result += each.monthlyBasePay()
end
puts(result)
end
개별 직원의 인스텀스를 생성할 수 있는 Employee 추상 데이터 타입
상태와 행위를 가지는 독립적인 객체에 더 가까움.
사용자 정의타입을 추가할 수 있게 하는것.
05 클래스
클래스는 추상 데이터 타입인가?
클래스와 추상데이터 타입
공통점
- 두가지 모두 데이터 추상화를 기반으로 시스템을 분해
- 외부에서는 객체의 내부속성에 접근할 수 없으며 오직 퍼블릭 인터페이스를 통해서만 의사소통 할 수 있음.
차이점
- 클래스는 상속과 다형성을 지원 / 추상데이터 타입은 지원하지 못함.
- 객체지향 프로그래밍 / 객체기반 프로그래밍
- 타입을 기준으로 절차를 추상화한 것 / 오퍼레이션을 기준으로 타입을 추상화한 것
Employee Type | ||
오퍼레이션 | 정규직원 | 아르바이트 직원 |
calculatePay() | basePay - (basePay*taxRate) | (basePay*timeCard) - (basePay*timeCard)*taxRate |
monthlyBasePay() | basePay | 0 |
하나의 타입처럼 보이는 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)
@time Card = 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),
]
def sumOfBasePays()
result = 0
for each in $employees
result += each.monthlyBasePay()
end
puts(result)
end
$employees 에 포함된 객체가 어떤 타입인지를 고민하지 않고 메시지 전송
변경을 기준으로 선택하라
인스턴스 변수에 저장된 값을 기반으로 메서드 내에서 타입을 명시적으로 구분하는 방식은 객체지향을 위반하는 것으로 간주함.
Replace Type Code with Class
- 타입변수를 이용한 조건문을 다형성으로 대체
- 변경이 일어날시 타입 값을 체크하는 조건문을 수정하는 대신
타입 유형을 구현하는 클래스를 상속계층에 추가하고 필요한 메서드 오버라이딩
설계에 요구되는 압력이 '타입추가' 에 관한 것인지 '오퍼레이션 추가'에 관련한 것인지에 따라 설계 방향이 달라진다
- 새로운 타입을 빈번하게 추가한다면 객체지향 클래스 구조
- 새로운 오퍼레이션을 추가해야한다면 추상데이터 타입
모듈과 추상데이터 타입이 데이터 중심적인 관점을 취한다면 객체지행은 서비스 중심적인 관점을 취함.
협력이 중요하다
협력을 고려하지 않고 객체를 고립시킨채 오퍼레이션 구현방식을 타입별로 분배하는 것은 올바른 접근법이 아니다.
객체를 설계하는 방법에서 중요한 것은 책임주도 설계의 흐름을 따르는 것 (chapter03 참고)
타입 계층과 다형성은 협력을 위한 책임을 수행하기 위한 수단으로서의 결과물인 것이지 목적일 수는 없다.
'IT Book Summary > Object: 객체지향설계' 카테고리의 다른 글
Chapter 09 유연한 설계 (0) | 2019.12.18 |
---|---|
Chapter 08 의존성 관리하기 (0) | 2019.12.11 |
Chapter 06 메시지와 인터페이스 (0) | 2019.11.25 |
Chapter 05 책임 할당하기 (0) | 2019.11.18 |
Chapter 04 설계 품질과 트레이드 오프 (0) | 2019.11.18 |