이 추상화와 분해라는 기능을 통해 복잡한 기능을 청크 단위로 이해할 수 있다.
소프트웨어 개발 영역에서 어떻게 추상화와 분해를 이용해 복잡한 현실세계의 문제들을 해결해왔는지 알아보자.
거대하고 복잡한 문제를 해결하기 위해 추상화 기법이 이용된다.
추상화란; 거대하고 식별하기 어려운 문제를, 응집성있는 모듈화를 통한 분해로, 인식하기 쉽게 하려는 노력이다.
따라서 추상화를 위해서는어떻게 분해할 것인지
,어떻게 응집할 것인지
를 고려해야 한다.
어떻게 응집할 것인지
어떻게 분해할 것인지
은행계좌관리()
├── 계좌개설()
├── 입금처리()
├── 출금처리()
├── 잔액조회()
└── 계좌해지()
데이터 추상화 기반 분해
추상 데이터 타입(Abstraction Data Type)
Stack ADT
├── 데이터: 요소들의 집합
└── 연산:
├── push(element)
├── pop()
├── peek()
└── isEmpty()
객체지향(Object-Oriented)
데이터를 중심으로 프로시저를 추상화(procedure abstraction)
실세계 객체를 모델링하여 시스템을 구성
예시: 은행 계좌 시스템의 객체지향 설계
class 계좌 {
속성:
- 계좌번호
- 소유자
- 잔액
메서드:
- 입금(금액)
- 출금(금액)
- 잔액조회()
}
협력하는 공동체
를 구성하도록 객체들로 나누는 과정시스템 분해 중 더 이전부터 오랜시간 사용되던 기준은
기능 분해
였다.
프로시저 추상화
; 메인 함수로서의 시스템기능
을 기준으로 분해하는 방식.급여 = 기본급 - (기본급 * 소득세율)
직원의 급여를 계산한다.
직원의 급여를 계산한다.
사용자로부터 소득세율을 입력받는다.
직원의 급여를 계산한다.
양식에 맞게 결과를 출력한다.
직원의 급여를 계산한다.
사용자로부터 소득세율을 입력받는다.
"세율을 입력하세요:"라는 문장을 화면에 출력한다.
키보드를 통해 세율을 입력받는다.
직원의 급여를 계산한다.
전역 변수에 저장된 직원의 기본급 정보를 얻는다.
급여를 계산한다.
양식에 맞게 결과를 출력한다.
"이름: {직원명}, 급여: {계산된 금액}" 형식에 따라 출력 문자열을 생성한다.
루비 언어를 통해 구현을 해보자!
직원의 급여를 계산한다.
def main(name)
end
직원의 급여를 계산한다.
사용자로부터 소득세율을 입력받는다.
직원의 급여를 계산한다.
양식에 맞게 결과를 출력한다.
def main(name)
taxRate = getTaxRate()
pay = calculatePayFor(name, taxRate)
puts(describeResult(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 = $emplyees.index(name)
basePay = $basePays[index]
return basePay - (basePay * taxRate)
end
def describeResult(name, pay)
return "이름: #{name}, 급여: #{pay}"
end
main("직원C")
- 실제 시스템에 정상(top)이란 존재하지 않는다. -버트란드 마이어
- 정상(top); 분해가 시작되는 메인함수
- 훌륭한 프로그램은 많은 정상(top)을 명확하게 제시하고, 훌륭한 아키텍처는 정상들을 우아하게 표현한다. -제임스 코플리엔
$basePays
에 저장돼 있는 직원들의 모든 급여를 더하기def sumOfBasePayse()
result = 0
for basePay in $basePays
result += basePay
end
puts(result)
end
main
함수 안의 로직을 새로운 함수 calculatePay
함수로 옮긴다.calculatePay
와 sumOfBasePays
를 선택적으로 호출하도록 수정한다.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
사용자로부터 소득세율을 입력받아 급여를 계산한 후 계산된 결과를 화면에 출력한다.
main
함수 안에 뒤섞여 있다.일한 시간 * 시급
의 금액을 지급받는다.$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 effectclaculatePay
함수를 변경했으나, 예상치 못하게 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].
모듈은 서브 프로그램이라기 보다는 책임의 할당이다. 모듈화는 개별적인 모듈에 대한 작업이 시작되기 전에 정해져야 하는 설계 결정들을 포함한다. (...) 분할된 모듈은 다른 모듈에 대해 감춰야 하는 설계 결정에 따라 특징지어진다. 해당 모듈 내부의 작업을 가능한 한 적게 노출하는 인터페이스 또는 정의를 선택한다. (...) 어려운 설계 결,ㅡ정이나 변화할 것 같은 설계 결정들의 목록을 사용해 설계를 시작할 것을 권장한다. 이러한 결정이 외부 몯류에 대해 숨겨지도록 각 모듈을 설계해야 한다.[Parnas72].
시스템의 가장 일반적인 비밀은 데이터다. 이 관점에서 데이터 캡슐화와 정보 은닉이 혼동스럽게 느껴질 수 있다. 비밀이 반드시 데이터일 필요는 없으며 복잡한 로직이나 변경 가능성이 큰 자료 구조일수도 있다. 그럼에도 변경 시 가장큰 위험요소는 대부분 데이터가 변경되는 경우다.
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
함수 쪽에 위치한다.Employees
모듈에 포함된 비즈니스 로직은 변경되지 않는다.Employees
모듈은 단지 회사에 속한 모든 직원 정보를 가지고 있는 모듈일 뿐이다.이 시대의 프로그램에서 사용하는 주된 추상화는 프로시저 추상화였다.
시간이 흐르면서 사람들은 프로시저 추상화로는 프로그램의 표현력을 향상시키는 데 한계가 있다는 사실을 발견했다.
직원의 급여를 계산한다
라는 하나의 커다란 절차를 이용한 사고보다(프로시저 추상화) 직원
,과 급여
라는 추상적인 개념들을 머릿속에 떠올린 후 이들을 이용해 계산
에 필요한 절차를 생각하는 데(데이터 추상화) 익숙하다.안타깝게도 프로시저만으로는 충분히 풍부한 추상화의 어휘집을 제공할 수 없다. (...) 이것은 언어 설계에서 가장 중요한 추상 데이터 타입(Abstract Data Type) 개념으로 우리를 인도했다. 추상 데이터 타입은 추상 객체의 클래스를 정의한 것으로 추상 객체에 사용할 수 있는 오퍼레이션을 이용해 규정된다.(데이터 추상화, 데이터 캡슐화) 이것은 오퍼레이션을 이용해 추상 데이터 타입을 정의할 수 있음을 의히만다. (...) 추상 데이터 객체를 사용할 때 프로그래머는 오직 객체가 외부에 제공하는 행위에만 관심을 가지며 행위가 구현되는 세부적인 사항에 대해서는 무시한다.(인터페이스-구현 분리) 객체가 저장소 내에서 어떻게 표현되는지와 같은 구현 정보는 오직 오퍼레이션을 어떻게 구현할 것인지에 집중할 때만 필요하다. 객체의 사용자는 이 정보를 알거나 제공받을 필요가 없다.(정보은닉)[Liskov74].
위 인용문은 데이터 추상화, 정보 은닉, 데이터 캡슐화, 인터페이스-구현 분리의 개념들이 모두 다 녹아들어있다.
$employees
; 직원의 이름$basePays
; 기본급$hourlys
; 아르바이트 직원 여부$timeCards
; 아르바이트 직원일 경우 한달 간 작업시간Employee = Struct.new(:name, :basePay, :houlry, :timeCard) do
End
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
monthlyBasePay
; 개별직원의 기본급 계산Employee = Struct.new(:name, :basePay, :hourly, :timeCard) do
def monthlyBasePay()
if (hourly) then return 0 end
return basePay
end
end
$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),
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
def sumOfBasePays()
result = 0
for each in $employees
result += each.mounthlyBasePay()
end
puts(result)
end
Employee
는 상태와 행위를 가지는 독립적인 객체의 의미를 가진다.그렇다면 클래스는 추상 데이터 타입인가?
Employee
타입Employee
가 제공하는 2개의 퍼블릭 인터페이스; calcualtePay
, monthlyBasePay
Employee
내부에 두개의 타입이 공존한다.Employee
타입은 구체적인 직원 타입을 외부에 캡슐화하고 있다.Employee
를 사용하는 클라이언트는 calcualtePay
와 monthlyBasePay
를 호출할 수 있으나, 정규 직원이나 아르바이트 직원이 있다는 사실은 알 수 없다. (오퍼레이션은 알 수 있으나 타입은 알 수 없다)Employee
내부에 감춰져 있으며 암묵적이다.calculatePay
와 monthlyBasePay
오퍼레이션을 적절하게 구현한다.Employee
추상 데이터 타입에 구현되어 있던 타입별 코드가 두 개의 클래스로 분배ruby
에서는 항상 인스턴스 변수명이 @
로 시작해야 한다.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)
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),
]
monthlyBasePay
메시지만 전송하고 있다.def sumOfBasePay()
result = 0
for each in $employees
result += each.monthlyBasePay()
end
puts(result)
end
Employee
에는 hourly
라는 인스턴스 변수를 통해 직원의 유형을 저장하고 있다.왜 조건문 사용을 기피할까? 여타 다른 설계 문제처럼, 변경 때문이다.
Employee
에 새로운 직원 타입을 추가하는 경우hourl
의 값을 체크하는 클라이언트의 조건문을 전부 다 찾아 수정해야 한다.Employee
상속 계층에 추가하고, 필요한 메서드를 오버라이딩 한다.항상 절차를 추상화하는 객체지향 설계방식을 따라야 하는가? 추상 데이터 타입은 모든 경우에 최악의 선택인가?
설계의 이유는 변경이고, 따라서 설계의 유용성은 변경의 방향성과 발생 빈도에 달려있다.
추상 데이터 타입
과 객체지향
의 선택 기준은, 현재 요구되는 문제의 변경 방향성과 발생 빈도에 따라 결정된다.
따라서, 변경의 압력이 타입추가인 경우, 객체지향을 선택하면 된다.
따라서, 변경의 압력이 오퍼레이션 추가인 경우, 추상 데이터 타입을 선택하면 된다.
데이터-주도 설계
- 추상 데이터 타입의 접근법을 객체지향 설계에 구현한 것; 데이터 주도 설계 (by. 레베카 워프스브록)
- 책임 주도 설계 = 데이터 주도 설계 방법을 개선하고자 하는 노력
- 모듈과 추상 데이터 타입이 데이터 중심적인 관점(data centered view)을 취하는데 비해 객체지향은 서비스 중심적인 관점(service entered view)을 취한다 (by. 티모시 버드)
클래스 계층에 오퍼레이션의 구현 방법이 분배된다고 해서 객체지향적인 애플리케이션을 설계한 것은 아니다.
객체지향에서 가장 중요한 것은 역할, 책임, 협력이다.
협력이라는 문맥을 고려하지 않고 객체를 고립시킨채 오퍼레이션의 구현 방식을 타입별로 분배하는 것은 올바른 접근법이 아니다.
객체가 참여할 협력을 결정하고, 협력에 필요한 책임을 수행하기 위해 어떤 객체가 필요한지에 관해 고민해라.
그 책임을 다양한 방식으로 수행해야 할 때만 타입 계층 안에 각 절차를 추상화하라.
타입 계층과 다형성은 협력이라는 문맥 안에서 책임을 수행하는 방법에 관해 고민한 결과물이어야 하며 그 자체가 목적이 되어서는 안된다.