"추상화를 통해 복잡성을 극복하고, 분해를 통해 큰 문제를 작은 문제로 나누자"
- 추상화(Abstraction): 불필요한 정보를 제거하고 문제 해결에 필요한 핵심만 남기는 작업
- 분해(Decomposition): 큰 문제를 해결 가능한 작은 문제로 나누는 작업
- 프로시저 추상화: 소프트웨어가 무엇을 해야 하는지 추상화 → 기능 분해
- 데이터 추상화: 소프트웨어가 무엇을 알아야 하는지 추상화 → 추상 데이터 타입 / 객체지향
- 정보 은닉(Information Hiding): 변경되는 부분을 안정적인 인터페이스 뒤로 감추기
- 모듈(Module): 변경을 관리하기 위한 기본 구현 단위
- 인지 과부하를 극복하는 추상화와 분해의 필요성 이해하기
- 하향식 기능 분해의 문제점을 명확히 파악하기
- 정보 은닉과 모듈을 통한 변경 관리 방법 학습하기
- 추상 데이터 타입과 클래스의 차이점 이해하기
- 급여 시스템의 진화를 통해 객체지향의 장점 체득하기
인간의 단기 기억 한계: 7±2개 항목
→ 복잡한 문제를 한 번에 이해할 수 없음
해결책 1: 추상화 (Abstraction)
- 불필요한 세부사항 제거
- 핵심만 남기기
- 이해 가능한 수준으로 단순화
해결책 2: 분해 (Decomposition)
- 큰 문제를 작은 문제로
- 각각을 독립적으로 해결
- 다시 조합하여 전체 해결
이번 챕터의 여정:
절차적 프로그래밍 (기능 분해)
↓ 문제점 발견
모듈 (정보 은닉)
↓ 인스턴스 개념 부족
추상 데이터 타입
↓ 다형성 부족
객체지향 (클래스)
George Miller의 연구: 인간은 한 번에 7±2개 항목만 기억 가능
문제 해결 과정:
1. 장기 기억에서 필요한 정보 인출
2. 단기 기억으로 이동
3. 단기 기억에서 문제 해결
4. 결과를 다시 장기 기억으로 저장
병목: 단기 기억의 용량!
→ 7개를 초과하는 순간 인지 과부하 발생
→ 문제 해결 능력 급격히 저하
┌─────────────────────────────────────┐
│ 복잡한 시스템 (100개 요소) │
│ │
│ ❌ 한 번에 이해 불가능 │
└─────────────────────────────────────┘
추상화 & 분해 ↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 모듈 A │ │ 모듈 B │ │ 모듈 C │
│ (5개) │ │ (5개) │ │ (5개) │
└──────────┘ └──────────┘ └──────────┘
✅ 각 모듈은 이해 가능한 크기
✅ 모듈 간 인터페이스로 연결
✅ 한 번에 하나의 모듈에 집중
핵심:
추상화 = 복잡도 감소
분해 = 관리 가능한 크기로 나누기
추상화 + 분해 = 복잡한 시스템 구축 가능
현대 프로그래밍 언어의 핵심:
| 추상화 종류 | 질문 | 중심 개념 | 분해 방법 |
|---|---|---|---|
| 프로시저 추상화 | "무엇을 해야 하는가?" | 기능, 알고리즘 | 기능 분해 |
| 데이터 추상화 | "무엇을 알아야 하는가?" | 데이터, 타입 | 타입/객체 분해 |
초점: 소프트웨어가 수행해야 할 기능
예시: "급여를 계산한다"
→ 어떤 데이터를 사용할지는 나중에 결정
→ 먼저 기능을 분해
결과: 기능 중심 시스템
- 함수들의 계층 구조
- 데이터는 전역 변수로 공유
- 하향식 접근법
초점: 소프트웨어가 알아야 할 정보
예시: "직원 정보"
→ 어떤 기능이 필요한지는 나중에 결정
→ 먼저 데이터를 정의
두 가지 방향:
1. 타입 추상화 → 추상 데이터 타입 (ADT)
2. 프로시저 추상화 → 객체지향 (OOP)
프로그래밍 패러다임의 스펙트럼:
[기능 분해]────[모듈]────[추상 데이터 타입]────[객체지향]
↑ ↑ ↑ ↑
함수 중심 정보 은닉 타입 추상화 다형성 추가
객체지향 = 데이터 추상화 + 프로시저 추상화
= 상속 + 다형성
전통적 관점: 시스템 = 하나의 큰 함수
시스템의 개념적 모델:
┌────────────────────────────────────┐
│ main(input) │
│ │
│ 1. 입력을 받는다 │
│ 2. 입력을 처리한다 │
│ 3. 결과를 출력한다 │
│ │
│ return output │
└────────────────────────────────────┘
특징:
- 시스템 = 거대한 수학 함수
- 입력 → 처리 → 출력의 단순 흐름
- 모든 것이 메인 함수 아래 배치
1단계: 최상위 기능 정의
"직원의 급여를 계산한다"
2단계: 하위 기능으로 분해
"직원의 급여를 계산한다"
├─ "사용자로부터 소득세율을 입력받는다"
├─ "직원의 급여를 계산한다"
└─ "양식에 맞게 결과를 출력한다"
3단계: 더 세부적으로 분해
"사용자로부터 소득세율을 입력받는다"
├─ "세율을 입력하세요"를 출력한다
└─ 키보드로 세율을 입력받는다
4단계: 구현 가능할 때까지 반복
결과물: 함수들의 계층적 트리 구조
main()
│
┌───────────┼───────────┐
│ │ │
getTaxRate() calculatePay() describeResult()
│
┌───────────┴───────────┐
getBasePay() calculateFee()
기본 규칙:
1. 회사는 정규 직원과 아르바이트 직원을 고용
2. 정규 직원: 기본급 - (기본급 × 소득세율)
3. 아르바이트: 시급 × 근무시간 - (시급 × 근무시간 × 소득세율)
4. 매월 모든 직원의 급여 계산 및 지급
이제 6단계에 걸쳐 시스템이 어떻게 진화하는지 추적해봅시다!
📂 코드:
employees_step01.rb
#encoding: UTF-8
$employees = ["직원A", "직원B", "직원C"]
$basePays = [400, 300, 250]
def main(name)
taxRate = getTaxRate()
pay = calculatePayFor(name, taxRate)
puts(describeResult(name, pay))
end
def getTaxRate()
print("세율을 입력하세요: ")
return gets().chomp().to_f()
end
def calculatePayFor(name, taxRate)
index = $employees.index(name)
basePay = $basePays[index]
return basePay - (basePay * taxRate)
end
def describeResult(name, pay)
return "이름 : #{name}, 급여 : #{pay}"
end
main("직원A")함수 계층:
main(name)
├─ getTaxRate()
├─ calculatePayFor(name, taxRate)
└─ describeResult(name, pay)
데이터:
$employees = 전역 배열
$basePays = 전역 배열
의존성:
모든 함수가 전역 변수에 의존
main("직원A") 호출
↓
┌─────────────────────────────────────┐
│ 1. getTaxRate() 호출 │
│ - "세율을 입력하세요: " 출력 │
│ - 사용자 입력: 0.1 │
│ - return 0.1 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 2. calculatePayFor("직원A", 0.1) │
│ - $employees에서 "직원A" 찾기 │
│ - index = 0 │
│ - basePay = $basePays[0] = 400 │
│ - 400 - (400 * 0.1) = 360 │
│ - return 360 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 3. describeResult("직원A", 360) │
│ - "이름 : 직원A, 급여 : 360" │
│ - return 문자열 │
└─────────────────────────────────────┘
↓
puts로 출력: "이름 : 직원A, 급여 : 360"
❌ 새로운 기능 추가 시나리오
요구사항: "전체 직원 기본급 합계 계산"
문제:
- main() 함수는 한 명의 급여만 계산
- 합계 계산은 완전히 다른 기능
- main()을 어떻게 수정해야 할까?
→ 시스템 = 하나의 메인 함수라는 가정 붕괴!
📂 코드:
employees_step02.rb
def main(operation, args={})
case(operation)
when :pay then calculatePay(args[:name])
when :basePays then sumOfBasePays()
end
end
def calculatePay(name)
taxRate = getTaxRate()
pay = calculatePayFor(name, taxRate)
puts(describeResult(name, pay))
end
def sumOfBasePays()
result = 0
for basePay in $basePays
result += basePay
end
puts(result)
end
# 사용
main(:basePays) # 기본급 합계
main(:pay, name:"직원A") # 개별 급여Before (Step 01):
main(name) - 단일 기능만 수행
After (Step 02):
main(operation, args)
├─ :pay → calculatePay(name)
└─ :basePays → sumOfBasePays()
특징:
✅ 여러 기능 지원 가능
❌ 새 기능 추가 시 main() 수정 필요
시나리오: 새 기능 추가
1. 직원 추가 기능
2. 직원 삭제 기능
3. 급여 인상 기능
...
매번 main() 함수에 case문 추가:
def main(operation, args={})
case(operation)
when :pay then calculatePay(args[:name])
when :basePays then sumOfBasePays()
when :addEmployee then addEmployee(args[:name]) # 추가
when :removeEmployee then removeEmployee(args[:name]) # 추가
when :raisePayment then raisePayment(args[:rate]) # 추가
end
end
❌ 개방-폐쇄 원칙 위반!
❌ 기존 코드 수정 → 버그 위험
📂 코드:
employees_step03.rb
새로운 요구사항:
"아르바이트 직원도 관리해주세요"
아르바이트 급여 계산:
시급 × 근무시간 - (시급 × 근무시간 × 소득세율)
# 데이터 구조 복잡화
$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] # 근무시간
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 hourly?(name)
return $hourlys[$employees.index(name)]
end
def calculateHourlyPayFor(name, taxRate)
index = $employees.index(name)
basePay = $basePays[index] * $timeCards[index]
return basePay - (basePay * taxRate)
end
def sumOfBasePays()
result = 0
for name in $employees
# ❌ 여기서도 타입 확인 필요!
if (not hourly?(name)) then
result += $basePays[$employees.index(name)]
end
end
puts(result)
end병렬 배열 (Parallel Arrays) 패턴:
Index: 0 1 2 3 4 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 │
└────────┴────────┴────────┴────────────┴────────────┴────────────┘
문제점:
❌ 4개 배열의 동기화 필요
❌ 인덱스 불일치 시 버그
❌ 새 속성 추가 시 배열 하나 더 추가
❌ 타입 정보($hourlys)가 명시적으로 필요
def calculatePay(name)
# UI: 사용자 입력
taxRate = getTaxRate() # ← print, gets 사용
# 비즈니스 로직
if (hourly?(name)) then
pay = calculateHourlyPayFor(name, taxRate)
else
pay = calculatePayFor(name, taxRate)
end
# UI: 결과 출력
puts(describeResult(name, pay)) # ← puts 사용
end문제:
시나리오 1: 웹 인터페이스로 변경
→ getTaxRate(), puts() 모두 수정 필요
시나리오 2: 모바일 앱 개발
→ 전체 함수 재작성 필요
시나리오 3: 테스트 코드 작성
→ 사용자 입력을 mock하기 어려움
원인: 비즈니스 로직과 UI가 섞여있음
def calculatePay(name)
# 1. 입력 (순서 고정)
taxRate = getTaxRate()
# 2. 계산 (순서 고정)
if (hourly?(name)) then
pay = calculateHourlyPayFor(name, taxRate)
else
pay = calculatePayFor(name, taxRate)
end
# 3. 출력 (순서 고정)
puts(describeResult(name, pay))
end문제:
만약 세율을 미리 알고 있다면?
→ 함수 수정 필요
만약 결과를 출력하지 않고 반환만 하려면?
→ 함수 수정 필요
만약 계산 후 다른 처리를 하려면?
→ 함수 수정 필요
실행 순서가 너무 일찍, 너무 강하게 결정됨
→ 재사용성 ↓
시나리오: timeCards 배열을 Dictionary로 변경
$timeCards = [120, 120, 120] # Before
↓
$timeCards = {"아르바이트D" => 120, ...} # After
영향받는 함수들:
❌ calculateHourlyPayFor() - 접근 방식 변경
❌ 모든 timeCards를 사용하는 함수들
문제: 어떤 함수들이 영향받는지 추적 어려움
근본 원인:
데이터 = 전역 변수
함수 = 데이터에 자유롭게 접근
→ 데이터와 함수 사이의 의존성이 명시적이지 않음
→ 변경 영향 범위 파악 불가능
→ 유지보수 악몽
📂 코드:
employees_step04.rb
문제: 전역 데이터 + 자유로운 접근 = 파급효과 예측 불가
해결책: 정보 은닉 (Information Hiding)
1. 자주 변경되는 데이터를 식별
2. 안정적인 인터페이스 뒤로 숨기기
3. 인터페이스를 통해서만 접근 허용
module Employees
# ✅ Private: 외부에서 접근 불가
$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]
# ✅ Public Interface: 외부에 제공하는 인터페이스
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.sumOfBasePays()
result = 0
for name in $employees
if (not Employees.hourly?(name)) then
result += $basePays[$employees.index(name)]
end
end
return result
end
# ✅ Private: 모듈 내부에서만 사용
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)
index = $employees.index(name)
basePay = $basePays[index]
return basePay - (basePay * taxRate)
end
end
# 외부 코드 (UI 레이어)
def calculatePay(name)
taxRate = getTaxRate()
pay = Employees.calculatePay(name, taxRate) # ✅ 모듈 통해서만 접근
puts(describeResult(name, pay))
end
def sumOfBasePays()
puts(Employees.sumOfBasePays()) # ✅ 모듈 통해서만 접근
end1. 변경 영향 최소화
Before (Step 03):
$basePays 변경 → 모든 함수 확인 필요
After (Step 04):
$basePays 변경 → Employees 모듈 내부만 확인
변경의 파급효과가 모듈 경계에서 멈춤!
2. 비즈니스 로직과 UI 분리
Before:
calculatePay() 안에 getTaxRate(), puts() 섞여있음
After:
┌─────────────────────┐
│ UI Layer │
│ - getTaxRate() │
│ - puts() │
│ - describeResult() │
└─────────────────────┘
↓ uses
┌─────────────────────┐
│ Business Layer │
│ Employees 모듈 │
│ - calculatePay() │
│ - sumOfBasePays() │
└─────────────────────┘
관심사의 분리 (Separation of Concerns)
3. 네임스페이스 오염 방지
Before:
calculatePayFor() - 전역 함수
hourly?() - 전역 함수
→ 이름 충돌 가능성
After:
Employees.calculatePayFor() - 모듈 내부
Employees.hourly?() - 모듈 내부
→ 명확한 소속, 충돌 방지
문제: 직원 개개인을 다루기 어려움
현재:
Employees.calculatePay("직원A", 0.1)
Employees.calculatePay("직원B", 0.1)
Employees.calculatePay("직원C", 0.1)
→ 매번 이름으로 검색
→ 직원 객체를 직접 다룰 수 없음
필요한 것:
employee1.calculatePay(0.1)
employee2.calculatePay(0.1)
employee3.calculatePay(0.1)
→ 인스턴스 개념 필요!
📂 코드:
employees_step05.rb
정의: 데이터와 그 데이터를 조작하는 오퍼레이션을 하나로 묶은 것
타입 (Type) = 데이터 + 연산
예시:
Integer 타입 = {정수값} + {+, -, *, /, ...}
String 타입 = {문자열} + {concat, substring, ...}
추상 데이터 타입:
사용자 정의 타입 = {내부 데이터} + {오퍼레이션}
Employee = Struct.new(:name, :basePay, :hourly, :timeCard) do
# ✅ 오퍼레이션 1: 급여 계산
def calculatePay(taxRate)
if (hourly) then
return calculateHourlyPay(taxRate)
end
return calculateSalariedPay(taxRate)
end
# ✅ 오퍼레이션 2: 월 기본급 조회
def monthlyBasePay()
if (hourly) then return 0 end
return basePay
end
private
def calculateHourlyPay(taxRate)
return (basePay * timeCard) - (basePay * timeCard) * taxRate
end
def calculateSalariedPay(taxRate)
return basePay - (basePay * taxRate)
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.monthlyBasePay()
end
puts(result)
endBefore (Step 04) - 병렬 배열:
$employees = ["직원A", "직원B", "직원C", ...]
$basePays = [400, 300, 250, ...]
$hourlys = [false, false, false, ...]
$timeCards = [0, 0, 0, ...]
→ 4개의 배열을 동기화해야 함
After (Step 05) - 구조체 배열:
$employees = [
Employee("직원A", 400, false, 0),
Employee("직원B", 300, false, 0),
Employee("직원C", 250, false, 0),
...
]
→ 하나의 배열로 통합!
→ 각 요소가 완전한 직원 정보 포함
캡슐화:
# ✅ 외부에서 내부 구현 몰라도 됨
pay = employee.calculatePay(0.1)
# employee가 정규직인지 아르바이트인지?
# → 알 필요 없음!
# → 내부에서 알아서 처리타입 추상화:
# 모든 직원은 Employee 타입
for employee in $employees
pay = employee.calculatePay(taxRate)
# 동일한 인터페이스!
end인스턴스 개념:
# ✅ 각 직원을 개별 인스턴스로 다룰 수 있음
employee1 = Employee.new("직원A", 400, false, 0)
employee2 = Employee.new("직원B", 300, false, 0)
pay1 = employee1.calculatePay(0.1)
pay2 = employee2.calculatePay(0.1)def calculatePay(taxRate)
if (hourly) then # ❌ 타입 체크
return calculateHourlyPay(taxRate)
end
return calculateSalariedPay(taxRate)
end문제:
1. 타입 변수 (hourly) 사용
→ 타입에 따른 조건 분기
2. 새로운 타입 추가 시
→ if문에 케이스 추가 필요
3. 오퍼레이션을 기준으로 타입을 분류
→ 타입 추가에 취약
클래스는 이 문제를 다형성으로 해결!
📂 코드:
employees_step06.rb
# ✅ 부모 클래스: 공통 인터페이스 정의
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
# ✅ 자식 클래스 1: 정규 직원
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
# ✅ 자식 클래스 2: 아르바이트
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),
]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.monthlyBasePay()
end
puts(result)
end추상 데이터 타입 (Step 05):
┌──────────────────────────────┐
│ Employee (Struct) │
│ │
│ - name, basePay, hourly, │
│ timeCard │
│ │
│ + calculatePay(taxRate) │
│ if (hourly) then │ ← 타입 체크
│ hourlyPay │
│ else │
│ salariedPay │
│ end │
└──────────────────────────────┘
클래스 (Step 06):
┌──────────────┐
│ Employee │ ← 추상 인터페이스
└──────────────┘
↑ ↑
┌───────┘ └───────┐
│ │
┌──────────────┐ ┌──────────────┐
│Salaried │ │ Hourly │
│Employee │ │ Employee │
│ │ │ │
│calculatePay()│ │calculatePay()│ ← 각자 구현
└──────────────┘ └──────────────┘
새로운 요구사항: "계약직 직원 추가"
ADT 방식 (Step 05):
def calculatePay(taxRate)
if (hourly) then
return calculateHourlyPay(taxRate)
elsif (contract) then # ❌ 기존 코드 수정
return calculateContractPay(taxRate)
end
return calculateSalariedPay(taxRate)
end
# ❌ 모든 메서드에 조건문 추가 필요
def monthlyBasePay()
if (hourly) then return 0 end
if (contract) then return contractPay end # ❌ 추가
return basePay
end클래스 방식 (Step 06):
# ✅ 새 클래스만 추가! 기존 코드 수정 불필요
class ContractEmployee < Employee
attr_reader :monthlyPay
def initialize(name, monthlyPay)
super(name, monthlyPay)
@monthlyPay = monthlyPay
end
def calculatePay(taxRate)
return monthlyPay - (monthlyPay * taxRate)
end
def monthlyBasePay()
return monthlyPay
end
end
# ✅ 인스턴스 생성만 하면 됨
$employees << ContractEmployee.new("계약직G", 250)
# ✅ 기존 코드는 변경 없이 동작!
pay = employee.calculatePay(taxRate) # 다형성!| Step | 이름 | 핵심 개념 | 데이터 구조 | 코드 길이 | 주요 문제 |
|---|---|---|---|---|---|
| 01 | 순수 기능 분해 | 하향식, 전역 변수 | 병렬 배열 2개 | 20줄 | 단일 기능만 |
| 02 | Operation 패턴 | case문 분기 | 병렬 배열 2개 | 35줄 | main() 계속 수정 |
| 03 | 타입 추가 | 타입 플래그 | 병렬 배열 4개 | 55줄 | 동기화 복잡 |
| 04 | 모듈 | 정보 은닉 | 병렬 배열 4개 | 60줄 | 인스턴스 부족 |
| 05 | ADT | 구조체 | 구조체 배열 | 50줄 | 타입 체크 필요 |
| 06 | 클래스 | 다형성, 상속 | 객체 배열 | 70줄 | ✅ 해결 |
| 시나리오 | Step 01-03 | Step 04 | Step 05 | Step 06 |
|---|---|---|---|---|
| 새 직원 타입 추가 | 모든 함수 수정 | 모듈 내부 수정 | if문 추가 | ✅ 새 클래스만 |
| 급여 계산 방식 변경 | 여러 함수 수정 | 모듈 내부만 | 해당 부분만 | ✅ 해당 클래스만 |
| 데이터 구조 변경 | 전체 영향 | 모듈 내부만 | 구조체 정의만 | ✅ 클래스 내부만 |
| 새 오퍼레이션 추가 | 새 함수 추가 | 모듈에 추가 | 구조체에 추가 | ✅ 부모에 추가 |
| UI 변경 | 전체 재작성 | ✅ UI 레이어만 | ✅ UI 레이어만 | ✅ UI 레이어만 |
추상 데이터 타입 (ADT):
"오퍼레이션 기준으로 타입을 묶는다"
┌───────────────────────────┐
│ Employee (하나의 타입) │
│ │
│ if (정규직) then A │
│ elsif (아르바이트) then B │
│ │
│ 타입 내부에서 구분 │
└───────────────────────────┘
→ 타입 추상화
→ "Employee가 무엇인지" 캡슐화
클래스 (OOP):
"타입 기준으로 프로시저를 묶는다"
Employee (역할)
↑
┌───────┴───────┐
│ │
SalariedEmployee HourlyEmployee
│ │
구현 A 구현 B
→ 프로시저 추상화
→ "어떻게 하는지" 캡슐화
오퍼레이션 추가가 많다면?
→ ADT 유리
→ 새 오퍼레이션을 ADT에 추가
→ 모든 타입이 한 곳에 있어 수정 용이
타입 추가가 많다면?
→ 클래스 유리 ✅
→ 새 클래스만 추가
→ 개방-폐쇄 원칙 (OCP)
질문 1: "새로운 타입이 자주 추가되는가?"
YES → 클래스 선택
질문 2: "새로운 기능이 자주 추가되는가?"
YES → ADT도 고려 가능
질문 3: "타입마다 동작이 완전히 다른가?"
YES → 클래스 선택
질문 4: "모든 타입이 동일한 로직을 공유하는가?"
YES → ADT도 가능
대부분의 비즈니스 도메인:
→ 타입 추가가 더 빈번
→ 클래스 (객체지향) 선택
main("직원A")
├─ getTaxRate()
│ ├─ print("세율을 입력하세요: ")
│ ├─ gets() → "0.1"
│ └─ return 0.1
│
├─ calculatePayFor("직원A", 0.1)
│ ├─ $employees.index("직원A") → 0
│ ├─ $basePays[0] → 400
│ ├─ 400 - (400 * 0.1) → 360
│ └─ return 360
│
├─ describeResult("직원A", 360)
│ └─ return "이름 : 직원A, 급여 : 360"
│
└─ puts("이름 : 직원A, 급여 : 360")
출력: 이름 : 직원A, 급여 : 360
main(:pay, name:"아르바이트F")
├─ calculatePay("아르바이트F")
│ ├─ getTaxRate() → 0.1
│ │
│ ├─ hourly?("아르바이트F")
│ │ ├─ $employees.index("아르바이트F") → 5
│ │ ├─ $hourlys[5] → true
│ │ └─ return true
│ │
│ ├─ calculateHourlyPayFor("아르바이트F", 0.1)
│ │ ├─ index = 5
│ │ ├─ basePay = $basePays[5] = 1.5
│ │ ├─ timeCard = $timeCards[5] = 120
│ │ ├─ 1.5 * 120 = 180
│ │ ├─ 180 - (180 * 0.1) = 162
│ │ └─ return 162
│ │
│ ├─ describeResult("아르바이트F", 162)
│ └─ puts("이름 : 아르바이트F, 급여 : 162")
│
└─ 출력: 이름 : 아르바이트F, 급여 : 162
calculatePay("아르바이트F")
├─ getTaxRate() → 0.1
│
├─ for each in $employees
│ ├─ each = SalariedEmployee("직원A") → skip
│ ├─ each = SalariedEmployee("직원B") → skip
│ ├─ each = SalariedEmployee("직원C") → skip
│ ├─ each = HourlyEmployee("아르바이트D") → skip
│ ├─ each = HourlyEmployee("아르바이트E") → skip
│ └─ each = HourlyEmployee("아르바이트F") → match!
│ └─ employee = HourlyEmployee 인스턴스
│
├─ employee.calculatePay(0.1)
│ ├─ [다형성] HourlyEmployee.calculatePay() 호출
│ ├─ basePay = 1.5
│ ├─ timeCard = 120
│ ├─ 1.5 * 120 = 180
│ ├─ 180 - (180 * 0.1) = 162
│ └─ return 162
│
├─ describeResult("아르바이트F", 162)
└─ puts("이름 : 아르바이트F, 급여 : 162")
핵심: if문 없이 다형성으로 자동 선택!
Open-Closed Principle: 확장에는 열려있고, 수정에는 닫혀있어야 한다
ADT (Step 05):
새 타입 추가 → if문 수정 필요 ❌
클래스 (Step 06):
새 타입 추가 → 새 클래스만 추가 ✅
예시:
$employees << ContractEmployee.new("계약직G", 250)
# 기존 코드 수정 없이 동작!
클래스가 추상 데이터 타입을 따르는지 확인:
❌ 나쁜 신호:
if (employee.type == "정규직") then ...
if (employee.isHourly()) then ...
switch (employee.getType()) ...
✅ 좋은 신호:
employee.calculatePay() # 다형성
employee.monthlyBasePay() # 다형성
규칙: 클래스 내부에 타입 변수가 있다면 객체지향이 아니다!
잘못된 접근:
1. 직원 클래스 설계
2. 타입별로 분리 (정규직/아르바이트)
3. 각각 메서드 구현
올바른 접근:
1. 협력 파악: "급여를 계산해야 한다"
2. 책임 할당: "Employee가 급여 계산"
3. 다양한 방식 필요 → 다형성 적용
객체지향은 협력을 설계하는 것!
타입 분해는 도구일 뿐!
A:
모듈 (Step 04):
- 데이터 + 함수를 하나로 묶음
- 인스턴스 개념 없음
- 정적 메서드만 제공
- 예: Employees.calculatePay("직원A", 0.1)
클래스 (Step 06):
- 데이터 + 메서드를 하나로 묶음
- 인스턴스 개념 있음
- 인스턴스 메서드 제공
- 예: employee.calculatePay(0.1)
핵심 차이: 인스턴스!
A:
ADT가 유리한 경우:
1. 타입이 고정되어 있음
2. 오퍼레이션이 자주 추가됨
3. 모든 타입이 한 곳에 있어야 이해하기 쉬움
예: 수학 라이브러리
- Vector, Matrix, Quaternion (타입 고정)
- add, multiply, normalize (오퍼레이션 추가)
- 모든 연산을 한눈에 보고 싶음
클래스가 유리한 경우:
1. 타입이 자주 추가됨
2. 오퍼레이션이 상대적으로 안정적
3. 타입마다 완전히 다른 구현
예: 비즈니스 도메인
- 직원 타입, 주문 타입 (계속 추가)
- calculateTotal, validate (비교적 안정)
A:
상속의 문제점:
1. 강한 결합
- 부모 변경 → 모든 자식 영향
2. 불필요한 메서드 상속
- "is-a" 관계가 아닌 경우 문제
3. 다중 상속 불가
- 여러 역할을 동시에 수행하기 어려움
대안: 컴포지션 (Composition)
- "has-a" 관계
- 유연한 조합
- 느슨한 결합
규칙:
- 상속은 타입 계층이 명확할 때만
- 행동을 공유하려면 컴포지션 고려
- "is-a"인지 항상 확인
A:
기능 분해가 유용한 경우:
✅ 작은 유틸리티 함수
✅ 이미 해결된 알고리즘 문서화
✅ 한 번 작성하고 변경 없는 스크립트
예시:
def calculateTax(amount, rate)
return amount * rate
end
def formatCurrency(amount)
return "$#{amount.round(2)}"
end
→ 간단하고 재사용 가능
→ 변경될 가능성 낮음
→ 함수로 충분!
기능 분해가 문제가 되는 경우:
❌ 대규모 시스템
❌ 빈번한 요구사항 변경
❌ 복잡한 도메인 로직
→ 이 경우 객체지향 필요
현재 코드가 Step 03과 비슷하다면?
1단계: 모듈 추출 (Step 04)
- 관련 데이터와 함수를 모듈로 묶기
- 퍼블릭 인터페이스 정의
- 예상 시간: 1-2일
2단계: ADT 도입 (Step 05)
- 병렬 배열을 구조체로 변환
- 오퍼레이션 메서드 추가
- 예상 시간: 2-3일
3단계: 클래스로 전환 (Step 06)
- 타입별로 클래스 분리
- 다형성 적용
- 조건문 제거
- 예상 시간: 3-5일
총 예상 시간: 1-2주
→ 점진적 개선이 핵심!
시나리오: 5000줄의 절차적 코드
❌ 나쁜 접근:
"전체를 객체지향으로 재작성!"
→ 위험도 높음
→ 비즈니스 가치 없음
✅ 좋은 접근:
1. Boy Scout Rule
- 수정할 때마다 조금씩 개선
2. Strangler Fig Pattern
- 새 기능은 객체지향으로
- 기존 코드는 점진적 교체
3. 핵심부터 개선
- 자주 변경되는 부분 우선
- 안정적인 부분은 나중에
예시:
Week 1: 직원 타입 부분만 클래스로
Week 2: 급여 계산 로직 개선
Week 3: 보고서 생성 부분 리팩터링
...
6개월 후: 90% 개선
1. "Refactoring" - Martin Fowler
→ Replace Type Code with Class
→ Replace Type Code with Subclasses
→ 실전 리팩터링 기법
2. "Working Effectively with Legacy Code" - Michael Feathers
→ 레거시 코드 다루기
→ 의존성 깨기
→ 테스트 가능하게 만들기
3. "Design Patterns" - Gang of Four
→ Strategy 패턴 (행동 캡슐화)
→ Template Method (알고리즘 골격)
→ Factory 패턴 (객체 생성)
4. "Domain-Driven Design" - Eric Evans
→ 도메인 모델 설계
→ Aggregate 패턴
→ Repository 패턴
SRP (Single Responsibility Principle)
- 하나의 변경 이유만
- Step 04 모듈이 적용한 원칙
OCP (Open-Closed Principle)
- 확장에 열려있고 수정에 닫혀있음
- Step 06 클래스가 달성한 목표
LSP (Liskov Substitution Principle)
- 부모 타입을 자식 타입으로 대체 가능
- Step 06 상속 구조의 전제 조건
ISP (Interface Segregation Principle)
- 클라이언트별로 인터페이스 분리
- 불필요한 의존성 제거
DIP (Dependency Inversion Principle)
- 추상화에 의존
- 구체 클래스에 의존하지 않기
Step 01-03: 문제 발견
- 하향식 기능 분해의 한계
- 전역 데이터의 문제
- 변경의 파급효과
Step 04: 첫 번째 해결책
- 모듈과 정보 은닉
- 변경의 캡슐화
- 관심사의 분리
Step 05: 두 번째 해결책
- 추상 데이터 타입
- 인스턴스 개념
- 타입 추상화
Step 06: 최종 해결책
- 객체지향과 클래스
- 다형성과 상속
- 프로시저 추상화
□ 데이터와 함수가 함께 변경되는가?
YES → 모듈/클래스로 묶기
□ 개별 인스턴스를 다뤄야 하는가?
YES → ADT 또는 클래스
□ 타입이 자주 추가되는가?
YES → 클래스 선택
□ 타입별로 동작이 완전히 다른가?
YES → 다형성 적용
□ 변경이 거의 없는 단순 로직인가?
YES → 함수로도 충분
□ 협력을 설계했는가?
NO → 먼저 협력 설계!
객체지향은 목적이 아니라 수단:
목적: 변경에 유연한 시스템
수단: 캡슐화, 추상화, 다형성
"객체지향을 위한 객체지향"은 금물
"문제 해결을 위한 객체지향"을 하자
핵심은 항상:
1. 변경을 관리하라
2. 복잡도를 낮춰라
3. 협력을 설계하라
[Step 01] 순수 기능 분해
↓ 문제: 단일 기능만 지원
[Step 02] Operation 패턴
↓ 문제: main() 계속 수정
[Step 03] 타입 추가 (병렬 배열)
↓ 문제: 데이터 동기화, 타입 체크 범람
[Step 04] 모듈 (정보 은닉)
↓ 문제: 인스턴스 개념 부족
[Step 05] 추상 데이터 타입
↓ 문제: if문으로 타입 구분
[Step 06] 클래스 (다형성)
✅ 문제 해결!
Chapter 08: 의존성 관리하기
- 의존성의 종류와 문제
- 의존성 방향과 안정성
- 의존성 역전 원리 (DIP)
- 유연한 설계 기법
"실제 시스템에 정상(top)이란 존재하지 않는다" - Parnas
이론:
┌─────────────────┐
│ main() │ ← 모든 것의 시작점
│ │
│ 시스템 전체 │
└─────────────────┘
현실:
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│main1 │ │main2 │ │main3 │ │main4 │
└──────┘ └──────┘ └──────┘ └──────┘
↓ ↓ ↓ ↓
급여 보고서 통계 백업
계산 생성 분석 처리
→ 여러 개의 진입점!
→ 메인 함수는 하나가 아니다!
초기 릴리즈:
# Version 1.0
def main(name)
calculateAndPrintPay(name)
end6개월 후:
# Version 1.5
def main(operation, args)
case operation
when :pay then calculatePay(args[:name])
when :report then generateReport()
when :export then exportToExcel()
when :backup then backupData()
end
end1년 후:
# Version 2.0
# main() 함수가 너무 커져서 분리
def main_payroll(args)
# 급여 관련
end
def main_hr(args)
# 인사 관련
end
def main_admin(args)
# 관리 관련
end
# → "하나의 메인 함수" 개념 붕괴!하향식 설계의 가정:
"시스템이 수행해야 할 하나의 주요 기능이 있다"
현실:
- 사용자는 계속 새로운 기능 요청
- 비즈니스는 계속 진화
- 시스템은 다양한 용도로 사용됨
결과:
→ 최초의 "메인 기능"은 점점 의미 없어짐
→ 시스템 = 동등한 수준의 여러 기능들
→ 계층 구조 = 환상
Open-Closed Principle 위반의 전형
시나리오 1: 기본급 합계 기능 추가
# Before
def main(name)
taxRate = getTaxRate()
pay = calculatePayFor(name, taxRate)
puts(describeResult(name, pay))
end
# After - main() 수정 필요!
def main(operation, args={})
case(operation)
when :pay then calculatePay(args[:name])
when :basePays then sumOfBasePays() # ← 추가
end
end시나리오 2: 직원 목록 조회 기능 추가
def main(operation, args={})
case(operation)
when :pay then calculatePay(args[:name])
when :basePays then sumOfBasePays()
when :list then listEmployees() # ← 추가
end
end시나리오 3: 연간 총 급여 계산 추가
def main(operation, args={})
case(operation)
when :pay then calculatePay(args[:name])
when :basePays then sumOfBasePays()
when :list then listEmployees()
when :annual then calculateAnnualPay() # ← 추가
end
end패턴:
새 기능 추가 → main() 수정 필요
새 기능 추가 → main() 수정 필요
새 기능 추가 → main() 수정 필요
...
결과:
1. main()이 계속 커짐
2. 수정할 때마다 버그 위험
3. 모든 개발자가 main()을 건드림
4. 병합 충돌 (Merge Conflict)
근본 원인:
"기능 추가 = 기존 코드 수정"
→ OCP 위반!
# 조심스럽게 기능 추가
def main(operation, args={})
case(operation)
when :pay then calculatePay(args[:name])
when :basePays then sumOfBasePays()
when :list then listEmployees()
when :annual then calculateAnnualPay()
# ... 10개 더 ...
end
end
# 😱 실수로 오타
def main(operation, args={})
case(operation)
when :pay then calculatePay(args[:name])
when :basePays then sumOfBasePays()
when :list then listEmployees()
when :annual then calculateAnnualPay()
when :export then exportData()
whne :import then importData() # ← 오타! when → whne
end
end
# 결과: import 기능 동작 안 함
# 원인 파악: 어려움 (에러 없이 조용히 실패)def calculatePay(name)
# UI Layer: 사용자 입력
print("세율을 입력하세요: ") # ← Console I/O
taxRate = gets().chomp().to_f() # ← Console I/O
# Business Layer: 계산
index = $employees.index(name)
basePay = $basePays[index]
pay = basePay - (basePay * taxRate)
# UI Layer: 결과 출력
puts("이름 : #{name}, 급여 : #{pay}") # ← Console I/O
end계층이 섞여있음:
calculatePay() 안에:
- 입력 (UI)
- 계산 (Business)
- 출력 (UI)
→ 3가지 관심사가 하나의 함수에!
시나리오 1: 웹 인터페이스로 전환
기존: Console I/O
목표: Web Form
필요한 변경:
❌ getTaxRate() - 전체 재작성
❌ calculatePay() - 전체 재작성
❌ describeResult() - 전체 재작성
이유: UI와 로직이 섞여있어서
시나리오 2: REST API 제공
기존: 직접 출력 (puts)
목표: JSON 반환
필요한 변경:
❌ 모든 함수의 출력 부분 제거
❌ 반환 값을 JSON으로 변환
❌ HTTP 응답 처리 추가
이유: 출력이 로직에 섞여있어서
시나리오 3: 단위 테스트 작성
문제:
- getTaxRate()가 사용자 입력 대기
- 테스트 자동화 불가능
해결책:
- 모든 I/O를 mock해야 함
- 복잡하고 깨지기 쉬운 테스트
이유: 테스트 불가능한 설계
# ✅ 올바른 분리
# Business Layer - 순수 함수
def calculatePayFor(name, taxRate)
index = $employees.index(name)
basePay = $basePays[index]
return basePay - (basePay * taxRate)
end
# UI Layer - Console
def console_calculatePay(name)
taxRate = console_getTaxRate()
pay = calculatePayFor(name, taxRate)
console_displayResult(name, pay)
end
# UI Layer - Web
def web_calculatePay(name, taxRate)
pay = calculatePayFor(name, taxRate)
return {name: name, pay: pay}.to_json
end
# UI Layer - Test
def test_calculatePay()
pay = calculatePayFor("직원A", 0.1)
assert_equal(360, pay)
end"어떻게(how)"가 "무엇을(what)"보다 앞서면 안 된다
def calculatePay(name)
# 1. 순서 고정: 항상 입력부터
taxRate = getTaxRate()
# 2. 순서 고정: 그 다음 계산
pay = calculatePayFor(name, taxRate)
# 3. 순서 고정: 마지막 출력
puts(describeResult(name, pay))
end문제:
만약:
- 세율을 미리 알고 있다면?
- 여러 직원을 한 번에 계산한다면?
- 결과를 파일에 저장한다면?
- 계산 후 추가 처리가 필요하다면?
→ 모두 함수 수정 필요!
→ 재사용 불가능!
# 모든 제어가 상위 함수에 집중
def processPayroll()
# Step 1: 준비
loadEmployeeData()
# Step 2: 처리 (순서 고정!)
for name in $employees
# Step 2-1: 입력
taxRate = getTaxRate()
# Step 2-2: 계산
pay = calculatePayFor(name, taxRate)
# Step 2-3: 출력
puts(describeResult(name, pay))
# Step 2-4: 저장
saveToDatabase(name, pay)
end
# Step 3: 정리
generateReport()
end하위 함수의 운명:
calculatePayFor():
- 언제 호출될지 정해짐 (Step 2-2)
- 무엇과 함께 호출될지 정해짐 (getTaxRate, describeResult)
- 어떤 순서로 호출될지 정해짐 (고정된 루프)
→ 상위 함수의 문맥에 강하게 종속
→ 다른 문맥에서 재사용 불가
→ 유연성 제로
하향식 설계:
# ❌ 재사용 어려움
def calculatePayAndPrint(name)
taxRate = getTaxRate()
pay = calculatePayFor(name, taxRate)
puts(describeResult(name, pay))
end
# 다른 곳에서 사용하려면?
# → calculatePayAndPrint를 수정하거나
# → 새로운 함수 작성객체지향 설계:
# ✅ 재사용 쉬움
employee = Employee.new("직원A", 400)
# 상황 1: Console 출력
pay1 = employee.calculatePay(0.1)
puts "급여: #{pay1}"
# 상황 2: Web 응답
pay2 = employee.calculatePay(0.15)
render json: {pay: pay2}
# 상황 3: Batch 처리
employees.each do |emp|
pay = emp.calculatePay(taxRate)
savePay(emp, pay)
end
# → 같은 메서드, 다양한 문맥!"전역 변수 = 변경의 공포"
# 전역 데이터
$employees = ["직원A", "직원B", "직원C"]
$basePays = [400, 300, 250]
$hourlys = [false, false, false]
$timeCards = [0, 0, 0]
# 질문: $basePays를 수정하면 어떤 함수가 영향을 받을까?
# 답: 일일이 찾아봐야 함...
def calculatePayFor(name, taxRate)
basePay = $basePays[...] # ← 영향받음
end
def sumOfBasePays()
for basePay in $basePays # ← 영향받음
...
end
end
def averageBasePay()
sum = 0
for pay in $basePays # ← 영향받음
sum += pay
end
return sum / $basePays.length # ← 여기도!
end
def exportBasePays()
File.write("pays.csv", $basePays.join(",")) # ← 영향받음
end
# → 전체 코드를 검색해야만 알 수 있음!시나리오: basePays를 배열에서 Hash로 변경
# Before
$employees = ["직원A", "직원B", "직원C"]
$basePays = [400, 300, 250]
def calculatePayFor(name, taxRate)
index = $employees.index(name)
basePay = $basePays[index] # Array 접근
return basePay - (basePay * taxRate)
end
# After
$basePays = {
"직원A" => 400,
"직원B" => 300,
"직원C" => 250
}
def calculatePayFor(name, taxRate)
basePay = $basePays[name] # Hash 접근
return basePay - (basePay * taxRate)
end영향받는 모든 곳:
✅ calculatePayFor() - 수정 완료
❌ calculateHourlyPayFor() - 아직 Array 접근!
❌ sumOfBasePays() - 아직 Array 접근!
❌ averageBasePay() - 아직 Array 접근!
❌ exportBasePays() - 아직 Array 접근!
❌ printAllPays() - 아직 Array 접근!
...
→ 놓친 곳에서 런타임 에러!
→ 디버깅 지옥!
전역 변수 시스템:
$employees ←─┬─ calculatePayFor()
├─ sumOfBasePays()
├─ listEmployees()
├─ findEmployee()
├─ addEmployee()
├─ removeEmployee()
├─ exportEmployees()
└─ ... (20개 이상)
$basePays ←─┬─ calculatePayFor()
├─ calculateHourlyPayFor()
├─ sumOfBasePays()
├─ averageBasePay()
├─ maxBasePay()
├─ minBasePay()
├─ exportBasePays()
└─ ... (15개 이상)
→ 변경 영향 파악 불가능!
→ 두려움 기반 프로그래밍
class Employee
def initialize(name, basePay)
@name = name
@basePay = basePay
end
def calculatePay(taxRate)
return @basePay - (@basePay * taxRate)
end
end
# basePay 변경 시 영향 범위:
# → Employee 클래스 내부만!
# 확인 방법:
# → Employee.rb 파일만 보면 됨!
# 변경 두려움:
# → 감소!| 문제 | Step 01 | Step 02 | Step 03 | Step 04 | Step 05 | Step 06 |
|---|---|---|---|---|---|---|
| 하나의 메인 함수 | 🔴 최악 | 🟡 개선 시도 | 🟡 여전함 | 🟢 해결 | 🟢 해결 | 🟢 해결 |
| 메인 함수 수정 | 🔴 매번 | 🔴 매번 | 🔴 매번 | 🟡 모듈만 | 🟡 구조체만 | 🟢 해결 |
| UI/로직 결합 | 🔴 완전 섞임 | 🔴 완전 섞임 | 🔴 완전 섞임 | 🟢 분리됨 | 🟢 분리됨 | 🟢 분리됨 |
| 실행 순서 고정 | 🔴 고정 | 🔴 고정 | 🔴 고정 | 🟡 부분 개선 | 🟢 유연 | 🟢 유연 |
| 데이터 파급효과 | 🔴 추적 불가 | 🔴 추적 불가 | 🔴 더 심함 | 🟡 모듈 내부 | 🟢 캡슐화 | 🟢 캡슐화 |
| 타입 추가 | 🔴 모든 함수 | 🔴 모든 함수 | 🔴 모든 함수 | 🟡 모듈 수정 | 🟡 if문 추가 | 🟢 클래스 추가 |
| 재사용성 | 🔴 거의 없음 | 🔴 거의 없음 | 🔴 거의 없음 | 🟡 모듈 단위 | 🟡 구조체 단위 | 🟢 높음 |
| 테스트 용이성 | 🔴 어려움 | 🔴 어려움 | 🔴 어려움 | 🟡 가능 | 🟢 쉬움 | 🟢 쉬움 |
Step 03 (정보 은닉 없음):
┌────────────────────────────┐
│ 전역 스코프 │
│ │
│ $employees │ ← 모두 접근 가능
│ $basePays │ ← 모두 접근 가능
│ $hourlys │ ← 모두 접근 가능
│ $timeCards │ ← 모두 접근 가능
│ │
│ function1() │
│ function2() │
│ ... │
└────────────────────────────┘
Step 04 (모듈로 은닉):
┌────────────────────────────┐
│ Employees Module │
│ ┌──────────────────────┐ │
│ │ Private: │ │
│ │ $employees │ │ ← 모듈 내부만
│ │ $basePays │ │ ← 모듈 내부만
│ │ $hourlys │ │ ← 모듈 내부만
│ │ $timeCards │ │ ← 모듈 내부만
│ └──────────────────────┘ │
│ ┌──────────────────────┐ │
│ │ Public: │ │
│ │ calculatePay() │ │ ← 외부 접근 허용
│ │ sumOfBasePays() │ │ ← 외부 접근 허용
│ └──────────────────────┘ │
└────────────────────────────┘
Step 06 (객체로 캡슐화):
┌──────────────────┐
│ Employee │
│ ┌────────────┐ │
│ │ Private: │ │
│ │ @name │ │ ← 인스턴스마다
│ │ @basePay │ │ ← 독립적
│ └────────────┘ │
│ ┌────────────┐ │
│ │ Public: │ │
│ │ calcPay() │ │ ← 인스턴스 메서드
│ └────────────┘ │
└──────────────────┘
Step 01-03 (낮은 응집도):
calculatePay():
- UI 입력
- 비즈니스 로직
- UI 출력
- 데이터 접근
→ 4가지 책임!
Step 04 (중간 응집도):
Employees 모듈:
- 직원 데이터 관리
- 급여 계산
- 통계 계산
→ 관련 책임 묶음
Step 06 (높은 응집도):
Employee 클래스:
- 자신의 데이터 관리
- 자신의 급여 계산
→ 단일 책임!
Step 03 (높은 결합도):
calculatePay()
→ $employees (전역)
→ $basePays (전역)
→ $hourlys (전역)
→ getTaxRate() (함수)
→ describeResult() (함수)
→ 5개 이상의 의존성!
Step 04 (중간 결합도):
외부 코드
→ Employees.calculatePay()
Employees 모듈
→ 내부 데이터만
→ 의존성 감소!
Step 06 (낮은 결합도):
client
→ employee.calculatePay(taxRate)
employee
→ @basePay만 사용
→ 최소 의존성!
1. 일회성 스크립트
# ✅ 하향식 분해 적합
def backup_database()
connect_to_db()
export_tables()
compress_files()
upload_to_s3()
send_notification()
end
# 이유:
# - 한 번 작성하고 끝
# - 변경 거의 없음
# - 순서가 명확함2. 알고리즘 문서화
# ✅ 하향식 분해 적합
def quicksort(arr)
return arr if arr.length <= 1
pivot = arr[0]
less = arr[1..]select { |x| x <= pivot }
greater = arr[1..].select { |x| x > pivot }
return quicksort(less) + [pivot] + quicksort(greater)
end
# 이유:
# - 알고리즘은 고정
# - 순서가 핵심
# - 재사용보다 이해가 중요3. 설정 스크립트
# ✅ 하향식 분해 적합
def setup_development_environment()
install_dependencies()
create_database()
run_migrations()
seed_test_data()
start_services()
end
# 이유:
# - 정해진 순서 필요
# - 반복 작업
# - 변경 드뭄1. 대규모 비즈니스 시스템
# ❌ 하향식 분해 부적합
def process_order(order)
validate_order(order)
check_inventory(order)
calculate_price(order)
process_payment(order)
update_inventory(order)
send_confirmation(order)
notify_warehouse(order)
end
# 문제:
# - 요구사항 자주 변경
# - 각 단계가 복잡함
# - 재사용 필요
# → 객체지향 필요!2. 다중 사용자 상호작용
# ❌ 하향식 분해 부적합
def handle_user_request(request)
authenticate_user(request)
authorize_action(request)
validate_input(request)
process_business_logic(request)
format_response(request)
end
# 문제:
# - 다양한 요청 타입
# - 각 단계가 독립적
# - 순서 변경 가능
# → 객체지향 필요!3. 장기 유지보수 프로젝트
# ❌ 하향식 분해 부적합
def generate_report(type)
load_data(type)
filter_data(type)
aggregate_data(type)
format_output(type)
save_report(type)
end
# 문제:
# - 새 리포트 타입 추가
# - 각 단계 커스터마이징
# - 재사용 및 확장 필요
# → 객체지향 필요!하향식 분해를 사용해도 되는 경우:
□ 요구사항이 안정적인가?
□ 일회성 또는 단순 반복인가?
□ 순서가 명확하고 고정적인가?
□ 유지보수 기간이 짧은가?
□ 재사용 필요성이 낮은가?
5개 중 4개 이상 YES → 하향식 OK
하향식을 피해야 하는 경우:
□ 요구사항이 자주 변경되는가?
□ 장기 유지보수가 필요한가?
□ 다양한 변형이 필요한가?
□ 여러 곳에서 재사용해야 하는가?
□ 복잡한 비즈니스 로직인가?
5개 중 3개 이상 YES → 객체지향 필요!