[오브젝트] chap07을 읽고

인간은 문제를 해결하기 위해 장기 기억을 사용하지 않고 단기 기억을 사용한다. 하지만 단기 기억에 저장할 수 있는 정보는 5개에서 많아봐야 9개 정도를 넘지 못한다. 따라서 인간은 단기 기억의 한계를 가지고 있기 때문에 불필요한 정보를 제거하고 현재의 문제 해결에 필요한 핵심만 남긴다. 이를 추상화라고 한다.

또한 인간은 해결 하기 어려운 큰 문제는 단기 기억 안에서 처리할 수 있을 만한 작은 문제로 나누는 경향이 있다. 이를 분해라고 한다.

인간은 문제 해결에 추상화와 분해라는 기본적인 사고 도구를 활용해왔다. 이는 소프트웨어 개발 영역에도 적용된다. 추상화의 분해의 방식에 따라서 여러 프로그래밍 패러다임이 탄생했다.

프로시저 추상화와 데이터 추상화

프로그래밍 패러다임은 프로그래밍을 구성하기 위해 사용하는 추상화의 종류와 이 추상화를 이용해 소프트웨어를 분해하는 방법의 두 가지 요소로 결정된다. 따라서 모든 프로그래밍 패러다임은 추상화와 분해의 관점에서 설명할 수 있다.

현대적인 프로그래밍 언어를 특징 짓는 중요한 두 가지 추상화 메커니즘은 프로시저 추상화와 데이터 추상화다. 프로시저 추상화는 소프트웨어가 무엇을 해야하는지를 추상화 한다. 데이터 추상화는 소프트웨어가 무엇을 알아야 하는지를 추상화한다.

시스템을 분해하는 방법은 어떤 추상화를 중심으로 할것인지에 따라 나뉜다.

  • 프로시저 추상화 중심 시스템 분해 방법
    • 기능 분해 = 알고리즘 분해
  • 데이터 추상화 중심 시스템 분해 방법
    • 데이터 중심 타입 추상화 = 추상 데이터 타입
    • 데이터 중심 프로시저 추상화 = 객체지향

프로시저 추상화 중심 시스템 분해 방법에서는 기능 분해(알고리즘 분해)가 있다. 데이터 추상화 중심 시스템 분해 방법에는 데이터 중심 타입 추상화(추상 데이터 타입)데이터 중심 프로시저(객체지향)가 있다.

전통적인 기능 분해 방식보다 객체지향이 더욱 효과적이라고 말한다. 전통적인 기능 분해 방법에서 시작해서 객체지향 분해 방법에 이르는 역사를 살펴보며 효과적이라는 말의 의미를 찾아보자.

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

기능은 오랜 시간 동안 시스템을 분해하기 위한 기준으로 사용되었다. 이 같은 시스템 분해 방식을 알고리즘 분해 또는 기능 분해라고 부른다. 기능 분해의 관점에서 추상화의 단위는 프로시저이며 시스템은 프로시저를 단위로 분해된다.

프로시저는 반복적으로 싱행되는 작업들을 하나의 장소에 모아놓을으로써 로직을 재사용하고 중복을 방지할 수 있는 추상화 방법이다. 프로시저를 추상화라고 부르는 이유는 내부의 상세한 구현 내용을 모르더라도 인터페이스만 알면 프로시저를 사용할 수 있기 때문이다. 프로시저는 잠재적으로 정보은닉의 가능성을 제시하지만 효과적인 정보은닉 체계를 구축하는 데는 한계가 있다.

전통적인 기능 분해 방법은 시스템을 입력과 출력이 있는 수학의 함수와 동일하게 본다. 따라서 하향식 접근법을 따른다. 하나의 메인 함수인 최상위 기능을 정의하고 이 최상위 기능을 좀 더 작은 단계의 하위 기능으로 분해해 나가는 방법을 말한다. 각 세분화 단계는 바로 위 단계보다 구체적이어야한다. 즉, 덜 추상적이어야 한다.

기능을 구현 가능한 수준까지 분해했다면 기능 분해 방법은 그제서야 필요한 데이터를 결정한다. 데이터는 기능을 보조하는 조연의 역할에 머무른다. 이는 유지보수에 다양한 문제를 야기한다.

  • 시스템은 하나의 메인 함수로 구성돼 있지 않다.
  • 기능 추가나 요구사항 변경으로 인해 메인 함수를 빈번하게 수정해야 한다.
  • 비즈니스 로직이 사용자 인터페이스와 강하게 결합된다.
  • 하향식 분해는 너무 이른 시기에 함수들의 실행 순서를 고정시키기 때문에 유연성과 재사용성이 저하된다.
  • 데이터 형식이 변경될 경우 파급효과를 예측할 수 없다.

기능 분해의 문제점

하나의 메인 함수라는 비현실적인 아이디어

시스템은 최초에 릴리스됐전 당시의 모습을 그대로 유지하지는 않는다. 새로운 요구사항을 도출해나가면서 지속적으로 새로운 기능을 추가하게 된다. 대부분의 경우 추가되는 기능은 최초에 배포된 메인 함수의 일부가 아닐 것이다. 결국 처음에 중요하게 생각됐던 메인 함수는 동등하게 중요한 여러 함수들 중 하나로 전락하고 만다. 따라서 하향식 접근법은 하나의 알고리즘을 구현하거나 배치 처리를 구현기에는 적합하지만 현대적인 상호작용 시스템을 개발하는 데는 적합하지 않다.

메인 함수의 빈번한 재설계

하향식 기능 분해의 경우에는 새로운 기능을 추가할 때마다 매번 메인 함수를 수정해야 한다. 기존 로직과는 아무런 상관이 없는 새로운 함수의 적절한 위치를 확보해야 하기 떄문에 메인 함수의 구조를 급격하게 변경할 수밖에 없는 것이다. 결과적으로 기존 코드의 빈번한 수정으로 인한 버그 발생 확률이 높아지기 때문에 시스템은 변경에 취약해 질 수밖에 없다.

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

하향식 접근법은 비즈니스 로직을 설계하는 초기 단계부터 입력 방법과 출력 양식을 함께 고민하도록 강요한다. 사용자 인터페이스의 관심사가 함께 섞여 있다는 것을 의미한다. 결과적으로 코드 안에서 비즈니스 로직과 사용자 인터페이스 로직이 밀접하게 결합된다.

문제는 비즈니스 로직과 사용자 인터페이스의 변경 주기가 다르다는 것이다. 인터페이스는 빈번하게 변ㄷ경되고 비즈니스 로직은 그에 비해 변경이 적게 발생한다. 인터페이스와 비즈니스 로직의 밀접한 결합으로 인터페이스 변경 시 비즈니스 로직까지 변경에 영향을 받게 된다.

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

성급하게 결정된 실행 순서

하향식으로 기능을 분해하는 과정을 하나의 함수를 더 작은 함수로 분해하고, 분해된 함수들의 실행 순서를 결정하는 작업으로 요약할 수 있다. 이것은 설계를 시작하는 시점부터 시스템이 무엇(what)을 해야 하는지가 아니라 어떻게(how) 동작해야 하는지에 집중하게 만든다.

메인 함수가 작음 함수들로 분해되기 위해서는 우선 함수들의 순서를 결정해야 한다. 실행 순서나 조건, 반복과 같은 제어 구조를 미리 결정하지 않고는 분해를 진행할 수 없기 때문에 기능 분해 방식은 중장집중 제어스타일의 형태를 띌 수밖에 없다. 결과적으로 중요한 제어 흐름의 결정이 상위 함수에서 이뤄지고 하위 함수는 상위 함수의 흐름에 따라 적절한 시점에 호출된다.

문제는 중요한 설계 결정사항인 함수의 제어 구조가 빈번한 변경의 대상이라는 점이다. 기능이 추가되거나 변경될 때마다 초기에 결정된 함수들의 제어 구조가 올바르기 않다는 것이 판명된다. 결과적으로 기능을 추가하거나 변경하는 작업은 매번 기존에 결정된 함수의 제어구조를 변경하도록 만든다.

또한 하향식 접근법을 통해 분해한 함수들은 재사용하기도 어렵다. 모든 함수는 상위 함수를 분해하는 과정에서 필요에 따라 식별되며, 그에 따라 상의 함수가 강요하는 문맥 안에서만 의미를 가지기 때문이다. 재사용이라는 개념은 일반성이라는 의미를 포함한다는 점을 기억하라. 함수가 재사용 가능 하려면 상위 함수도다 더 일반적이어야 한다. 하지만 하향식 접근법을 따를 경우 분해된 하위 함수는 항상 사우이 함수보다 문맥에 더 종속적이다. 이것은 정확하게 재사용성과 반대되는 개념이다.

하향식 설계와 관련된 모든 문제의 원인은 결합도다. 함수는 상위 함수가 강요하는 문맥에 강하게 결합된다. 함수는 함께 절차를 구성하는 다른 함수들과 강하게 결합되어 있다. 강한 결합도는 시스템을 변경에 취약하게 만들고 이해하기 어렵게 만든다. 강하게 결합된 시스템은 아주 사송한 변경만으로도 전체 시스템을 크게 요동치게 만들 수 있다. 현재의 문맥에 강하게 결합된 시스템은 현재 문맥을 떠나 다른 문맥으로 옮겨갔을 때 재사용하기 어렵다.

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

하향식 기능 분해의 가장 큰 문제점은 어떤 데이터르 어떤 함수가 사용하고 있는지를 추적하기 어렵다는 것이다. 따라서 데이터 변경으로 인해 어떤 함수가 영향을 받을지 예상하기 어렵다. 개벼 함수의 입장에서 사용하는 데이터를 파악하는 것은 어렵지 않지만, 어떤 데이터가 어떤 함수에 의존하고 있는지를 파악하는 것은 어려운 일이다. 모든 함수를 열어 데이터를 사용하고 있는지를 모두 확인해봐야 하기 때문이다.

이것은 코드 안의 텍스트를 검색하는 단순한 문제가 아니라 의존성과 결합도의 문제다. 그리고 테스트의 문제이기도 하다. 데이터 변경으로 인한 영향은 데이터를 직접 참조하는 모든 함수로 퍼져나간다.

기능 분해의 문제를 해결하기 위한 방법

위와 같은 문제를 해결하기 위해 등장한 개념은 모듈이었다. 모듈이 무엇인지 알아보고 장점과 한계를 알아보자.

정보 은닉과 모듈

시스템의 변경을 관리하는 기본적인 전략은 함께 변경되는 부분을 하나의 구현 단위로 묶고 퍼블릭 인터페이스를 통해서만 접근하도록 만드는 것이다. 즉, 기능을 기반으로 시스템을 분해하는 것이 아니라 변경의 방향에 맞춰 시스템을 분해하는 것이다.

정보 은닉은 시스템을 모듈 단위로 분해하기 위한 기본 원리로 시스템에서 자주 변경되는 부분을 상대적으로 덜 변경되는 안정적인 인터페이스 뒤로 감춰야 하다는 것이 핵심이다. 모듈 분할은 변경될 가능성이 있는 비밀을 내부로 감추고, 잘 정의되고 쉽게 변경되지 않을 퍼블릭 인터페이스를 외부에 제공해서 내부의 비밀에 함부로 접근하지 못하게 한다.

모듈과 기능 분해는 상호 배타적인 관계가 아니다. 시스템을 모듈로 분해한 후에는 각 모듈 내뷰를 구현하기 위해 기능 분해를 적용할 수 있다.

모듈은 다음과 같은 두 가지 비밀을 감춰야 한다.

  • 복잡성 : 모듈이 너무 복잡한 경우 이해하고 사용하긱 어렵다. 외부에 모듈을 추상화할 수 있는 간단한 인터페이스를 제공해서 모듈의 복잡도를 낮춘다.
  • 변경 가능성 : 변경 가능한 설계 결정이 외부에 노출될 경우 실제로 변경이 발생했을 때 파급효과가 커진다. 변경 발생 시 하나의 모듈만 수정하면 되도록 변경 가능한 설계 결정을 모듈 내부로 감추고 외부에는 쉽게 변경되지 않을 인터페이스를 제공한다.

시스템의 가장 일반적인 비밀은 데이터이다. 하지만 반드시 비밀이 데이터일 필요는 없으며 복잡한 로직이나 변경 가능성이 큰 자료 구조 일수도 있다. 그럼에도 변경 시 시스템을 굴복시키는 대부분의 경우는 데이터가 변경되는 경우다.

과거에 모듈로 시스템을 분할하는 방법은 개별 모듈을 별도의 물리적인 파일로 분리하는 것이었다. 자바에서 모듈의 개념은 패키지를 이용해 구현 가능하다.

모듈의 장점과 한계

모듈의 장점은 다음과 같다.

  • 모듈 내부의 변수가 변경되더라도 모듈 내부에만 명향을 미친다.
  • 비즈니스 로직과 사용자 인터페이스에 대한 관심사를 분리한다.
  • 전역 변수와 전역 함수를 제거함으로써 네임스페이스 오염을 방지한다.

모듈은 기능이 아니라 변경의 정도에 따라 시스템을 분해하게 한다. 각 모듈은 외부에 감춰야 하는 비밀과 관련성 높은 데이터와 함수의 집합이다. 따라서 모듈 내부는 높은 응집도를 유지한다. 모듈과 모듈 사이에는 퍼블릭 인터페이스를 통해서만 통신해야 한다. 따라서 낮은 결합도를 유지한다.

여기서 눈여겨봐야 할 부분은 모듈에 있어 핵심은 데이터라는 것이다. 메인 함수를 정의하고 필요에 따라 더 세부적인 함수로 분해하는 하향식 기능 분해와 달리 모듈은 감춰야 할 데이터를 결정하고 이 데이터를 조작하는 데 필요한 함수를 결정한다. 다시 말해서 기능이 아니라 데이터를 중심으로 시스템을 분해하는 것이다.

비록 모듈이 프로시저 추상화보다는 높은 추상화 개념을 제공하지만 태생적으로 변경을 관리하기 위한 기법이기 때문에 추상화 관점에서의 한계점이 명확하다. 모듈은 단지 모듈에 해당하는 전체 데이터만 가지고 있다. 좀 더 높은 추상화를 위해서는 데이터 전체가 아니라 개별 데이터를 독립적인 인스턴스로 다룰 수 있어야 한다. 이를 만족시키기 위해 등장한 개념이 바로 추상 데이터 타입이다.

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

추상 데이터 타입

프로그래밍 언어에서 타입이란 변수에 저장할 수 있는 내용물의 종류와 변수에 적용될 수 있는 연산의 가짓수를 의미한다. 타입은 저장된 값에 대해 수행될 수 있는 연산의 집합을 결정하기 떄문에 변수의 값이 어떻게 행동할 것이라는 것을 예측할 수 있게 한다.

사람들은 ‘직원의 급여를 계산한다’라는 하나의 커다란 절차를 이용해 사고하기보다는 ‘직원’과 ‘급여’라는 추상적인 개념들을 머리속에 떠올린 후 이들을 이용해 ‘계산’에 필요한 절차를 생각하는데 익숙하다. 사람들은 기존의 프로시저 추상화로는 프로그램의 표현력을 향상시키는 데 한계가 있다는 사실을 발견했다. 이후 추상 데이터 타입을 지원하는 언어가 등장했다.

프로그래밍 언어가 사람의 사고방식과 가깝게 진화했다. 추상 데이터 타입의 정의를 기반으로 객체를 생성하는 것이 가능해졌다. 하지만 여전히 데이터와 기능을 분리해서 바라본다. 추상 데이터 타입은 말 그대로 시스템의 상태를 저장할 데이터를 표현한다.

추상 데이터 타입으로 표현된 데이터를 이용해 기능을 구현하는 핵심 로직은 추상 데이터 타입 외부에 존재한다. 예를 들어 main함수의 로직들이 데이터를 사용하는 형태를 말한다. 추상 데이터 타입은 데이터에 대한 관점을 설계의 표면으로 끌어올리기는 하지만 여전히 데이터와 기능을 분리하는 절차적인 설계의 틀에 갇혀 있는 것이다.

클래스

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

추상 데이터 타입과 클래스는 동일하지 않다. 가장 핵심적인 차이는 클래스는 상속과 다형성을 지원하는 데 비해 추상 데이터 타입은 지원하지 못한다는 점이다. 상속과 다형성을 지원하는 객체지향 프로그래밍과 구분하기 위해 상속과 다형성을 지원하지 않는 추상 데이터 타입 기반의 프로그래밍 패러다임을 객체기반 프로그래밍이라고 부르기도 한다.

추상 데이터 타입은 타입을 추상화 한것이다. 예를 들면, 추상 데이터 타입의 Employee는 정규 직원과 아르바이트 직원이라는 두 개의 개별적인 개념을 포괄하는 복합 개념이다. 하나의 타입처럼 보이는 Employee는 다수의 타입을 감추고 있다. 이를 타입 추상화라고 한다.

타입 추상화는 개별 오퍼레이션이 모든 개념적인 타입에 대한 구현을 포괄하도록 함으로써 하나의 물리적인 타입 안에 전체 타입을 감춘다. 따라서 타입 추상화는 오퍼레이션을 기준으로 타입을 통합하는 데이터 추상화 기법이다. 정규 직원과 아르바이트의 직원의 월급 계산이 달라도 같은 오퍼레이션을 사용하기 때문에 하나의 타입으로 묶고 내부에서 분기해 처리한다.

하지만 객체지향은 타입을 기준으로 오퍼레이션을 묶는다. 정규 직원과 아르바이트 직원이라는 두 개의 타입을 명시적으로 정의하고 두 직원 유형과 관련된 오퍼레이션의 실행 절차를 두 타입에 분배한다.

정규 직원과 아르바이트 직원의 부모 클래스의 참조자에 대해 메시지를 전송하면 실제 클래스가 무엇인가에 따라 적절한 절차가 싱행된다. 즉, 동일한 메시지에 대해 서로 다르게 반응한다. 이것이 바로 다형성이다.

클라이언트의 입장에서 두 클래스의 인트턴스는 구분할 수 없다. 실제로 내부에서 수행되는 절차는 다르지만 클래스를 이용한 다형성을 절차에 대한 차이점을 감춘다. 다시 말해 객체지향은 절차 추상화다.

변경을 기준으로 선택하라

단순히 클래스를 구현 단위로 사용하는 것이 객체지향 프로그래밍을 한다는 것을 의미하지는 않는다. 타입을 기준으로 절차를 추상화하지 않았다면 그것은 객체지향 분해가 아니다.

클래스가 추상 데이터 타입을 따르는지를 확인할 수 있는 가장 간단한 방법은 클래스 내부에 인스턴스의 타입을 표현하는 변수가 있는지를 살펴보는 것이다. 인스턴스 변수에 저장된 값을 기반으로 메서드 내에서 타입을 명시적으로 구분하는 방식은 객체지향을 위반하는 것으로 간주된다.

객체지향에서는 타입 변수를 이용한 조건문을 다형성으로 대체한다. 모든 설계 문제가 그런 것 처럼 조건문을 사용하는 방식을 기피하는 이유 역시 변경 때문이다. 타입 변수를 이용한 조건문 대신 유형을 구현하는 클래스를 상속 계층에 추가하고 필요한 메서드를 오버라이딩한다.

새로 추가된 클래스의 메서드를 실행하기 위한 어떤 코드도 추가할 필요가 없다. 즉, 새로운 로직을 추가하기 위해 클라이언트 코드를 수정할 필요가 없다는 것을 의미한다. 이런 객체지향의 특성을 개방-폐쇄 원칙이라고 부른다.

그렇다면 항상 절차를 추상화하는 객체지향 설계 방식을 따라야 하는가? 추상 데이터 타입은 모든 경우에 최악의 선택인가?

설계는 변경과 관련된 것이다. 즉, 요구되는 변경의 압력에 따라 어떤 방식을 선택해야하는지 달라진다. 변경의 압력이 ‘타입 추가’에 관한 것이라면 객체 지향을 선택하고, ‘오퍼레이션 추가’라면 추상 데이터 타입을 선택해야한다.

‘타입 추가’에 관한 변경이라면 추상 데이터 타입의 경우 타입을 체크하는 클라이언트 코드를 일일이 찾아 수정한 후 올바르게 작동하는지 테스트해야한다. 반면 객체지향의 경우에는 클라이언트 코드를 수정할 필요가 없다. 간단하게 새로운 클래스를 상속 계층에 추가하기만 하면 된다.

‘오퍼레이션 추가’에 관한 변경이라면 객체지향의 경우 상속 계층에 속하는 모든 클래스를 한번에 수정해야 한다. 추상 데이터 타입의 경우에는 전체 타입에 대한 구현 코드가 하나의 구현체 내에 포함되어 있기 떄문에 새로운 오퍼레이션을 추가하는 작업이 상대적으로 간단하다.

협력이 중요하다

단순하게 오퍼레이션과 타입을 표에 적어 놓고 클래스 계층에 오퍼레이션의 구현 방법을 분배한다고 해서 객체지향적인 애플리케이션을 설계하는 것은 아니다. 객체 지향에서 중요한 것은 역할, 책임, 협력이다. 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션의 구현 방식을 타입별로 분배하는 것은 올바를 접근법이 아니다.

객체가 참여할 협력을 결정하고 협력에 필요한 책임을 수행하기 위해 어떤 객체가 필요한지에 관해 고민하라. 그 책임을 다향한 방식으로 수행해야 할 때만 타입 계층 안에 각 절차를 추상화라라. 타입 계층과 다형성은 렵력이라는 문맥 안에서 책임을 수행하는 방법에 관해 고민한 결과물이어야 하며 그 자체가 목적이 되어서는 안 된다.

태그:

카테고리:

업데이트:

댓글남기기