본문 바로가기

기술서적 정리/오브젝트 - 코드로 이해하는 객체지향 설계

07 CHAPTER 객체 분해

https://wikibook.co.kr/object/

 

오브젝트: 코드로 이해하는 객체지향 설계

역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라! 객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두 번째 걸음은 객체를

wikibook.co.kr

 

 

◈ 프로시저 추상화와 기능 분해

하향식 기능 분해 방식이 가지는 문제점을 이해하는 것은 유지보수 관점에서 객체지향의 장점을 이해할 수 있는 좋은 출발점이다.

하향식 기능 분해의 문제점

1. 시스템은 하나의 메인 함수로 구성돼 있지 않다.

2. 기능 추가나 요구사항 변경으로 인해 메인 함수를 빈번하게 수정해야 한다.

3. 비즈니스 로직이 사용자 인터페이스와 강하게 결합된다.

4. 하향식 분해는 너무 이른 시기에 함수들의 실행 순서를 고정시키기 때문에 유연성과 재사용성이 저하된다.

5. 데이터 형식이 변경될 경우 파급효과를 예측할 수 없다.

 

위 목록들을 통해 알 수 있는 것처럼 하향식 접근법과 기능 분해가 가지는 근본적인 문제점은 변경에 취약한 설계를 낳는다는 것이다.




모든 시스템은 사용자를 만족시키기 위해 새로운 요구사항을 도출해나가면서 지속적으로 새로운 기능을 추가하게 된다.

이것은 시스템이 오직 하나의 메인 함수만으로 구현된다는 개념과는 완전히 모순된다.

 

대부분 추가되는 기능은 메인 함수의 일부가 아닐 것이고 결국 처음에 중요하게 생각했던 메인 함수는 동등하게 중요한 여러 함수들 중 하나로 전락하고 만다.

 

대부분의 시스템에서 하나의 메인 기능이란 개념은 존재하지 않는다.

 

하향식 기능 분해의 경우 새로운 기능을 추가할 때마다 기존 로직과는 아무런 상관이 없는 새로운 함수의 적절한 위치를 확보해야 하기 때문에 메인 함수의 구조를 급격하게 변경할 수밖에 없다.

 

시스템은 여러 개의 정상으로 구성되기 때문에 새로운 정상을 추가할 때마다 하나의 정상이라고 간주했던 메인함수의 내부 구현을 수정할 수밖에 없다.

결과적으로 기존 코드의 빈번한 수정으로 인한 버그 발생 확률이 높아지기 때문에 시스템은 변경에 취약해질 수밖에 없다.



비즈니스 로직과 사용자 인터페이스의 결합

하향식 접근법은 비즈니스 로직과 사용자 인터페이스 로직이 밀접하게 결합된다.

문제는 비즈니스 로직과 사용자 인터페이스가 변경되는 빈도가 다르다는 것이다.
사용자 인터페이스는 시스템 내에서 가장 자주 변경되는 부분이다.

하향식 접근법은 사용자 인터페이스 로직과 비즈니스 로직을 한데 섞기 때문에 사용자 인터페이스를 변경하는 경우 비즈니스 로직까지 변경에 영향을 받게 된다.

따라서 하향식 접근법은 근본적으로 변경에 불안정한 아키텍처를 낳는다.

 

하향식 접근법에서는 중요한 비즈니스 로직과 사용자 인터페이스 로직이 메인 함수안에서 뒤섞여 있어 사용자 인터페이스를 변경하는 유일한 방법은 전체 구조를 재설계하는 것뿐이다.

 

하향식 접근법은 기능 분해하는 과정에서 사용자 인터페이스의 관심사와 비즈니스 로직의 관심사를 동시에 고려하도록 강요하기 때문에 “관심사의 분리”라는 아키텍처 설계의 목적을 달성하기 어렵다.



성급하게 결정된 실행 순서

하향식 접근법의 설계는 처음부터 구현을 염두에 두기 때문에 자연스럽게 함수들의 실행 순서를 정의하는 시간 제약을 강조한다.

함수들의 실행 순서를 미리 결정하지 않는 한 기능 분해를 진행할 수 없다.

 

문제는 실행 순서나 조건, 반복 같은 함수의 제어 구조가 빈번한 변경의 대상이라는 점이다.

 

객체지향은 함수 간의 호출 순서가 아니라 객체 사이의 논리적인 관계를 중심으로 설계를 이끌어 나간다.

결과적으로 전체적인 시스템은 어떤 한 구성요소로 제어가 집중되지 않고 여러 객체들 사이로 제어 주체가 분산된다.

 

함수가 재사용 가능하려면 상위 함수보다 더 일반적이어야 한다.

하지만 하향식 접근법을 따를 경우 분해된 하위 함수는 항상 상위 함수보다 문맥에 더 종속적이다.

 

하향식 설계와 관련된 모든 문제의 원인은 결합도다.

함수는 상위 함수가 강요하는 문맥에 강하게 결합된다.

함수는 함께 절차를 구성하는 다른 함수들과 시간적으로 강하게 결합돼 있다.

 

현재의 문맥에 강하게 결합된 시스템의 가장 큰 문제는 전체 시스템의 핵심적인 구조를 결정하는 함수들이 데이터와 강하게 결합된다는 것이다.



데이터 변경으로 인한 파급효과

하향식 기능 분해의 가장 큰 문제점은 어떤 데이터를 어떤 함수가 사용하고 있는지를 추적하기 어렵다는 것이다.

따라서 데이터 변경으로 인해 어떤 함수가 영향을 받을지 예상하기 어렵다.

어떤 데이터가 어떤 함수에 의존하고 있는지를 파악하는 것은 어려운이다.

모든 함수를 열어 데이터를 사용하고 있는지를 모두 확인해봐야 하기 때문이다.

 

데이터 변경으로 인한 영향은 데이터를 직접 참조하는 모든 함수로 퍼져나간다.

 

코드가 성장하고 라인 수가 증가할수록 전역 데이터를 변경하는 것은 악몽으로 변해간다.

 

데이터 변경으로 인한 영향을 최소화하려면 데이터와 함께 변경되는 부분과 그렇지 않은 부분을 명확하게 분리해야 한다.

이를 위해 데이터와 함께 변경되는 부분을 하나의 구현 단위로 묶고 외부에서는 제공되는 함수만을 이용해 데이터에 접근해야 한다.

즉, 잘 정의된 퍼블릭 인터페이스를 통해 데이터에 대한 접근을 통제해야 하는 것이다.

 

이것이 의존성 관리의 핵심이다.

변경에 대한 영향을 최소화하기 위해 영향을 받는 부분과 받지 않는 부분을 명확하게 분리하고 잘 정의된 퍼블릭 인터페이스를 통해 변경되는 부분에 대한 접근을 통제하라.

기능 분해가 가진 본질적인 문제를 해결하기 위해 이 같은 개념을 기반으로한 것이 정보은닉모듈이란 개념이다.



언제 하향식 분해가 유용한가?

설계가 어느 정도 안정화된 후에는 설계의 다양한 측면을 논리적으로 설명하고 문서화하기 용이하다.

 

하향식 분해는 작은 프로그램과 개별 알고리즘을 위해서는 유용한 패러다임으로 남아 있다.

그러나 실제로 동작하는 커다란 소프트웨어를 설계하는 데 적합한 방법은 아니다.

 

지금까지 확인한 하향식 설계가 가지는 문제점들.

1. 확장이 어렵다.

2. 사용자 인터페이스 같은 비본질적인 측면에 집중하게 만든다.

3. 데이터에 대한 영향도를 파악하기 어렵게 만든다.

4. 하향식 분해를 적용한 설계는 근본적으로 재사용하기 어렵다.




 모듈

정보 은닉과 모듈

모듈은 변경될 가능성이 있는 비밀을 내부로 감추고, 잘 정의되고 쉽게 변경되지 않을 퍼블릭 인터페이스를 외부에 제공해서 내부의 비밀에 함부로 접근하기 못하게 한다.

 

모듈과 기능 분해는 상호 배타적인 관계가 아니다.

시스템을 모듈로 분해한 후에는 각 모듈 내부를 구현하기 위해 기능 분해를 적용할 수 있다.

기능 분해하나의 기능을 구현하기 위해 필요한 기능들을 순차적으로 찾아가는 탐색의 과정이라면.

모듈 분해감춰야 하는 비밀을 선택하고 비밀 주변에 안정적인 보호막을 설치하는 보존의 과정이다.

비밀을 결정하고 모듈을 분해한 후에는 기능 분해를 이용해 모듈을 필요한 퍼블릭 인터페이스를 구현할 수 있다.

 

시스템의 가장 일반적인 비밀은 데이터다.



모듈의 장점과 한계

모듈의 장점.

 

1. 모듈 내부의 변수가 변경되더라도 모듈 내부에만 영향을 미친다.

데이터가 변경됐을 때 영향을 받는 함수를 찾기 위해 해당 데이터를 정의한 모듈만 검색하면 된다.

모듈은 데이터 변경으로 인한 파급효과를 제어할 수 있기 때문에 코드를 수정하고 디버깅하기 더 용이하다.

 

2. 비즈니스 로직과 사용자 인터페이스에 대한 관심사를 분리한다.

사용자 인터페이스를 추가하더라도 비즈니스 로직은 변경되지 않는다.

 

3. 전역 변수와 전역 함수를 제거함으로써 네임스페이스 오염을 방지한다.




모듈은 기능이 아니라 변경의 정도에 따라 시스템을 분해하게 한다.

각 모듈은 외부에 감춰야 하는 비밀과 관련성 높은 데이터와 함수의 집합이다.

 

모듈이 정보 은닉이라는 개념을 통해 데이터라는 존재를 설계의 중심 요소로 부각시켰다.

모듈에 있어서 핵심은 데이터다.

모듈은 감춰야 할 데이터를 결정하고 이 데이터를 조작하는 데 필요한 함수를 결정한다.

다시 말해서 기능이 아니라 데이터를 중심으로 시스템을 분해하는 것이다.

모듈은 데이터와 함수가 통합된 한 차원 높은 추상화를 제공하는 설계 단위다.




비록 모듈이 프로시저의 추상화보다는 높은 추상화 개념을 제공하지만 태생적으로 변경을 관리하기 위한 구현 기법이기 때문에 추상화 관점에서 한계점이 명확하다.

모듈의 가장 큰 단점은 인스턴스의 개념을 제공하지 않는다는 점이다.

다수의 인스턴스를 하나의 개념으로 묶을 추상화 메커니즘이 필요한데 이를 만족 시키기 위해 등장한 개념이 추상 데이터 타입이다.




 

◈ 데이터 추상화와 추상 데이터 타입

추상 데이터 타입은 프로시저 추상화 대신 데이터 추상화를 기반으로 소프트웨어를 개발하게 한 최초의 발걸음이다.

 

추상 데이터 타입은 사람들이 세상을 바라보는 방식에 좀 더 근접해지도록 추상화 수준을 향상시킨다.

일상 생활에서 Employee라고 말할 때는 상태와 행위를 가지는 독립적인 객체라는 의미가 담겨 있다.

따라서 개별 직원의 인스턴스를 생성할 수 있는 Employee 추상 데이터 타입전체 직원을 캡슐화하는 Employees 모듈보다는 좀 더 개념적으로 사람들의 사고방식에 가깝다.




 

◈ 클래스

클래스는 추상 데이터 타입인가?

명확한 의미에서 추상 데이터 타입과 클래스는 동일하지 않다.

가장 핵심적인 차이는 클래스는 상속과 다형성을 지원하는 데 비해 추상 데이터 타입은 지원하지 못한다는 점이다.

 

추상 데이터 타입이 오퍼레이션을 기준으로 타입을 묶는 방법이라면 객체지향은 타입을 기준으로 오퍼레이션을 묶는다.

 

동일한 메시지에 대해 서로 다르게 반응하는 것을 다형성이라고 한다.

 

클라이언트의 관점에서 다형성을 통해 구현한 두 클래스의 인스턴스는 동일하게 보인다는 것에 주목하라.

실제 내부에서 수행되는 절차는 다르지만 클래스를 이용한 다형성은 절차에 대한 차이점을 감춘다.

다시 말해 객체지향은 절차 추상화다.

 

추상 데이터 타입은 오퍼레이션을 기준으로 타입을 추상화한다.

클래스타입을 기준으로 절차들을 추상화한다.

이것이 추상화와 분해의 관점에서 추상 데이터 타입과 클래스의 다른 점이다.


변경을 기준으로 선택하라.

인스턴스 변수에 저장된 값을 기반으로 메서드 내에서 타입을 명시적으로 구분하는 방식은 객체지향을 위반하는 것으로 간주된다.

 

객체지향에서는 타입 변수를 이용한 조건문을 다형성으로 대체한다.

클라이언트가 객체의 타입을 확인한 후 적절한 메서드를 호출하는 것이 아니라 객체가 메시지를 처리할 적절한 메서드를 선택한다.

 

모든 설계 문제가 그런 것처럼 조건문을 사용하는 방식을 기피하는 이유 역시 변경 때문이다.

객체지향의 다형성을 적절하게 이용하면 시스템에 새로운 로직을 추가하기 위해 클라이언트 코드를 수정할 필요가 없다.

 

이처럼 기존 코드에 아무런 영향도 미치지 않고 새로운 객체 유형과 행위를 추가할 수 있는 객체지향의 특성을 개방-폐쇄 원칙이라고 부른다.

 

추상 데이터 타입과 객체지향 설계의 유용성은 설계에 요구되는 변경의 압력이 ‘타입 추가’에 관한 것인지, 아니면 ‘오퍼레이션 추가’에 관한 것인지에 따라 달라진다.

 

타입 추가라는 변경의 압력이 더 강한 경우에는 객체지향의 손을 들어줘야 한다.

새로운 타입을 추가하려면 객체지향의 경우에는 클라이언트 코드를 수정할 필요가 없다.

 

변경의 주된 압력이 오퍼레이션을 추가하는 것이라면 추상 데이터 타입의 승리를 선언해야 한다.

객체지향의 경우 새로운 오퍼레이션을 추가하기 위해서는 상속 계층에 속하는 모든 클래스를 한번에 수정해야 한다.

이와 달리 추상 데이터 타입의 경우에는 전체 타입에 대한 구현 코드가 하나의 구현체 내에 포함돼 있기 때문에 새로운 오퍼레이션을 추가하는 작업이 상대적으로 간단하다.

 

새로운 타입을 빈번하게 추가해야 한다면 객체지향의 클래스 구조가 더 유용하다.

새로운 오퍼레이션을 빈번하게 추가해야 한다면 추상 데이터 타입을 선택하는 것이 현명한 판단이다.

변경의 축을 찾아라.

객체지향적인 접근법이 모든 경우에 올바른 해결 방법인 것은 아니다.



협력이 중요하다.

책임을 다양한 방식으로 수행해야 할 때만 타입 계층 안에서 각 절차를 추상화하라.

타입 계층과 다형성는 협력이라는 문맥안에서 책임을 수행하는 방법에 관해 고민한 결과물이어야 하며 그 자체가 목적이 되어서는 안 된다.