사람의 기억은 단기 기억과 장기 기억으로 나뉘는데 장기 기억은 먼저 단기
기억의 영역으로 옮긴 후에 처리가 가능하다고 한다. 문제 해결에 필요한
요수의 수가 단기 기억의 용량을 초과화는 순간 문제 해결 능력이 급격히
떨어지는데, 이 현상을 인지 과부하(cognitive overload)라고 부른다.
인지 과부하를 방지하는 가장 좋은 방법은 단기 기억 내에 보관할 정보 양을
조절하는 것이다. 이를 위해 문제 해결에 필요한 핵심만을 남기는 작업을
추상화라고 부른다.
가장 일반적인 추상화 방법은 한 번에 다룰 문제의 크기를 줄이는 것이다.
이처럼 큰 문제를 해결 가능한 작은 문제로 나누는 방법을 분해(decomposition)
이라고 부른다.
이런 추상화와 분해가 가장 많이 사용되는 분야가 바로 소프트웨어 개발 영역이다.
현대 프로그래밍 언어를 특징 짓는 중요한 두 추상화 메커니즘은 프로시저 추상화와
데이터 추상화다. 프로시저 추상화는 소프트웨어가 무엇을 해야 하는지를
추상화하고 데이터 추상화는 소프트웨어가 무엇을 알아야 하는지를 추상화한다.
프로그래밍 패러다임이란 적절한 추상화의 윤곽을 따라 시스템을 어떤 식으로 나눌
것인지를 결정하는 원칙과 방법의 집합이다.
시스템을 분해하는 방법을 결정하려면 프로시저 추상화와 데이터 추상화 중 중심으로
할 것을 결정해야 한다. 프로시저 추상화를 중심으로 시스템을 분해한다면 기능 분해의
길로 들어서는 것이다.
데이터 추상화를 중심으로 시스템을 분해한다면 타입을 추상화하는 것과 데이터를 중심으로 프로시저를 추상화하는 것 중 선택해야 한다. 전자를 추상 데이터
타입이라 부르고, 후자를 객체지향이라고 부른다.
OOP에서 '역할과 책임을 수행하는 객체'가 바로 OOP가 이용하는 추상화다.
프로그래밍 언어의 관점에서 객체지향이란 데이터를 중심으로 데이터 추상화와 프로시저
추상화를 통합한 객체를 통해 시스템을 분해하는 방법이다. 그리고 이런 객체를 구현하기
위해 대부분의 OOP 언어는 클래스라는 도구를 제공한다.
기능을 기준으로 하여 시스템을 분해하는 방식을 알고리즘 분해 또는 기능
분해라고 부른다. 기능 분해의 관점에서 추상화의 단위는 프로시저이며
시스템은 프로시저를 단위로 분해된다.
프로시저는 내부의 상세한 구현 내용을 모르더라도 인터페이스만 알면 사용할 수
있기 때문에 추상화라고 부를 수 있다. 한편, 시스템은 필요한 더 작은 작업으로
분해될 수 있는 커다란 메인 함수다.
전통적인 기능 분해 방법은 하향식 접근법을 따른다. 시스템을 구성하는
최상위 기능을 정의하고, 이 최상위 기능을 좀 더 작은 단계의 하위 기능으로
분해해 나가는 방법을 말한다. 각 세분화 단계는 위 단계보다 더 구체적이어야
한다.
회사는 세율에 따라 직원 급여에서 일정 세금을 공제한다.
급여 = 기본급 - (기본급*소득세율)
급여 관리 시스템을 구현하기 위해 기능 분해 방법을 이용하겠다. 기능 분해의
초점은 하나의 문장으로 표현된 기능을 여러 개의 더 작은 기능으로 분해하는
것이다. 먼저, 시스템에 대한 최상위 문장을 기술하며 시작하자.
직원의 급여를 계산한다.
급여를 계산하는 데 필요한 정보는 직원의 이름과 소득세율이다. 문장을 좀 더
구체적인 절차로 기술하자.
- 직원의 급여를 계산한다.
- 사용자로부터 소득세율을 입력받는다.
- 직원의 급여를 계산한다.
- 양식에 맞게 결과를 출력한다.
각 정제 단계는 이전 문장의 추상화 수준을 감소시켜야 한다. 구현이 가능할 정도로
충분히 저수준의 문장이 될 때까지 기능을 분해해야 한다.
급여 계산을 위해 직원 기본급 정보도 필요하다. 해당 데이터는 시스템 내부에
보관하며 급여 계산 결과는 이름 : {직원명}, 급여: {계산된 금액}형식으로
스크린에 출력하기로 결정됐다.
- 직원의 급여를 계산한다.
- 사용자로부터 소득세율을 입력받는다.
- "세율을 입력하세요: "라는 문장을 화면에 출력한다.
- 키보드를 통해 세율을 입력받는다.
- 직원의 급여를 계산한다.
- 전역 변수에 저장된 직원의 기본급 정보를 얻는다.
- 급여를 계산한다.
- 양식에 맞게 결과를 출력한다.
- "이름 : {직원명}, 급여 : {계산된 금액}" 형식에 따라 출력 문자열을 생성한다.
급여 관리 시스템을 그림으로 나타내보면 다음과 같은 형태다.

기능 분해 방법에서는 기능을 중심으로 필요한 데이터를 결정한다.
이제 기능 분해 방식에 따라 시스템을 구현해 가며 전통적인 하향식 기능 분해
방법이 가지는 문제를 살펴보자
여기서는 OOP 언어이며 전역 변수와 전역 범위의 프로시저를 허용하는 루비를
사용해 예제 코드를 작성한다.
정의한 급여 관리 시스템의 최상위 문장을 코드로 옮기면 다음과 같다.
직원의 급여를 계산한다.
def main(name)
end
세분화된 내용을 통해 메인 함수의 내부를 채워보자.
- 직원의 급여를 계산한다.
- 사용자로부터 소득세율을 입력받는다.
- 직원의 급여를 계산한다.
- 양식에 맞게 결과를 출력한다.
def main(name)
taxRate = getTaxRate()
pay = calculatePayFor(name, taxRate)
puts(describeResult(name, pay))
end
getTaxRate 함수는 다음과 같은 두 개의 절차로 분해할 수 있다.
- 사용자로부터 소득세율을 입력받는다.
- "세율을 입력하세요: "라는 문장을 화면에 출력한다.
- 키보드를 통해 세율을 입력받는다.
def getTaxRate()
print("세율을 입력하세요:")
return gets().chomp().to_f()
end
급여를 계산하는 코드는 기본급 정보를 이용해 두 개의 단계로 구현할 수 있다.
- 직원의 급여를 계산한다.
- 전역 변수에 저장된 직원의 기본급 정보를 얻는다.
- 급여를 계산한다.
직원의 목록은 $employees라는 전역 변수에, 직원별 기본급은 $basePays라는
전역 변수에 저장하기로 하였다.
$employees = ["직원A", "직원B", "직원C"]
$basePays = [400, 300, 250]
급여를 계산하는 calculatePayFor 함수는 다음과 같이 구성된다.
def calculatePayFor(name, taxRate)
index = $employees.index(name)
basePay = $basePays[index]
return basePay - (basePay * taxRate)
end
마지막으로 급여 내역을 양식에 맞게 포맷팅한 후 출력하면 모든 작업이 완료된다.
def describeResult(name, pay)
return "이름 : #{name}, 급여 : #{pay}"
end
현재까지 구현한 시스템은 메인 함수를 루트로 하는 트리로 표현할 수 있다.

이 분해 방법은 다음과 같은 문제에 직면한다.
대부분의 시스템에서 하나의 메인 기능이란 존재하지 않는다. 기능성의 측면에서
모든 기능들은 동등하게 독립적이고 완결된 하나의 기능을 표현한다.
새로운 기능을 추가할 때마다 매번 메인 함수를 수정해야 한다. 이는 버그를
발생시킬 가능성을 높인다.
모든 직원들의 기본급 총합을 구하는 기능을 추가하는 요구사항이 발생했다고
가정해보자.
def sumOfBasePays()
result = 0
for basePay in $basePays
result += basePay
end
puts(result)
end
문제는 기존의 메인 함수는 직원 각각의 급여를 계산하는 것이 목적이라 새로운
함수가 들어설 자리가 마땅치 않다는 것이다.
이 문제를 해결하는 방법은 메인 함수의 로직 전체를 calculatePay라는
함수로 추출한 후 메인 함수에서 적절하게 sumOfBasePays 함수와
calculatePay 함수를 호출하는 것이다.
def calculatePay(name)
taxRate = getTaxRate()
pay = calculatePayFor(name, taxRate)
puts(describeResult(name, pay))
end
메인 함수에서 operation 인자를 설정하고 이에 따라 적절한 방법으로 급여를
계산하도록 하자.
def main(operation, args={})
case(operation)
when :pay then calculatePay(args[:name])
when :basePays then sumOfBasePays()
end
end
결과적으로 기존 코드의 빈번한 수정으로 인해 버그 발생률이 높아지게 된다.
급여 계산 기능의 경우 급여를 계산하는 비즈니스 로직과 소득 세율을 입력받고
화면에 결과를 출력하는 사용자 인터페이스 관심사가 한데 섞여 있는 것을 확인할
수 있다. 결과적으로 두 로직이 밀접히 결합된다.
문제는 비즈니스 로직과 사용자 인터페이스의 변경 빈도가 다르다는 것이다.
사용자 인터페이스는 시스템 상에서 자주 변경되는 반면, 비즈니스 로직은 자주
변경되지 않는다. 하향식 접근법에 의해 두 로직이 섞이면 사용자 인터페이스의
변경이 비즈니스 로직에까지 영향을 미친다.
결과적으로 "관심사의 분리"라는 아키텍처 설계의 목적을 달성하기 어렵다.
하향식 접근법은 설계를 시작하는 시점부터 시스템이 무엇을 해야 하는지가 아니라
어떻게 동작해야 하는지를 집중하게 만든다.
처음부터 구현을 염두에 두기 때문에 자연스럽게 함수들의 실행 순서를 정의하는
시간 제약을 강조한다. 실행 순서, 조건, 반복과 같은 제어 구조를 미리 결정하지
않고는 분해를 진행할 수 없기 때문에 중앙집중 제어 스타일의 형태를 띨 수 밖에
없다.
문제는 중요한 설계 결정사항인 함수의 제어 구조가 빈번한 변경의 대상이라는
점이다. 결과적으로 기능의 추가, 변경은 매번 기존에 결정된 함수의 제어구조를
변경하도록 만든다.
이를 해결할 수 있는 방법은 객체지향 방식처럼 안정적인 논리적 제약을 설계의
기준으로 삼는 것이다. 제어가 한 구성요소로 집중되는 것이 아닌 여러 객체들
사이로 분산된다.
하향식 설계와 관련된 모든 문제의 원인은 결합도다. 함수는 상위 함수가
강요하는 문맥에 강하게 결합된다. 또한 다른 함수들과 시간적으로도 강하게
결합되어 있다. 따라서 변경에 상당히 취약해진다.
하향식 기능 분해의 가장 큰 문제점은 어떤 데이터를 어느 함수가 사용하고 있는지
추적하기 어렵다는 것이다. 따라서 데이터 변경으로 인해 어떤 함수가 영향을
받을지 예상하기 어렵다. 이것은 의존성과 결합도의 문제다.
예를 들어 급여 시스템에 아르바이트의 급여도 계산할 수 있게 해달라는 변경
요청이 들어왔다고 가정해보자. 기존 $employees와 $basePays에 아르바이트의
이름, 기본급을 같이 보관하고 $hourlys라는 전역 변수를 통해 정규직, 아르바이트를
구분하게 코드를 구성한다.
$employees = ["직원A", "직원B", "직원C", "아르바이트D", "아르바이트E", "아르바이트F"]
$basePays = [400, 300, 250, 1, 1, 1.5]
$hourlys = [false, false, false, true, true, true]
아르바이트의 급여를 계산하기 위해 한달간의 업무 누적 시간이 필요하다. 전역 변수
$timeCards에 보관하기로 한다. 정규직의 경우 0이다.
$timeCards = [0, 0, 0, 120, 120, 120]
함수를 어떻게 수정해야 할까? calculatePay함수에 조건 분기를 추가하여 해결할 수
있다. 더불어 아르바이트의 급여를 계산하는 calculateHourlyPayFor, hourly?
함수도 추가한다.
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 calculatePay(name)
taxRate = getTaxRate()
if (hourly?(name)) then
pay = calculateHourlyPayFor(name, taxRate)
else
pay = calculatePayFor(name, taxRate)
end
puts(describeResult(name, pay))
end
모든 코드의 수정이 완료됐을까? 안타깝게도 현재 $basePays에는 정규 직원의
기본급뿐 아니라 아르바이트의 기본급도 포함된다. 따라서 sumOfBasePays 로직을
수정해야 한다.
def sumOfBasePays()
result = 0
for name in $employees
if (not hourly?(name)) then
result += $basePays[$employees.index(name)]
end
end
puts(result)
end
이 예제가 말해주는 것은 데이터 변경으로 인해 발생하는 함수의 영향도를 파악하는
것이 생각보다 쉽지 않다는 것이다.
데이터 변경으로 인한 영향을 최소화하려면 데이터와 함께 변경되는 부분과 그렇지
않은 부분을 명확하게 분리해야 한다. 즉, 퍼블릭 인터페이스를 통해 데이터에 대한
접근을 통제해야 한다.
이것이 바로 의존성 관리의 핵심이다. 개발 분야의 선구자 중 한명인 데이비드
피나스는 기능 분해의 본질적 문제를 해결하기 위해 정보 은닉과 모듈
이라는 개념을 제시하였다.
하향식 분해는 이미 해결된 알고리즘을 문서화하고 서술하는 데 유용한 기법이다.
그러나 커다란 소프트웨어를 설계하는 데 적합한 방법은 아니다.
시스템의 변경을 관리하는 기본적인 전략은 함께 변경되는 부분을 하나의 구현 단위로
묶고 퍼블릭 인터페이스를 통해서만 접근하도록 만드는 것이다. 즉, 기능 기반이 아닌
변경의 방향에 맞춰 시스템을 분해하는 것이다.
데이비드 파나스는 1972년 발표한 저서에서 정보 은닉의 개념을 소개했다.
정보 은닉의 핵심은 시스템을 모듈 단위로 분해하기 위한 기본 원리로 자주
변경되는 부분을 상대적으로 덜 변경되는 안정적인 인터페이스 뒤로 감춰야
한다는 것이 핵심이다.
기능 분해가 하나의 기능을 구현하기 위해 필요한 기능들을 순차적으로 찾아가는
탐색의 과정이라면 모듈 분해는 감춰야 하는 비밀을 선택하고 비밀 주변에
안정적인 보호막을 설치하는 보존의 과정이다. 모듈은 다음과 같은 두 가지 비밀을
감춰야 한다.
시스템의 가장 일반적인 비밀은 데이터다. 루비에서는 module이라는 키워드를
통해 모듈의 개념을 할 수 있고 자바의 경우는 패키지를 이용한다.
전체 직원에 관한 처리를 Employees 모듈로 캡슐화하면 다음과 같다.
module 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]
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
end
지금까지 전역 변수였던 데이터들($employees 등)이 Employees라는 모듈 내부에
숨겨졌다. 이제 외부에서는 퍼블릭 인터페이스인 calculatePay등의 함수를 통해서만
내부 변수를 조작할 수 있다.
모듈 내부의 함수가 변경되더라도 모듈 내부에만 영향을 미친다
모듈을 사용하면 모듈 내부에 정의된 변수를 직접 참조하는 코드의 위치를
모듈 내부로 제한할 수 있다.
비즈니스 로직과 사용자 인터페이스에 관한 관심사를 분리한다
수정된 코드에서 Employees모듈은 비즈니스 로직만을 담당하며 사용자
인터페이스와 관련된 로직은 main에 위치한다.
전역 변수와 전역 함수를 제거함으로써 네임스페이스 오염을 방지한다
변수와 함수를 모듈 내부에 포함시키기 때문에 다른 모듈에서도 동일한 이름을
사용할 수 있게 된다. 이에 따라 네임스페이스 오염을 방지하고 이름 충돌 위험을
완화한다.
모듈을 사용함에 따라 모듈 내부는 높은 응집도를 유지하고, 모듈과 모듈
사이는 퍼블릭 인터페이스를 통해 소통하여 낮은 결합도를 유지한다.
모듈은 데이터를 중심으로 시스템을 분해하기 때문에 한 차원 높은 추상화를
제공하는 설계 단위로 볼 수 있다. 하지만 좀 더 높은 수준의 추상화를 위해
개별 직원을 독립적인 단위로 다룰 수 있어야 한다. 이를 만족시키기 위해
등장한 개념이 바로 추상 데이터 타입이다.
프로그래밍 언어에서 타입이란 변수에 저장할 수 있는 내용물의 종류와
변수에 적용될 수 있는 연산의 가짓수를 의미한다. 절차형 언어들은 적은
수의 내장 타입만을 제공했으며 새로운 타입을 추가하는 것이 불가했다.
바바라 리스코프는 이런 한계를 인지하고 프로시저 추상화를 보완하기 위한
데이터 추상화의 개념을 제안했다.
추상 데이터 타입은 추상 객체와 클래스를 정의한 것으로 추상 객체에
사용할 수 있는 오퍼레이션을 이용해 규정된다.
추상 데이터 타입을 구현하려면 다음과 같은 특성을 위한 프로그래밍 언어의
지원이 필요하다.
루비는 추상 데이터 타입을 흉내낼 수 있는 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
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),
]
calculatePay, sumOfBasePays 오퍼레이션은 다음과 같이 작성할 수 있다.
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
이러한 추상 데이터 타입은 모듈보다는 좀 더 사람들의 사고방식에 가깝게 코드를
구성할 수 있게 해준다. 하지만, 추상 데이터 타입으로 표현된 데이터를 이용해서
기능을 구현하는 핵심 로직은 추상 데이터 타입 외부에 존재한다. 여전히 데이터와
기능을 분리하는 절차적인 설계의 틀에 갇혀 있는 것이다.
대부분의 서적은 클래스를 추상 데이터 타입으로 설명한다. 하지만 두 개념은
동일하지 않다. 가장 핵심적인 차이는 클래스는 상속과 다형성을 지원하는데
비해 추상 데이터 타입은 지원하지 못한다는 점이다. 상속과 다형성을 지원하지
않는 추상 데이터 타입 기반의 프로그래밍 패러다임을 객체기반 프로그래밍
이라고 부르기도 한다.
윌리엄 쿡의 정의를 빌리자면 추상 데이터 타입은 타입을 추상화한 것이고 클래스는
절차를 추상화한 것이다.
두 개념간 차이점을 이해하기 위해 Employee를 살펴보면 사실 하나의 타입처럼
보이지만 정규 직원과 아르바이트 직원이라는 두 개의 타입이 공존한다는 것을
알 수 있다. 이처럼 하나의 대표적 타입이 세부적 타입을 감추는 것을 타입
추상화라고 부른다. 이는 오퍼레이션을 기준으로 타입을 통합하는 데이터 추상화
기법이다.
반면, 객체지향은 타입을 기준으로 오퍼레이션을 묶는다. 즉, 정규 직원과
아르바이트 직원이라는 두 타입을 명시적으로 정의하고 두 직원 유형과 관련된
오퍼레이션의 실행 절차를 두 타입에 분배한다.
이렇게 두 클래스로 분리할 경우 공통 로직은 부모 클래스에 두고 상속받게 하는
구조로 설계할 수 있다. 이제 클라이언트는 부모 클래스의 참조자에 대해 메시지만
전송하면 실제 클래스에 따라 적절한 절차가 실행된다. 즉, 동일 메시지에 대해
다르게 반응하는 다형성이 등장한다. 클래스를 이용한 다형성은 절차에
대한 차이점을 감춘다.
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)
return (basePay * timeCard) - (basePay * timeCard) * taxRate
end
def monthlyBasePay()
return 0
end
end
모든 직원 타입에 대해 Employee 인스턴스를 생성했던 추상 데이터 타입과 달리
클래스를 이용한 코드에서 클라이언트는 명시적인 직원 타입의 클래스 인스턴스를
생성할 수 있다.
$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
클래스가 추상 데이터 타입을 따르는 지 확인할 수 있는 가장 간단한 방법은
클래스 내부에 인스턴스 타입을 표현하는 변수가 있는지를 살펴보는 것이다.
추상 데이터 타입 Employee는 내부에 hourly라는 직원 유형을 저장하는
변수가 존재한다. 인스턴스 변수에 지정된 값을 기반으로 메서드 내에서
타입을 명시적으로 구분하는 방식은 객체지향을 위반하는 것으로 간주된다.
객체지향에서는 타입 변수를 이용한 조건문을 다형성으로 대체한다.
클라이언트가 객체의 타입을 확인한 후 적절한 메서드를 호출하는 것이 아닌
객체가 메시지를 처리할 적절한 메서드를 선택한다. 조건문을 기피하는 이유는
바로 변경때문이다. 객체지향은 새로운 직원 타입이 추가되면 그저 새로운
직원 유형을 구현하는 클래스를 상속 계층에 추가하면 된다.
이처럼 기존 코드에 아무 영향도 미치지 않고 새로운 객체 유형과 행위를
추가할 수 있는 객체지향의 특성을 OCP라고 부른다.
하지만 무조건적으로 객체지향이 옳은 것은 아니다. 새로운 타입을 빈번히
추가해야 된다면 객체지향의 클래스 구조가 더 유용하고, 새로운 오퍼레이션을
빈번히 추가해야 된다면 추상 데이터 타입이 더 좋다.
클래스 계층에 오퍼레이션 방법을 분배한다고 해서 객체지향적인 어플케이션을
설계하는 것은 아니다. 이는 추상 데이터 타입과 클래스의 차이를 보이기 위함이지
타입 계층과 다형성은 협력이라는 문맥 안에서 책임을 수행하는 방법에 초점을
두어야 한다.