소프트웨어를 설계 하는 방법 (분할 정복의 응집도, 결합도 소개)

우선 설계를 시작하기 전

비용 절감을 통해 이익을 낼 수 있도록 설계해야 한다는 점과, 요구한 것과 일치하는지 확인해야 한다는 생각을 염두에 두어야 한다.

 

 

이제 설계를 시작하기 위해 필요한 원리들을 알아보자.

설계의 원리는 다음과 같이 이루어져 있다.

 

 

 

 

분할 정복

알고리즘에서 들어봤듯이, 큰 문제를 작은 문제로 나눠서 소 단위의 작업을 하나씩 처리하여 전체 일을 끝내는 방법이다.

 

분할하는 형태는 어떤 것을 설계하여 분할하는지에 따라 달라진다.

  • 분산 시스템은 클라이언트와 서버로 분할
  • 시스템은 여러 서브시스템으로 분할
  • 서브시스템은 하나 이상의 패키지로 분할
  • 패키지는 여러 클래스로 분할
  • 클래스는 여러 메서드로 분할

 

 

예를 들어 대학교의 종합 정보 시스템을 설계해야 한다면 대략

학사 관리, 회계 관리, 인사 관리 등으로 나눈 후, 학사 관리는 수강 관리, 수업 관리, 성적 관리 등으로 나누어서 설계한다.

 

여기에 응집도 cohesion라는 개념이 추가된다.

 

 

 

 

응집도 cohesion

응집도는 모듈 내부에 존재하는 구성 요소들 사이의 밀접한 정도를 말한다.
하나의 모듈 안에서 구성 요소들 간에 똘똘 뭉쳐 있는 정도로 평가한다.

 

서브시스템이나 모듈이 서로 관련 있다면 같이 있게 하고 그 외의 것은 외부에 존재 하게 한다.
이렇게 하면 전체 시스템을 이해하거나 변경하기 쉬워진다.

 

 

같은 수준의 계층에 연관된 서비스를 제공하는 기능끼리 묶었을 때 나타나는 응집력을 계층 응집력이라고 한다.

계층을 형성하게 되면 고위층은 하위층의 서비스를 접근하지만, 하위층은 고위층에 접근하지 못한다.

 

 

 

응집력의 높고 낮음에 따라 정리해 놓은 그림을 토대로 응집력이 높은 수준부터 차례대로 알아보자.

 

기능적(functional) 응집

응집도가 가장 높은 경우이며 단일 기능의 요소로 하나의 모듈을 구성한다.

 

여기에는 단일 결과를 계산하기 위한 코드만이 모여 있는데

즉, 모듈이 단일 작업을 수행하여 결과를 내고 외부 부작용(side-effect)이 없는 것을 말한다.

 

기능적 응집의 특징으로 이해가 쉽고, 재사용하기 좋아서 다른 것으로 쉽게 대체할 수 있다.

 

 

 

순차적 응집

A 요소의 출력을 B 요소의 입력으로 사용하므로 두 요소가 하나의 모듈을 구성한 경우를 말한다.

두 요소가 아주 밀접하므로 하나의 모듈로 묶을 만하다.

 

즉, 한 프로시저의 출력이 다음 프로시저의 입력이 될 때 이들을 함께 두고 나머지는 외부에 둔다.

 

예를 들어 문서 인식 시스템의 경우

비트 맵을 받아 글자를 구성하는 구획으로 나누면, 해당 글자가 결정되고, 단어의 순서를 결정한다.

 

 

 

교환적 응집

같은 입력을 사용하는 구성 요소들을 하나의 모듈로 구성한다.

즉, 특정 자료를 저장하고 조작하는 시스템의 기능을 한 클래스 안에 포함한다.

 

클래스가 그 데이터를 조작하는 이외의 일은 하지 않고, 데이터를 조작할 때 한 곳에서 모든 코드를 찾을 수 있다.

(요소들 간의 순서는 중요하지 않음)

 

 

 

절차적 응집

순서가 정해진 몇 개의 구성 요소를 하나의 모듈로 구성한다.

 

하나의 프로시저 결과가 다음 프로시저에 입력으로 사용되지 않더라도, 프로시저 이후에 차례로 수행되는 프로시저를 모아 놓은 응집이다. 순차적 응집 보다 약하다.

 

순차적 응집과 비슷해 보이는데, 다른 점은

어떤 구성 요소의 출력이 다음 구성 요소의 입력으로 사용되지 않고, 순서에 따라 수행만 된다는 것이다.

 

 

 

시간적 응집

모듈 내 구성 요소들의 기능도 다르고, 한 요소의 출력을 입력으로 사용하는 것도 아니고, 요소들 간에 순서도 정해져 있지 않다.

 

하지만 !! 그 구성 요소들이 같은 시간대에 함께 실행된다는 이유로 하나의 모듈로 구성한다.

주로 프로그램 수행에서 같은 단계에 있는 오퍼레이션들이 같이 모여 있다.

 

예를 들어 한 시스템의 시작이나 초기화 하는 동안 사용되는 코드들을 같이 둔다. (절차적 응집보다 약함)

 

 

 

논리적 응집

모듈 간 순서와 무관하며, 한 모듈의 출력을 다른 모듈의 입력으로 사용하는 것도 아니다.

하지만! 요소들 간에 공통점이 있거나 관련된 임무가 존재하거나 기능이 비슷하다는 이유로 하나의 모듈로 구성한다.

 

ex)

  • 비슷한 기능(입출력) : scanf( ), printf( )를 결합시킨 입출력 모듈
  • 공통점(덧셈) : 정수의 덧셈과 행렬의 덧셈을 결합시킨 덧셈 모듈
  • 공통점(출력) : 단말기 출력 기능과 파일 출력 기능을 결합시킨 출력 모듈

 

 

 

우연적 응집

구성 요소들이 말 그대로 우연히 모여 구성된다.

 

특별한 이유 없이, 크기가 크다 보니 몇 개의 모듈로 나누는 과정에서 우연히 같이 묶인 것..이다.

 

 

 

 


 

 

 

모듈 사이에 응집력이 있다면 반대로 결합력이 감소할 수도 있다.

 

결합은 두 모듈 사이에 의존관계가 있을 때 발생하는데, 의존관계가 존재할 때 한 부분이 변경되면 다른 부분의 변경을 유발시킨다.


따라서 의존관계가 복잡하면 컴포넌트들이 어떻게 수행되는지 한눈에 보기 어렵다.

 

그래서 높은 결합력은 어떤 모듈을 재사용하려 할 때 그것과 결합되어 있는 다른 모듈까지 같이 가져와야 하는 문제가 있다.

 

 

 

결합도coupling

모듈과 모듈 사이의 관계에서 관련 정도를 말한다.

응집도랑 결합도랑 조금 헷갈리기 시작하는데..

응집도는 모듈 내부에 존재하는 구성 요소들 사이의 밀접한 정도를 말한다.


결합도는 하나의 모듈 안에서 구성 요소들 간에 뭉쳐 있는 정도를 보는 것이 아니라, 상호 의존성을 보는 것으로 독립적인지, 의존적인지를 판단한다.

 

좋은 설계를 위해서는

 

상호 의존성을 줄이고 모듈의 독립성을 높여서 모듈 간에 영향이 적게 하는 loosely coupled(낮은 결합력)을 목표로 해야 한다.

 

 

 

결합도가 낮은 데이터 결합부터 알아보자.

데이터 결합

모듈들이 매개변수를 통해 데이터만 주고받음으로써 서로 간섭을 최소화하는 관계이다.

 

모듈 간의 독립성을 보장하고 관계가 단순하여 하나의 모듈을 변경했을 때 다른 모듈에 미치는 영향이 아주 적다.

 

 

 

스탬프 결합

두 모듈 사이에서 정보를 교환할 때 필요한 데이터만 주고받을 수 없고 스탬프처럼 필요 없는 데이터까지 전체를 주고받아야 하는 경우를 말한다.

 

레코드나 배열 같은 데이터 구조, C 언어의 구조체(struct) 같은 경우를 예로 들 수 있겠다.

 

스탬프 결합을 줄이는 방법으로 인터페이스를 매개변수로 사용하거나, 단순 변수를 전달하는 방법이 있다.

 

 

 

제어 결합

제어 플래그flag를 매개변수로 사용하여 간섭하는 관계이다.

즉, 호출하는 모듈이 호출되는 모듈의 내부 구조를 잘 알고 논리적 흐름을 변경하는 관계이다.

 

정보은닉을 크게 위배하는 결합으로,다른 모듈의 내부에 관여하여 관계가 복잡해진다.

변경하려면 호출하는 메소드와 호출 당하는 메소드 모두 변경해야 한다. 만약 다형성을 가진 오퍼레이션을 사용한다면, 제어결합을 피할 수 있다.

 

제어결합을 감소시키는 방법은 look-up 테이블을 만드는 것이다.
(look-up 테이블 : 명령이 구동될 때 호출되어야 하는 메소드로 매핑)

 

 

 

공통 결합

모듈들이 공통 변수(전역변수)를 같이 사용하여 발생하는 관계이다.

 

이 경우는 변수 값이 변하면 모든 모듈이 함께 영향을 받는 문제가 있다.

그래도 시스템 전체가 사용하는 디폴트 값을 나타내는 전역 변수는 허용할 만 하다.

 

 

 

내용 결합

모듈 간에 인터페이스를 사용하지 않고 직접 왔다 갔다 하는 경우의 관계이다.

즉, 상대 모듈의 데이터를 직접 변경할 수 있어 서로 간섭을 가장 많이 하는 관계이다.

 

C 언어의 goto 문 같이, 다른 컴포넌트의 내부 데이터를 알리지 않고 수정하는 컴포넌트이다.

 

내용 결합을 피하기 위하여 모든 인스턴스 변수를 캡슐화 해야 한다.
( private으로 선언, get과 set 함수 제공)

 

 

 

루틴 호출 결합

하나의 루틴(객체지향에서는 메소드)이 다른 루틴을 호출할 때 결합이 발생한다.

서로의 행위에 의존하기 때문에 결합한다.

 

 

루틴 호출 결합은 어느 시스템이나 항상 존재한다.

어떤 연산을 위하여 두 개 이상의 메소드를 반복하여 사용하는 경우가 있다.

이런 부분을 캡슐화 하여 하나의 루틴을 정의하는 것으로 루틴 호출 결합을 줄일 수 있다.

 

 

 

타입 사용 결합

어떤 모듈이 다른 모듈에서 정의된 자료형을 사용할 때 발생한다.

클래스 안에서 인스턴스 변수를 선언하거나 지역 변수를 정의할 때는 언제든지 이런 결합이 나타난다.

 

공통 결합과 유사하지만, 자료가 공유되는 것이 아니라 자료형만 공유된다는 점이 다르다.

 

자료 결합보다는 영향이 덜하고, 스템프 결합을 줄이는 방법으로 사용된다.

나중에 다른 타입의 집합을 사용하더라도 수정이 용이하다는 장점을 가진다.

 

 

 

포함 결합

컴포넌트가 패키지를 import 하는 경우(Java), 또는 컴포넌트가 다른 파일을 include하는 경우(C, C++)

include나 import하는 컴포넌트는 include 또는 import 당하는 컴포넌트에 노출된다.

 

include 또는 import 당하는 컴포넌트에 변경이 가해질 경우, 변경 하는 컴포넌트 안의 어떤 요소와 상충되어 변경을 하게 된다.

 

다른 서브시스템이나 라이브러리 사용을 허용하기 때문에, import 당하는 컴포넌트 안의 어떤 요소가 import 하는 컴포넌트의 어떤 것과 이름이 같은 경우 결합을 피할 수 없다.

 

 

 

 


 

 

 

 

추상화

일반적으로 추상화란, 주어진 문제(건물 도면)에서 현재의 관심사에 초점을 맞추기 위해, 특정한 목적과 관련된 필수 정보만 추출하여 강조하고 관련이 없는 세부 사항을 생략함으로써 본질적인 문제에 집중할 수 있도록 하는 작업이다.

 

그렇다면 설계 원리에서의 추상화란, 상세하고 구체적인 부분을 숨기거나 상세화를 지연함으로써 복잡도를 줄여야 한다.

 

좋은 추상화는 불필요하게 상세한 사항을 알지 않아도 서브시스템의 근본을 알 수 있게 해 주는 정보은닉을 제공한다.

 

 

 

도형의 추상화 예시

 

 

 

객체지향 설계에서의 추상화

위의 추상화 과정을 보고 추상화 시킬 수 있는 여러 가지 방법을 알아보자.

 

 

 

과정 추상화

전체에 대해 알고리즘 형태로 작성한다. 상세 부분은 생략하고 과정만 표현한다.

 

 

 

데이터 추상화

Class의 특징을 갖는다.

사용자에게 클래스가 제공할 수 있는 사용법만 알려주고, 불필요한 데이터와 연산을 감춘다.

 

사용자는 클래스에서 제공하는 연산 기능만 알고 그 연산을 사용하여 데이터 값을 변경한다.

데이터와 메서드를 클래스 형태로 캡슐화하여 숨겨 놓고, 사용자에게는 꼭 필요한 기능만 사용할 수 있게 개방한 구조이다.

 

 

 

제어 추상화

 

여러줄의 내용을 간략히 줄여서 표현한다.

 

언어 level이 올라갈수록 표현이 더욱 간결해지고 특징만 나타내게 된다는 장점을 갖는다.

 

 

 


 

 

 

 

단계적 분해

단계적 분해는 하향식 설계에서 사용한다. 기능을 점점 작은 단위로 나누어 점차 구체화하는 방법이다.

 

 

 

 

모듈화

모듈화는 재사용성을 증진시킨다.

 

이 방법은 시스템의 다양한 측면을 고려하여 다른 환경에서도 다시 사용될 수 있도록 설계한다.

 

앞에서 소개한 세가지(결합, 응집, 추상) 설계 원리를 따르고, 설계를 가능하면 단순화시킨다.

 

 

재사용을 위한 설계와, 코드 재사용은 서로 보완적이다.

적극적으로 설계나 코드를 재사용하는 것은 다른 사람들이 투자한 재사용 컴포넌트로 이득을 얻을 수 있다.

(착각할 수 있는 것이, 예를들어 복제(cloning)는 재사용의 형식으로 볼 수 없다.)

 

 

이러한 특징을 갖는 모듈화는 미래에 이루어질 변경을 미리 예상하고 이를 준비하는 것이다.

 

모듈화는 결합력 감소와 응집력 증가, 추상화 생성, 선택의 여지를 남길 수 있다.

(시스템을 나중에 변경하는 사람의 선택권을 제한하지 않도록)

 

핵심은 재사용 가능 코드를 사용하고 코드를 재사용 가능하게 만드는 것이다.

 

 

 

 

정보 은닉

우리가 설계하는 컴포넌트를 사용하는 다른 사람이 항상 올바로 사용한다고 생각 하지 않도록 하자.

 

컴포넌트를 부적절하게 사용할 경우를 대비하여 구현한 컴포넌트에 대한 모든 입력이 올바른지 체크한다.

 

하지만 과도한 방어 설계는 불필요한 반복적 체크를 유발한다…

 

 

 

 

 

이렇게 지금까지 소프트웨어 설계에서 품질을 높이기 위해 고려해야 할 설계원리를 알아봤다.

다음에는 설계 최적안을 결정하는 방법에 대하여 공부해보자..