Skip to content

Latest commit

 

History

History
 
 

README.md

Chapter 07. 객체 분해

"추상화를 통해 복잡성을 극복하고, 분해를 통해 큰 문제를 작은 문제로 나누자"

📌 핵심 개념

  • 추상화(Abstraction): 불필요한 정보를 제거하고 문제 해결에 필요한 핵심만 남기는 작업
  • 분해(Decomposition): 큰 문제를 해결 가능한 작은 문제로 나누는 작업
  • 프로시저 추상화: 소프트웨어가 무엇을 해야 하는지 추상화 → 기능 분해
  • 데이터 추상화: 소프트웨어가 무엇을 알아야 하는지 추상화 → 추상 데이터 타입 / 객체지향
  • 정보 은닉(Information Hiding): 변경되는 부분을 안정적인 인터페이스 뒤로 감추기
  • 모듈(Module): 변경을 관리하기 위한 기본 구현 단위

🎯 학습 목표

  1. 인지 과부하를 극복하는 추상화와 분해의 필요성 이해하기
  2. 하향식 기능 분해의 문제점을 명확히 파악하기
  3. 정보 은닉과 모듈을 통한 변경 관리 방법 학습하기
  4. 추상 데이터 타입클래스의 차이점 이해하기
  5. 급여 시스템의 진화를 통해 객체지향의 장점 체득하기

🔑 Chapter 핵심 메시지

왜 분해가 필요한가?

인간의 단기 기억 한계: 7±2개 항목
→ 복잡한 문제를 한 번에 이해할 수 없음

해결책 1: 추상화 (Abstraction)
- 불필요한 세부사항 제거
- 핵심만 남기기
- 이해 가능한 수준으로 단순화

해결책 2: 분해 (Decomposition)
- 큰 문제를 작은 문제로
- 각각을 독립적으로 해결
- 다시 조합하여 전체 해결

이번 챕터의 여정:

절차적 프로그래밍 (기능 분해)
    ↓ 문제점 발견
모듈 (정보 은닉)
    ↓ 인스턴스 개념 부족
추상 데이터 타입
    ↓ 다형성 부족
객체지향 (클래스)

💭 00. 인지 과부하와 분해의 필요성

단기 기억의 한계

George Miller의 연구: 인간은 한 번에 7±2개 항목만 기억 가능

문제 해결 과정:
1. 장기 기억에서 필요한 정보 인출
2. 단기 기억으로 이동
3. 단기 기억에서 문제 해결
4. 결과를 다시 장기 기억으로 저장

병목: 단기 기억의 용량!
→ 7개를 초과하는 순간 인지 과부하 발생
→ 문제 해결 능력 급격히 저하

추상화와 분해로 극복

┌─────────────────────────────────────┐
│  복잡한 시스템 (100개 요소)              │
│                                     │
│  ❌ 한 번에 이해 불가능                 │
└─────────────────────────────────────┘

         추상화 & 분해 ↓

┌──────────┐  ┌──────────┐ ┌──────────┐
│ 모듈 A    │  │ 모듈 B    │ │ 모듈 C    │
│ (5개)    │  │ (5개)     │ │ (5개)     │
└──────────┘  └──────────┘ └──────────┘

✅ 각 모듈은 이해 가능한 크기
✅ 모듈 간 인터페이스로 연결
✅ 한 번에 하나의 모듈에 집중

핵심:

추상화 = 복잡도 감소
분해 = 관리 가능한 크기로 나누기
추상화 + 분해 = 복잡한 시스템 구축 가능

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

두 가지 추상화 메커니즘

현대 프로그래밍 언어의 핵심:

추상화 종류 질문 중심 개념 분해 방법
프로시저 추상화 "무엇을 해야 하는가?" 기능, 알고리즘 기능 분해
데이터 추상화 "무엇을 알아야 하는가?" 데이터, 타입 타입/객체 분해

프로시저 추상화 (Procedure Abstraction)

초점: 소프트웨어가 수행해야 할 기능

예시: "급여를 계산한다"
→ 어떤 데이터를 사용할지는 나중에 결정
→ 먼저 기능을 분해

결과: 기능 중심 시스템
- 함수들의 계층 구조
- 데이터는 전역 변수로 공유
- 하향식 접근법

데이터 추상화 (Data Abstraction)

초점: 소프트웨어가 알아야 할 정보

예시: "직원 정보"
→ 어떤 기능이 필요한지는 나중에 결정
→ 먼저 데이터를 정의

두 가지 방향:
1. 타입 추상화 → 추상 데이터 타입 (ADT)
2. 프로시저 추상화 → 객체지향 (OOP)

객체지향의 위치

프로그래밍 패러다임의 스펙트럼:

[기능 분해]────[모듈]────[추상 데이터 타입]────[객체지향]
    ↑              ↑            ↑                ↑
  함수 중심    정보 은닉     타입 추상화      다형성 추가

객체지향 = 데이터 추상화 + 프로시저 추상화
         = 상속 + 다형성

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

메인 함수로서의 시스템

전통적 관점: 시스템 = 하나의 큰 함수

시스템의 개념적 모델:

┌────────────────────────────────────┐
│         main(input)                │
│                                    │
│  1. 입력을 받는다                     │
│  2. 입력을 처리한다                    │
│  3. 결과를 출력한다                    │
│                                    │
│         return output              │
└────────────────────────────────────┘

특징:
- 시스템 = 거대한 수학 함수
- 입력 → 처리 → 출력의 단순 흐름
- 모든 것이 메인 함수 아래 배치

하향식 접근법 (Top-Down Approach)

1단계: 최상위 기능 정의
"직원의 급여를 계산한다"

2단계: 하위 기능으로 분해
"직원의 급여를 계산한다"
  ├─ "사용자로부터 소득세율을 입력받는다"
  ├─ "직원의 급여를 계산한다"
  └─ "양식에 맞게 결과를 출력한다"

3단계: 더 세부적으로 분해
"사용자로부터 소득세율을 입력받는다"
  ├─ "세율을 입력하세요"를 출력한다
  └─ 키보드로 세율을 입력받는다

4단계: 구현 가능할 때까지 반복

결과물: 함수들의 계층적 트리 구조

                  main()
                    │
        ┌───────────┼───────────┐
        │           │           │
   getTaxRate()  calculatePay()  describeResult()
                    │
        ┌───────────┴───────────┐
   getBasePay()         calculateFee()

💰 급여 관리 시스템: 6단계 진화 과정

시스템 요구사항

기본 규칙:
1. 회사는 정규 직원과 아르바이트 직원을 고용
2. 정규 직원: 기본급 - (기본급 × 소득세율)
3. 아르바이트: 시급 × 근무시간 - (시급 × 근무시간 × 소득세율)
4. 매월 모든 직원의 급여 계산 및 지급

이제 6단계에 걸쳐 시스템이 어떻게 진화하는지 추적해봅시다!


🔴 Step 01: 순수 기능 분해

📂 코드: 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"

문제점 1: 하나의 메인 함수

❌ 새로운 기능 추가 시나리오

요구사항: "전체 직원 기본급 합계 계산"

문제:
- main() 함수는 한 명의 급여만 계산
- 합계 계산은 완전히 다른 기능
- main()을 어떻게 수정해야 할까?

→ 시스템 = 하나의 메인 함수라는 가정 붕괴!

🟠 Step 02: Operation 패턴 도입

📂 코드: 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() 수정 필요

문제점 2: 메인 함수의 빈번한 수정

시나리오: 새 기능 추가

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

❌ 개방-폐쇄 원칙 위반!
❌ 기존 코드 수정 → 버그 위험

🟡 Step 03: 아르바이트 직원 추가

📂 코드: 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)가 명시적으로 필요

문제점 3: 비즈니스 로직과 UI의 결합

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가 섞여있음

문제점 4: 성급한 실행 순서 결정

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

문제:

만약 세율을 미리 알고 있다면?
→ 함수 수정 필요

만약 결과를 출력하지 않고 반환만 하려면?
→ 함수 수정 필요

만약 계산 후 다른 처리를 하려면?
→ 함수 수정 필요

실행 순서가 너무 일찍, 너무 강하게 결정됨
→ 재사용성 ↓

문제점 5: 데이터 변경의 파급효과

시나리오: timeCards 배열을 Dictionary로 변경

$timeCards = [120, 120, 120]  # Before
↓
$timeCards = {"아르바이트D" => 120, ...}  # After

영향받는 함수들:
❌ calculateHourlyPayFor() - 접근 방식 변경
❌ 모든 timeCards를 사용하는 함수들

문제: 어떤 함수들이 영향받는지 추적 어려움

근본 원인:

데이터 = 전역 변수
함수 = 데이터에 자유롭게 접근

→ 데이터와 함수 사이의 의존성이 명시적이지 않음
→ 변경 영향 범위 파악 불가능
→ 유지보수 악몽

🟢 Step 04: 모듈을 통한 정보 은닉

📂 코드: 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())  # ✅ 모듈 통해서만 접근
end

모듈의 장점

1. 변경 영향 최소화

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)

→ 인스턴스 개념 필요!

🔵 Step 05: 추상 데이터 타입

📂 코드: employees_step05.rb

추상 데이터 타입 (Abstract Data Type, ADT)

정의: 데이터와 그 데이터를 조작하는 오퍼레이션을 하나로 묶은 것

타입 (Type) = 데이터 + 연산

예시:
Integer 타입 = {정수값} + {+, -, *, /, ...}
String 타입 = {문자열} + {concat, substring, ...}

추상 데이터 타입:
사용자 정의 타입 = {내부 데이터} + {오퍼레이션}

Struct를 이용한 구현

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)
end

데이터 구조 변화

Before (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),
  ...
]
→ 하나의 배열로 통합!
→ 각 요소가 완전한 직원 정보 포함

ADT의 특징

캡슐화:

# ✅ 외부에서 내부 구현 몰라도 됨
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)

ADT의 문제점

def calculatePay(taxRate)
  if (hourly) then  # ❌ 타입 체크
    return calculateHourlyPay(taxRate)
  end
  return calculateSalariedPay(taxRate)
end

문제:

1. 타입 변수 (hourly) 사용
   → 타입에 따른 조건 분기

2. 새로운 타입 추가 시
   → if문에 케이스 추가 필요

3. 오퍼레이션을 기준으로 타입을 분류
   → 타입 추가에 취약

클래스는 이 문제를 다형성으로 해결!

🟣 Step 06: 객체지향 (클래스)

📂 코드: 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

ADT vs 클래스 비교

추상 데이터 타입 (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)  # 다형성!

📊 6단계 비교표

전체 진화 과정

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 vs 클래스: 핵심 차이

타입 추상화 vs 프로시저 추상화

추상 데이터 타입 (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도 가능

대부분의 비즈니스 도메인:
→ 타입 추가가 더 빈번
→ 클래스 (객체지향) 선택

🔍 Step별 코드 실행 추적

Step 01 실행 흐름

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

Step 03 실행 흐름 (아르바이트)

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

Step 06 실행 흐름 (다형성)

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문 없이 다형성으로 자동 선택!

💡 핵심 통찰

개방-폐쇄 원칙 (OCP)

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. 다양한 방식 필요 → 다형성 적용

객체지향은 협력을 설계하는 것!
타입 분해는 도구일 뿐!

❓ 자주 하는 질문

Q1. 모듈과 클래스의 차이는?

A:

모듈 (Step 04):
- 데이터 + 함수를 하나로 묶음
- 인스턴스 개념 없음
- 정적 메서드만 제공
- 예: Employees.calculatePay("직원A", 0.1)

클래스 (Step 06):
- 데이터 + 메서드를 하나로 묶음
- 인스턴스 개념 있음
- 인스턴스 메서드 제공
- 예: employee.calculatePay(0.1)

핵심 차이: 인스턴스!

Q2. ADT는 언제 사용하나요?

A:

ADT가 유리한 경우:
1. 타입이 고정되어 있음
2. 오퍼레이션이 자주 추가됨
3. 모든 타입이 한 곳에 있어야 이해하기 쉬움

예: 수학 라이브러리
- Vector, Matrix, Quaternion (타입 고정)
- add, multiply, normalize (오퍼레이션 추가)
- 모든 연산을 한눈에 보고 싶음

클래스가 유리한 경우:
1. 타입이 자주 추가됨
2. 오퍼레이션이 상대적으로 안정적
3. 타입마다 완전히 다른 구현

예: 비즈니스 도메인
- 직원 타입, 주문 타입 (계속 추가)
- calculateTotal, validate (비교적 안정)

Q3. 상속은 항상 좋은가요?

A:

상속의 문제점:
1. 강한 결합
   - 부모 변경 → 모든 자식 영향

2. 불필요한 메서드 상속
   - "is-a" 관계가 아닌 경우 문제

3. 다중 상속 불가
   - 여러 역할을 동시에 수행하기 어려움

대안: 컴포지션 (Composition)
- "has-a" 관계
- 유연한 조합
- 느슨한 결합

규칙:
- 상속은 타입 계층이 명확할 때만
- 행동을 공유하려면 컴포지션 고려
- "is-a"인지 항상 확인

Q4. 기능 분해는 완전히 나쁜가요?

A:

기능 분해가 유용한 경우:
✅ 작은 유틸리티 함수
✅ 이미 해결된 알고리즘 문서화
✅ 한 번 작성하고 변경 없는 스크립트

예시:
def calculateTax(amount, rate)
  return amount * rate
end

def formatCurrency(amount)
  return "$#{amount.round(2)}"
end

→ 간단하고 재사용 가능
→ 변경될 가능성 낮음
→ 함수로 충분!

기능 분해가 문제가 되는 경우:
❌ 대규모 시스템
❌ 빈번한 요구사항 변경
❌ 복잡한 도메인 로직

→ 이 경우 객체지향 필요

🚀 실전 적용 가이드

Step-by-Step 리팩터링

현재 코드가 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. 협력을 설계하라

📖 Chapter 정리

급여 시스템 완전 진화도

[Step 01] 순수 기능 분해
    ↓ 문제: 단일 기능만 지원
[Step 02] Operation 패턴
    ↓ 문제: main() 계속 수정
[Step 03] 타입 추가 (병렬 배열)
    ↓ 문제: 데이터 동기화, 타입 체크 범람
[Step 04] 모듈 (정보 은닉)
    ↓ 문제: 인스턴스 개념 부족
[Step 05] 추상 데이터 타입
    ↓ 문제: if문으로 타입 구분
[Step 06] 클래스 (다형성)
    ✅ 문제 해결!

다음 Chapter 예고

Chapter 08: 의존성 관리하기

- 의존성의 종류와 문제
- 의존성 방향과 안정성
- 의존성 역전 원리 (DIP)
- 유연한 설계 기법

⚠️ 하향식 기능 분해의 5가지 치명적 문제점

문제 1: 하나의 메인 함수라는 환상

"실제 시스템에 정상(top)이란 존재하지 않는다" - Parnas

이론:
┌─────────────────┐
│   main()        │  ← 모든 것의 시작점
│                 │
│  시스템 전체       │
└─────────────────┘

현실:
┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐
│main1 │  │main2 │  │main3 │  │main4 │
└──────┘  └──────┘  └──────┘  └──────┘
    ↓         ↓         ↓         ↓
   급여      보고서       통계       백업
   계산       생성        분석       처리

→ 여러 개의 진입점!
→ 메인 함수는 하나가 아니다!

실제 사례

초기 릴리즈:

# Version 1.0
def main(name)
  calculateAndPrintPay(name)
end

6개월 후:

# 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
end

1년 후:

# Version 2.0
# main() 함수가 너무 커져서 분리

def main_payroll(args)
  # 급여 관련
end

def main_hr(args)
  # 인사 관련
end

def main_admin(args)
  # 관리 관련
end

# → "하나의 메인 함수" 개념 붕괴!

왜 이런 일이 생기는가?

하향식 설계의 가정:
"시스템이 수행해야 할 하나의 주요 기능이 있다"

현실:
- 사용자는 계속 새로운 기능 요청
- 비즈니스는 계속 진화
- 시스템은 다양한 용도로 사용됨

결과:
→ 최초의 "메인 기능"은 점점 의미 없어짐
→ 시스템 = 동등한 수준의 여러 기능들
→ 계층 구조 = 환상

문제 2: 메인 함수의 빈번한 수정

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 기능 동작 안 함
# 원인 파악: 어려움 (에러 없이 조용히 실패)

문제 3: 비즈니스 로직과 UI의 강결합

코드 분석

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

문제 4: 성급한 실행 순서 결정

"어떻게(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

# → 같은 메서드, 다양한 문맥!

문제 5: 데이터 변경의 파급효과

"전역 변수 = 변경의 공포"

데이터 의존성 추적의 어려움

# 전역 데이터
$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별 문제점 요약

문제점 매트릭스

문제 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 → 객체지향 필요!