# 07장 객체 분해
조영호 저 "오브젝트: 코드로 이해하는 객체지향 설계, 2019"를 읽고 정리한 내용입니다.
사람과 컴퓨터의 저장소는 다르다. 사람은 장기기억을 가질 수 있지만 컴퓨터는 단기기억만을 가지고 문제를 처리한다. 필요한 요소의 수가 단기기억의 용량을 초과하는 순간 문제해결능력은 급격히 떨어지게 된다. 이것을 인지과부하라고 하며, 해결을 위해 분해라는 작업 필요하다. 분해는 추상화 과정을 통해 이뤄질 수 있다. 한 번에 단기 기억에 담을 수 있는 추상화의 수에는 한계가 있지만 추상화를 더 큰 규모의 추상화로 압축시킴으로써 단기 기억의 한계를 초월할 수 있다.
1 프로시저 추상화와 데이터 추상화
프로그래밍 패러다임은 추상화의 종류와 이 추상화를 이용해 소프트웨어를 분해하는 방법, 이 2가지 요소로 결정된다. 따라서 모든 프로그래밍 패러다임은 추상화와 분해의 관점에서 설명할 수 있다.
프로시저 추상화 - 소프트웨어가 무엇을 해야 하는지를 추상화
기능 분해 사용 (=알고리즘 분해)
데이터 추상화 - 소프트웨어가 무엇을 알아야 하는지를 추상화
데이터를 중심으로 타입을 추상화 (=추상데이터타입)
데이터를 중심으로 프로시저를 추상화 (=객체지향)
프로그래밍 언어의 관점에서 객체지향이란? 데이터를 중심으로 데이터 추상화와 프로시저 추상화를 통합한 객체를 이용해 시스템을 분해하는 방법이다.
일반적으로 객체지향이 전통적인 기능 분해 방법에 비해 효과적이라고 말하는 이유가 무엇일까? 해답을 알기 위해 좌절과 극복의 역사를 살펴봐야 한다.
2 프로시저 추상화와 기능 분해
메인 함수로서의 시스템
기능과 데이터의 첫 번째 전쟁에서 신은 기능의 손을 들어 주었다. 기능 분해의 관점에서 추상화의 단위는 프로시저이며 시스템은 프로시저를 단위로 분해된다.
프로시저는 반복적으로 실행되거나 거의 유사하게 실행되는 작업들을 하나의 장소에 모아놓음으로써 로직을 재사용하고 중복을 방지할 수 있는 추상화 방법이다. 그리고 프로시저 중심의 기능 분해 관점에서 시스템은 입력 값을 계산해서 출력 값을 반환하는 수학의 함수와 동일하다.
전통적인 기능 분해 방법은 하향식 접근법(Top-Down Approach)을 따른다. 최상위(topmost) 기능을 정의하고, 더 작은 단계로 분해해 나가는 방법을 말한다. 자신의 바로 상위 기능보다 덜 추상적이어야 하며, 상위 기능은 더 간단하고 더 구체적이며 덜 추상적인 하위 기능의 집합으로 분해된다.
전통적 방식(하향식)을 따르는 기능 분해 방법을 이용해 급여 관리 시스템을 만들어보자.
급여 관리 시스템
세부적인 절차로 구체화한다면, (3개 목차로 구성)
직원의 급여를 계산한다 // 시스템을 시작하는 메인 프로시저
사용자로부터 소득세율을 입력 받는다
직원의 급여를 계산한다
양식에 맞게 결과를 출력한다
기능 분해의 초점은 하나의 문장으로 표현된 기능을 여러 개의 더 작은 기능으로 분해하는 것이다. 여기선 "직원의 급여를 계산한다"를 더 작은 기능으로 분해.
모든 절차를 더 세분화 한다면,
직원의 급여를 계산한다
사용자로부터 소득세율을 입력 받는다
"세율을 입력하세요: "라는 문장을 화면에 출력한다
키보드를 통해 세율을 입력받는다
직원의 급여를 계산한다
전역 변수에 저장된 직원의 기본급 정보를 얻는다
급여를 계산한다
양식에 맞게 결과를 출력한다
"이름: {직원명}, 급여: {계산된 금액}" 형식에 따라 출력 문자열을 생성한다
기능 분해 방법에서는 기능을 중심으로 필요한 데이터를 결정한다. 기능 분해라는 무대의 주연은 기능 이며 데이터는 기능을 보조하는 조연의 역할에 머무른다. 이런 방법은 유지보수시 다양한 문제를 야기한다.
코드를 보며 전통적 하향식 기능 분해가 어떻게 동작하는지 그리고 문제점이 무엇인지를 확인해보자
급여 관리 시스템 구현
https://repl.it/languages/ruby
객체지향 패러다임 뿐아니라 하향식 기능분해를 표현할 정도로, 표현력이 우수한 RUBY를 사용했다.
트리로 표현

단계적으로 정제해 가는 과정은 구조적이며 체계적인 동시에 이상적인 방법으로까지 보 일 것이다. 문제는 우리가 사는 세계는 그렇게 체계적이지도 이상적이지도 않다.
하향식 기능 분해의 문제점
하향식 기능 분해 방법은 겉으로는 이상적인 방법으로 보일 수 있지만 실제로 설계에 적용하다 보면 아래와 같은 문제에 직면한다.
시스템은 하나의 메인 함수로 구성돼 있지 않다.
기능 추가나 요구사항 변경으로 인해 메인 함수를 빈번하게 수정해야 한다.
비즈니스 로직이 사용자 인터페이스와 강하게 결합된다.
하향식 분해는 너무 이른 시기에 함수들의 실행 순서를 고정시키기 때문에 유연성과 재사용성이 저하된다.
데이터 형식이 변경될 경우 파급효과를 예측할 수 없다.
하향식 접근법의 문제점에서 시작해서 기능 분해가 가지는 문제점을 순서대로 살펴보자.
하나의 메인 함수라는 비현실적인 아이디어
속적으로 새로운 기능을 추가하게 된다면, 시스템이 오직 하나의 메인 함수만으로 구현된다는 개념과는 완전히 모순된다. 처음에는 중요하게 생각됐던 메인 함수는 동등하게 중요한 여러 함수들 중 하나로 전락하고 만다. 따라서, 하향식 접근법은 하나의 알고리즘을 구현하거나 배치 처리를 구현하기에는 적합하지만 현대적인 상호작용 시스템을 개발하는데는 적합하지 않다. (실제 시스템에 정상 top 이란 존재하지 않는다)
메인 함수의 빈번한 재설계
새로운 기능을 추가할 때마다 매번 메인 함수를 수정해야 한다. 예, 기본급의 총합을 구하는 기능 요구사항 추가.
기존의 메인 함수는 직원 각각의 급여를 계산하는 것이 목적이므로 전체 직원들의 기본급 총액을 계산하는 sumOfBasePays 함수가 들어설 자리가 마땅치 않다. if 문으로 인자로 받아 처리를 해야한다.
결과적으로 기존 코드의 빈번한 수정으로 인한 버그 발생 확률이 높아진다.
비즈니스 로직과 사용자 인터페이스의 결합
급여를 계산하는 기능의 경우 "사용자로부터 소득세율을 입력받아 급여를 계산한 후 계산된 결과를 화면에 출력한다"라는 말에는 급여를 계산하는 중요한 비즈니스 로직과 관련된 관심사와 소득세율을 입력받아 결과를 화면에 출력한다는 사용자 인터페이스의 관심사가 한데 섞여 있다는 것을 의미한다. 사용자 인터페이스 로직과 비즈니스 로직을 한데 섞기 때문에 사용자 인터페이스를 변경하는 경우 비즈니스 로직까지 변경에 영향을 받게 된다.
이렇게 사용자 인터페이스의 관심사와 비즈니스로직의 관심사를 동시에 고려하도록 강요하기 때문에 "관심사의 분리"라는 아키텍처 설계의 목적을 달성하기 어렵다.
성급하게 결정된 실행 순서
하향식으로 기능을 분해하는 과정은 하나의 함수를 더 작은 함수로 분해하고, 분해된 함수들의 실행 순서를 결정하는 작업으로 요약할 수 있다. 이것은 설계를 시작하는 시점부터 시스템이 무엇(what)을 해야 하는지가 아니라 어떻게(how) 동작해야 하는지에 집중하도록 만든다.
즉, 하향식 접근법의 설계는 처음부터 구현을 염두에 두기 때문에 자연스럽게 함수들의 실행 순서를 정의하는 시간 제약(temporal constraint)을 강조한다. 이로써 급여 계산에 필요한 함수들의 실행 순서를 미리 결정하지 않는 한 기능 분해를 진행할 수 없게 된다.
직원의 급여를 계산하려면 어떤 작업이 필요한가? -> how
함수들이 main이라는 상위 문맥에 종속되어 있기에 재사용성도 떨어뜨린다.
함수들의 실행 순서를 먼저 정의할 경우, 이후 결정된 함수들의 제어 구조가 올바르지 않다는 것이 판명된다. 이를 해결하기 위해, 시간적인 제약에 대한 미련을 버리고 좀 더 안정적인 논리적 제약(logical constraint)을 설계의 기준으로 삼아야 한다. 객체지향은 함수 간의 호출 순서가 아니라 객체 사이의 논리적인 관계를 중심으로 설계를 이끌어 나간다.
데이터 변경으로 인한 파급효과
하향식 기능 분해의 가장 큰 문제점은 어떤 데이터를 어떤 함수가 사용하고 있는지를 추적하기 어렵다는 것이다. 따라서 데이터 변경으로 인해 어떤 함수가 영향을 받을지 예상하기 어렵다.
예, 새로운 직원 타입 (아르바이트) 추가시 발생하는 문제. 다양한 전역변수 추가시 일일이 함수들을 확인해야 한다. 스파게티.
전역 변수에 의존하는 함수를 찾는 것은 심하게 얽힌 실타래를 푸는 것처럼 인내와 끈기를 요구하는 작업이다.
데이터 변경으로 인한 영향을 최소화하려면, 데이터와 함께 변경되는 부분을 하나의 구현 단위로 묶고 외부에서는 제공되는 함수만을 이용해 데이터에 접근해야된다.
언제 하향식 분해가 유용한가?
설계가 어느 정도 안정화된 후에는 설계의 다양한 측면을 논리적으로 설명하고 문서화하기에 용이하다. 즉, 완전히 이해된 사실을 서술하기에 적합한 방법이다. 작은 프로그램과 개별 알고리즘을 위한 유용한 패러다임으로도 남아 있다.
3 모듈
모듈은 외부에 감춰야 하는 비밀과 관련성 높은 데이터와 함수의 집합이다.
정보은닉과 모듈
앞에서 설명한 것처럼 시스템의 변경을 관리하는 기본적인 전략은 함께 변경되는 부분을 하나의 구현 단위로 묶고 퍼블릭 인터페이스를 통해서만 접근하도록 만드는 것이다. 즉, 기능을 기반으로 시스템을 분해하는 것이 아니라 변경의 방향에 맞춰 시스템을 분해하는 것이다.
정보 은닉은 외부에 감춰야 하는 비밀에 따라 시스템을 분할하는 모듈 분할 원리다. 모듈은 변경될 가능성이 있는 비밀을 내부로 감추고, 잘 정의되고 쉽게 변경되지 않을 퍼블릭 인터페이스를 외부에 제공해서 내부의 비밀에 함부로 접근하지 못하게 한다.
모듈은 2가지 비밀 (복잡성, 변경가능성) 을 감춰야 하고, 모듈 내부를 구현하기 위해 기능 분해를 적용할 수 있다.
시스템을 모듈 단위로 어떻게 분해할 것인가? 비밀을 찾아라. 시스템에서 비밀은 데이터이다.
변경가능성을 가졌던 전역변수들을 (비밀적으로) 모듈에 넣어보자.
자바에서 모듈의 개념은 패키지를 이용해 구현 가능하다.
모듈의 장점과 한계
장점
모듈 내부의 변수가 변경되더라도 모듈 내부에만 영향을 미친다.
비즈니스 로직과 사용자 인터페이스에 대한 관심사를 분리한다.
Employees 모듈이 비즈니스로직과 관련된 관심사만을 담당하며, 사용자 인터페이스 관련 main함수 쪽이 담당한다.
전역 변수와 전역 함수를 제거함으로써 네임스페이스 오염 방지한다.
개인적 질문 - 이 코드는 충분히 객체지향적일까? 아니라면 문제는? 의견을 듣고 싶어용.
눈여겨봐야 할 부분은 모듈이 정보 은닉이라는 개념을 통해 데이터라는 존재를 설계의 중심 요소로 부각시켰다는 것이다. 즉, 감춰야 할 데이터를 결정하고 이 데이터를 조작하는 데 필요한 함수를 결정하는 데이터 중심으로 분해 방법을 사용하였다. (아마 위 답변?)
단점
인스턴스의 개념을 제공하지 않는다. (아마 위 답변?)
Employees는 단지 모든 직원 정보를 가지고 있는 모듈일 뿐이고, 높은 추상화를 위해 독립적인 단위로 다룰 수 있어야 한다. 이를 만족시키기 위해 추상 데이터 타입을 사용해보자.
4 데이터 추상화와 추상 데이터 타입
추상 데이터 타입 (Object-Based Programming)
옛날 프로그래밍 언어는 정수같은 내장타입(built-in)만 제공했고, 새로운 타입을 추가하는 것은 제한적이었다. 이러한 부족이 추상 데이터 타입의 개념이 나오게 만들었다. 추상 데이터 객체를 사용할때 프로그래머는 오직 객체가 외부에 제공하는 행위에만 관심을 가지며 행위가 구현되는 세부적인 사항에 대해서는 무시한다.
앞의 예제를 추상 데이터 타입을 이용한다면, 사람들은 '직원의 급여를 계산한다'라는 하나의 커다란 절차를 이용해 사고하기보다는 '직원'과 '급여'라는 추상적인 개념들을 머릿속에 떠올린 후 이들을 이용해 '계산'에 필요한 절차를 생각하는데 익숙하다.
추상 데이터 타입이 가능한 언어는 4가지를 만족해야 한다.
타입 정의를 선언할 수 있어야 한다.
class Member타입의 인스턴스를 다루기 위해 사용할 수 있는 오퍼레이션의 집합을 정의할 수 있어야한다.
아마메서드들?제공된 오퍼레이션을 통해서만 조작할 수 있도록 데이터를 외부로부터 보호할 수 있어야 한다.
private타입에 대해 여러 개의 인스턴스를 생성할 수 있어야 한다.
Member member1 = new Member("유철");
위와 같은 언어적 매카니즘을 오퍼레이션 클러스터라고 부른다.
루비는 추상 데이터 타입을 흉내 낼 수 있는 Struct라는 구성 요소를 제공한다. 구현하는 방법은 언어마다 다르다.
추상 데이터 타입은 사람들이 세상을 바라보는 방식에 좀 더 근접해지도록 추상화 수준을 향상시킨다. 일상 생활에서 Employee라고 말할 때는 상태와 행위를 가지는 독립적인 객체라는 의미가 담겨 있다. 따라서, 전 모듈방식에 비해 사람들의 사고방식에 가깝다.
그럼 이럼으로써 객체지향적으로 완벽하다고 할 수 있을까? 여전히 데이터와 기능을 분리해서 바라본다는 점에 주의해야 한다. 표현된 데이터를 이용해 기능을 구현하는 핵심로직은 데이터 타입 외부에 존재한다.
질문 - 위 코드를 객체 지향적을 바꾸기 위해서는 어떻게 만들어야 될까? 루비.. 잘모르지만...
5 클래스
클래스는 추상데이터 타입인가?
클래스와 추상데이터타입 두 메커니즘 모두 외부에서는 객체의 내부 속성에 직접 접근할 수 없으며 오직 퍼블릭 인터페이스를 통해서만 외부와 의사소통할 수 있기 때문에 틀린 것만은 아니지만, 명확하게 따지면 동일하지 않다. 가장 핵심적인 차이는 클래스는 상속과 다형성을 지원하는데 비해 추상 데이터 타입은 지원하지 못한다는 점이다.
좀 더 설명을 위해 윌리엄 쿡 책 OOP Versus Abstract Data Types 따르면, 추상 데이터 타입은 타입을 추상화한 것(type abstraction)이고 클래스는 절차를 추상화한 것(procedural abstraction) 이다.


두 개의 타입이 존재한다는 사실을 명시적으로 표현한다
이해하기 위해 비교해보자. 첫번재는 전 코드와 (추상데이터타입) 동일.
추상 데이터 타입이 오퍼레이션을 기준으로 타입을 묶는 방법이라면 객체지향은 타입을 기준으로 오퍼레이션을 묶는다. 즉, 정규 직원과 아르바이트 직원이라는 두 개의 타입을 명시적으로 정의하고 두 직원 유형과 관련된 오퍼레이션의 실행 절차를 두 타입에 분배한다.
변경을 기준으로 선택하라
단순히 클래스를 구현 단위로 사용한다는 것이 객체지향 프로그래밍을 한다는 것을 의미하지는 않는다. 비록 클래스를 사용하고 있더라도, 타입을 기준으로 절차를 추상화하지 않았다면 그것은 객체지향 분해가 아니다.
클래스가 추상 데이터 타입인지 확인하기 위해서, Employee의 hourly 같이 인 스턴스 변수에 저장된 값을 기반으로 메서드 내에서 타입을 명시적으로 구분하는 방식이 있는지 확인하면 된다. = 객체지향 위반
객체지향에서는 타입 변수를 이용한 조건문을 다형성으로 대체한다. (조건문 사용을 기피하는 이유 변경 때문이다)
HourlyEmployee 클래스, SalariedEmployee 클래스
가시적으로 확인해보자.

이러한 전환은, 객체지향의 특성이 개방-폐쇄 Open-Close 원칙을 지키게 해준다. 기존 코드에 아무런 영향도 미치지 않고 새로운 객체 유형과 행위를 추가할 수 있는 객체지향의 특성.
추상 데이터 타입은 모든 경우에 최악의 선택인가?
유용성은 변경의 방향성과 발생 빈도에 따라 결정된다. 쉽게 말해, 설계에 요구되는 변경의 압력이 타입 추가에 관한 것인 지, 아니면 오퍼레이션 추가에 관한 것인지에 따라 달라진다.
추상 데이터 타입의 접근법을 객체지향 설계에 구현한 것을 데이터 주도 설계라고 부른다. 그리고 책임 주도 설계는 데이터 주도 설계 방법을 개선하고자 하는 노력의 산물이었다.
티모시 버드(Timomy Budd)는 모듈과 추상 데이터 타입이 데이터 중심적인 관점(data centered view)을 취하는 데 비해 객체지향은 서비스 중심적인 관점(service centered view)을 취한다는 말로 둘 사이의 차이점을 깔끔하게 설명했다.
그런데...
마지막 객체지향이라 말했던 코드처럼, 단순하게 오퍼레이션과 타입을 표에 적어 놓고 클래스 계층에 오퍼레션의 구현 방법을 분배한다고 해서 객체지향적인 애플리케이션을 설계하는 것은 아니다. 객체지향에서 중요한 것은 역할, 책임, 협력이다.
Last updated