지난 글에서는 요구사항에 대해서 모델링하는 방법, 특히 UML 모델링에 대해서 정리해 보았다. 이번 글에서는 설계하는 방법에 대해서 정리해 보려고 한다.
1. 설계 원리
요구 분석이 "무엇을 만들 것인가"를 다루는 작업이라면, 설계는 "어떻게 실현할 것인가"를 구체적으로 결정하는 단계이다. 따라서 요구 분석 단계에서는 고객과 사용자가 중심이 되지만 설계 단계에서는 요구 사항을 구현하기 위한 해결 방법(솔루션)이 중심이 된다.
예를 들어, 건축 설계 작업을 생각해보자. 집주인의 요구는 침실 셋, 거실, 서재, 주방, 냉난방 시설, 수도와 전기, 욕실 두 개와 같은 요구사항을 제시하고 설계사는 이러한 요구를 받아 공간 배치와 구조를 결정한다.
거실을 넓히기 위해 다른 공간을 줄이거나 주방의 위치를 변경하는 등 다양한 설계 대안이 존재할 수 있다. 이처럼 설계는 동일한 요구사항을 만족하는 여러 가지 해결 방법 중 하나를 선택하는 과정이다.
설계 작업은 다음과 같이 두 가지로 나눌 수 있다.
- 기본 구조 설계: 시스템의 전체 구조를 정의하고 각 모듈의 역할과 인터페이스를 설계하는 단계
- 상세 설계: 각 모듈 내부의 알고리즘과 데이터 구조를 구체적으로 정의하는 단계
설계는 개발자의 창의성이 발휘되는 창조적인 과정이며, 요구 분석 단계와 완전히 분리되지 않고 일부가 겹쳐 진행될 수 있다.
2. 설계 기본 개념
설계는 프로그램이라는 구체적인 대상을 다루지만l 동시에 개념적인 작업이기도 하다. 따라서 설계 작업에 기초가 되는 개념을 정확히 이해하고 적용해야 한다.
전통적인 설계 방법은 Divide and Conquer, 추상화(Abstraction), 합성 등의 원리를 적용하여 대규모 문제를 다뤘다.
하지만 최근에는 아키텍처 기반의 설계 방식이 강조되고 있다. 아키텍처를 고려한 설계가 이루어져야 복잡한 시스템을 효과적으로 다룰 수 있으며, 특히 변경에 유연하게 대응할 수 있다.
따라서 설계를 제대로 이해하기 위해서는 서브시스템, 모듈과 같은 구성 요소와 설계 관점, 설계 과정 등 아키텍처와 관련된 개념을 함께 이해해야 한다.
2.1. 서브시스템
우선 소프트웨어의 아키텍처는 시스템을 구성하는 컴포넌트와 컴포넌트 상호작용의 집합이다.
컴포넌트는 독립적으로 취급될 수 있는 구성 단위로 서브시스템 또는 모듈이라고 부른다.
서브시스템은 여러 개의 클래스가 모인 집합으로, 일반적으로 패키지 단위로 구성된다. 또한 다른 서브시스템과 상호작용하기 위한 명확한 인터페이스를 가진다.
시스템의 복잡도를 줄이기 위해 전체 시스템을 여러 개의 서브시스템으로 분할한다.
이렇게 분할하면 각 부분을 독립적으로 개발할 수 있어 새갑ㄹ자 간 커뮤니케이션 비용이 줄어들고, 특정 부분의 변경이 다른 부분에 미치는 영향도 최소화할 수 있다.
또한 복잡한 시스템의 경우 이러한 서브시스템들을 계층 구조로 조직하여 관리할 수 있다.

+) 컴포넌트, 모듈, 서브시스템의 개념
- 컴포넌트: 명확한 역할을 가지며 독립적으로 배포 및 실행될 수 있는 시스템의 구성 요소
- 인터페이스 기반으로 다른 컴포넌트로 대체 가능하며 재사용 가능하도록 설계
- 모듈: 프로그래밍 언어의 문법 구조 내에서 정의되는 구현 단위
- 실제 코드 수준에서 구분되는 단위, 클래스나 메서드와 같은 형태
- Java 프로그램의 모듈: 메서드, 클래스, 패키지 등
- 서브시스템: 시스템을 구성하는 큰 단위로, 여러 개의 컴포넌트나 모듈을 포함하는 독립적인 기능 영역
- 하나의 명확한 기능을 수행, 외부와는 정의된 인터페이스를 통해 상호작용
3. 설계 관점
소프트웨어는 개발 과정에서 다양한 형태로 표현되며, 이러한 설계 의도를 잘 나타낸 것이 아키텍서 설계이다. 아키텍처는 다음과 같은 관점에서 이해할 수 있다.
- 모듈 관점: 일정한 책임을 수행하는 코드 단위인 모듈과 그들 간의 관계를 통해 소프트웨어의 구조를 설명하는 관점

- 컴포넌트 관점: 실행 시 동작하는 컴포넌트와 이들 간의 상호작용을 중심으로 시스템 구조를 설명하는 관점

- 할당 관점: 소프트웨어가 실제로 어떤 하드웨어에 배치되는지, 작업이 어떻게 분배되는지, 데이터가 어디에 저장되는지를 다루는 관점

4. 설계 작업 과정
설계 작업은 의사 결정 과정이면서 동시에 시스템을 알아가는 과정이다. 즉 단순한 구현 준비 과정이 아닌 다양한 선택지 중에서 최적의 해결 방법을 결정하는 의사 결정 과정이다.
이 과정에서는 성능, 안정성, 확장성 등 다양한 요인을 고려해야 하며, 특히 개발하려는 시스템의 유형이 중요하다. 아키텍처 설계 과정은 다음과 같은 단계로 이루어진다.

- 설계 목표 설정
- 전체 시스템이 만족해야 할 설계 목표를 정의하는 단계
- ex) 전화 교환 시스템 개발할 경우 고장에 대한 내성, 안전과 보안, 최대 성능이 설계 목표가 된다.
- 스타일 결정
- 설꼐 목표와 시스템 유형에 적합한 아키텍처 스타일을 선택하는 단계
- 이미 존재하는 아키텍처 스타일을 선택할 수도 있고 맞춤형 아키텍처를 설계할 수도 있다.
- 서브시스템 기능 및 인터페이스 명세
- 시스템을 여러 개의 서브시스템으로 나누고, 각 서브시스템의 역할과 책임을 정의하는 단계
- 서브시스템 간의 상호작용을 위해 필요한 인터페이스 정의
- 아키텍처 설계 검토
- 설계한 아키텍처가 요구사항, 설계 목표, 설계 원리를 잘 만족하는지 검토
5. 품질 목표
기능 요구사항 외에도 시스템은 다양한 품질 요구사항을 만족해야 한다. 이러한 비기능 요구사항(Non-functional requirements)은 시스템의 품질 특성으로, 설계를 결정하는 중요한 요소가 된다.
따라서 설계 작업 전에 시스템이 요구하는 품질 특성을 충분히 분석해야 한다.
이러한 품질 제약사항은 설계 목표로 설정되며, 요구 분석 단계에서 도출된 비기능 요구사항을 구체적인 설계 목표로 변환해야 한다.
이후 이를 만족시키기 위한 여러 설계 대안을 도출하고, 그 중에서 가장 적절한 설계를 선택해야 한다.
예를 들어, 시스템이 유용성, 성능, 안정성을 동시에 만족해야 한다고 가정해보자.
이 경우 안정성을 높이기 위한 설계가 성능 저하를 초래할 수 있다. 반대로 성능을 극대화하면 안정성이 떨어질 수 있다.
이처럼 여러 품질 속성은 서로 충돌할 수 있기 때문에 설계 과정에서는 각 품질 속성 간의 영향을 고려하여 균형을 맞추는 것이 중요하다.
즉, 설계자는 품질 요구사항 간의 트레이드오프(Trade-off)를 고려하여 최적의 설계를 선택해야 한다.

6. 전통적인 설계 원리
소프트웨어를 설계할 때 전통적으로 중요하게 고려되는 특성은 효율성(efficiency)과 단순성(simplicity)이다.
6.1. 효율성
효율성은 시스템이 사용하는 자원이 적정하고 효과적으로 활용되는 정도를 의미한다.
일반적으로 고려되는 자원은 처리 시간과 기억 공간이며, 효율적인 시스템은 이 두 자원을 최소한으로 사용하면서 원하는 기능을 수행한다.
과거에는 제한된 CPU와 메모리 자원으로 인해 효율성이 매우 중요한 요소였짐나 현재는 하드웨어 성능의 발전으로 인해 일반적인 시스템에서는 그 중요도가 다소 낮아진 측면이 있다. 그렇지만, 실시간 시스템이나 대규모 트래픽을 처리하는 시스템에서는 여전히 중요한 설계 요소이다.
6.2 단순성
단순성은 소프트웨어 설계에서 매우 중요한 품질 특성이다.
특히 유지보수성에 큰 영향을 미치며 시스템을 이해하기 쉽게 만드는 핵심 요소이다.
유지보수 과정에서는 먼저 시스템 구조를 이해해야 하는데, 모듈 간의 관계가 복잡할수록 이해하기 어려워지고 수정 또한 어려워진다. 따라서 가능한 한 단순한 구조로 설계하는 것이 중요하다.
단순하고 효율적인 설계를 지향하는 것은 기본적인 원칙이다. 하지만 이를 실현하기 위해서는 분할, 계층화 원리, 추상화의 원리, 모듈화 원리 등을 깊이 이해하고 적용해야 한다.

6.3. 추상화
추상화(Abstraction)은 모든 엔지니어링 작업에서 사용되는 좋은 개념으로 대상 시스템에서 특정 목적과 관련된 핵심 정보만을 선택하고 나머지 세부 사항은 숨기는 것이다.
예를 들어 설계단계에서는 시스템의 복잡도를 줄이기 위해 이런 추상화를 적극적으로 활용한다.
예를 들어, 클라이언트와 서버 간의 통신을 설계할 때 실제 네트워크 연결 과정(TCP 연결, 데이터 전송 등)을 모두 표현하는 대신, 단순히 메시지라는 개념으로 표현할 수 있다.
이때 메시지는 데이터의 구조만을 표현한 것으로, 데이터 추상화의 한 예라고 볼 수 있다.
또한, 다음 그림과 같이 절차적인 동작을 send(client, server, message)로 정의하고 그 절차를 추상적으로 정의할 수 있다.

추상화는 시스템의 복잡성을 줄이고 설계를 단순하게 만들며 복잡한 문제를 여러 단계로 나누어 해결할 수 있도록 해준다. 따라서 추상화는 소프트웨어 설계 과정에서 필수적인 개념이며, 복잡한 문제를 분할하고 관리할 수 있게 하는 기반이 된다.
6.4. 캡슐화
캡슐화는 데이터와 그 데이터를 처리하는 기능을 하나로 묶고, 외부에는 필요한 인터페이스만 제공하는 개념이다. 즉, 객체가 제공하는 기능(서비스)은 외부에 공개하되, 내부 구현 방식은 숨기는 것을 의미한다.
캡슐화를 적용하면 내부에 데이터가 어떻게 저장되고 처리되는지는 외부에서 알 필요가 없으며, 이를 정보 은닉이라고 한다.

위 그림과 같이 예를 들어 TV와 리모컨을 생각해보자. 사용자는 리모컨을 통해 채널 변경, 전원 ON/OFF 등의 기능을 사용할 수 있다. 하지만 TV 내부에서 아날로그 신호를 디코딩하고 화면으로 변환하는 과정은 사용자에게 노출되지 않는다.
이처럼 사용자는 인터페이스만을 통해 기능을 사용하고, 내부 구현은 숨겨지는 것이 캡슐화이다.
캡슐화는 객체지향 프로그래밍뿐만 아니라 일반적인 소프트웨어 설계에서도 적용되는 중요한 원리이며, 시스템의 복잡도를 줄이고 유지보수성을 향상시키는 데 기여한다.
6.5. 모듈화
모듈화는 복잡한 문제를 소프트웨어의 구성 요소가 될 수 있는 단위로 분할하는 과정이다. 복잡한 시스템을 여러 개의 작은 구성 요소로 나누면 개발자와 사용자 모두 시스템을 더 쉽게 이해하고 관리할 수 있다.

모듈로 분할하게 되면 각각의 모듈을 별개로 만들고 수정할 수 있어 확장성이 향상된다. 또한 모듈 단위로 테스트하거나 빌드할 수 있기 때문에 특정 부분을 변경하더라도 전체 시스템에 미치는 영향을 최소화할 수 있다.
그러나 모듈성이 지나치게 이루어지면 모듈 간의 상호작용을 이해하는 데 오히려 더 많은 비용이 들 수 있다. 따라서 시스템의 복잡도와 규모를 고려하여 적절한 수준으로 모듈을 분할하는 것이 중요하다.
모듈화는 재사용성을 극대화하면서 의존성을 줄이는 방향으로 설계되어야 한다. 이를 위해서는 각 모듈이 독립적인 기능을 수행하도록 구성하고, 모듈 간 결합도를 낮추는 것이 바람직하다.
또한 모듈의 크기는 시스템을 이해하고 유지보수하며 확장하기 쉬운 수준에서 균형있게 설계하는 것이 중요하다.
6.6. 결합(Coupling)
결합은 모듈 간에 서로 의존하는 정도를 의미한다. 좋은 소프트웨어는 일반적으로 결합도가 낮아야 하며 모듈 간의 독립성이 높을수록 유지보수와 확장이 용이하다.
결합도가 높은 경우 모듈 간 의존성이 강해져 시스템을 이해하기 어려워지고, 하나의 모듈 변경이 다른 모듈까지 영향을 미칠 가능성이 커진다. 따라서 디버깅과 결함 수정 또한 복잡해진다.
모듈의 결합 정도는 다음 두 가지 요소에 의해 결정된다.
- 모듈 간 인터페이스 수
- 각 인터페이스의 복잡성

위 두요소에 따라 5가지로 나뉜다. 아래로 갈수록 결합도가 낮고 바람직하다.
- 내용 결합(Content Coupling): 한 모듈이 다른 모듈의 내부 구현이나 데이터 직접 참조
- 공통 결합(Common Coupling): 여러 모듈이 동일한 전역 데이터를 공유하는 경우
- 제어 결합(Control Coupling): 한 모듈이 다른 모듈의 동작 흐름을 제어하기 위해 제어값이나 플래그를 전달하는 경우

위 그림은 제어 결합의 예시로, print 함수를 호출한 모듈은 print 함수가 정의된 모듈과 제어 결합 수준이 된다. 그 이유는 첫 번째 줄 print의 displayMetricValues가 print 내부 로직의 if 분기를 결정하기 때문이다.
- 스탬프 결합(Stamp Coupling): 필요한 일부 데이터만 사용하면서도 복합 데이터 구조 전체를 전달하는 경우
- 데이터 결합(Data Coupling): 모듈 간에 필요한 데이터만 단순한 형태로 전달하는 경우
이러한 결합력을 낮추려면 모듈 사이의 인터페이스 수를 줄이고 각 인터페이스의 복잡도를 낮추어야 한다.
6.7. 응집
응집은 하나의 모듈 내부에서 수행되는 작업들이 서로 얼마나 관련되어 있는지를 나타내는 정도이다. 즉, 모듈을 구성하는 요소들이 하나의 목적을 위해 얼마나 밀접하게 결합되어 있는지를 의미한다.
모듈 내부의 요소들이 하나의 기능을 중심으로 유기적으로 구성될수록 응집도가 높아지고, 이러한 모듈은 이해하기 쉽고 재사용성과 유지보수성이 향상된다. 따라서 응집도는 높을수록 바람직하다.

응집은 일반적으로 다음과 같이 7가지 유형으로 구분되며, 아래로 갈수록 응집도가 높고 바람직하다.
- 우연적(Coincidental) 응집: 모듈 내 요소들이 서로 아무 관련이 없는 경우 (가장 낮은 응집)
- 논리적(Logical) 응집: 비슷한 종류의 작업들을 하나의 모듈에 묶은 경우 (예: 입출력 처리 모듈)
- 시간적(Temporal) 응집: 특정 시점에 함께 수행되는 작업들을 묶은 경우 (예 초기화 작업)
- 절차적(Procedural) 응집: 모듈 안에서 수행되는 연산이 프로그램에서 수행되는 순서와 관련 있는 경우
- 교환적(Communicational) 응집: 동일한 데이터를 처리하는 작업들을 하나의 모듈로 묶은 경우
- 기능적(Functional) 응집: 하나의 기능에 모두 기여하고 밀접하게 관련된 경우
- 정보적(Informational) 응집: 동일한 데이터 구조를 공유하면서 각 기능이 독립적인 entry point를 가지는 경우
객체지향 설계에서는 하나의 클래스가 하나의 책임을 가지도록 설계되기 때문에 자연스럽게 정보적 응집을 가지도록 유도된다.
응집을 높이기 위해서 모듈 안에 여러 가지 책임을 지워서는 안된다. 즉, 단일 책임을 갖게 하면 대부분 기능적 응집 이상으로 모듈 안의 응집력이 높아지게 된다.
7. 객체지향 설계 원리
앞서 정리한 전통적인 설계 원리는 객체지향 프로그램의 등장으로 함께 발전되었다. 객체지향 개념(상속, 인터페이스 등)의 새로운 구문이 추가되면서 재사용, 수정용이성 등의 품질을 높일 수 있게 되었으며 이를 위해 지켜야할 설계 원리가 존재한다.
객체지향의 일반적인 설계 원칙과 Martin이 제안한 5가지 중요한 설계 원칙(SOLID)에서 정리해 보려고 한다. SOLID는 다음과 같다.
- 단일 책임의 원리(Single Responsibility Principle)
- 개방 폐쇄의 원리(Open Close Principle)
- 리스코프 교체의 원리(Liskov Substitution Principle)
- 인터페이스 분리의 원리(Interface Segregation Principle)
- 의존관계 역전의 원리(Dependency Inversion Principle)
SOLID 설계 원리는 객체지향 소프트웨어를 이해하기 쉽고 유지 및 관리하기 쉽게 만든다.
7.1. 인터페이스와 구현의 분리
객체지향 언어에는 인터페이스를 통해 기능의 명세와 구현을 분리할 수 있다.
인터페이스는 메서드의 구현 없이 메서드의 이름, 매개변수, 반환형과 같은 시그니처만을 정의한 것이다.
일반적으로 클래스는 메서드의 실제 구현을 포함하지만, 공개해야 하는 기능은 인터페이스로 정의하고, 클래스는 이를 구현하는 방식으로 관계를 맺는 것이 바람직하다.
인터페이스와 구현 분리 원칙은 컴포넌트가 외부에 제공하는 기능(인터페이스)과 내부 구현을 분리하는 것을 의미한다.
이 원칙을 적용하면 내부 구현이 변경되더라도 인터페이스가 유지되는 한 해당 컴포넌트를 사용하는 다른 부분에는 영향을 주지 않는다.
예를 들어 데이터 저장 방식을 파일에서 데이터베이스로 변경하더라도, 인터페이스만 동일하게 유지된다면 사용하는 쪽의 코드는 수정할 필요가 없다.
7.2. 단일 책임의 원리
객체지향 설계에서 클래스는 중요한 모듈 단위이다. 단일 책임의 원리는 클래스는 하나의 책임만 가져야 하며, 클래스를 변경해야 하는 이유는 하나여야 한다는 원리이다.

위 그림에서 Book이라는 클래스는 관련된 정보 책명(name), 저자(author), 본문(text)을 저장하고 유지하는 책임이 있다. 이때, 본문 내용을 화면에 보여주는 역할을 가진 printTextToConsole()이라는 함수를 Book에 추가한다면, Book의 책임이 데이터 관리 책임, 출력 책임 두 가지로 다양해진다.
이렇게 하나의 클래스가 여러 책임을 가지면 출력 방식이 변경될 때도 Book 클래스를 수정해야 하고 데이터 구조가 변경될 때도 Book 클래스를 수정해야 한다. 즉, 유지보수가 어려워진다.
따라서 출력 기능은 별도의 BookPrinter 클래스로 분리하여 각 클래스가 하나의 책임만 가지도록 설계하는 것이 바람직하다.
7.3. 개방 폐쇄의 원리
개방 폐쇄 원칙은 소프트웨어의 구성 요소(클래스, 모듈, 함수 등)는 확장에 열려 있고(Opened), 수정에는 닫혀 있어야(Closed)한다는 원칙이다.
즉, 새로운 기능을 추가할 때 기존 코드를 변경하지 않고, 확장을 통해 기능을 추가할 수 있도록 설계해야 한다.

위 그림의 OCP 사례를 알아보기 위해서는 다형성을 이해해야 한다. SortAlgorithm이라는 상위 클래스(또는 인터페이스)에 sort() 메서드가 정의되어 있고, 이를 BubbleSort, HeapSort, ShellSort와 같은 하위 클래스들이 각각 구현한다. 이처럼 동일한 인터페이스를 가지지만 서로 다른 방식으로 동작하는 것을 다형성이라고 한다.
Client는 SortAlogrithm 타입에 의존하여 정렬 기능을 사용한다. 따라서 새로운 정렬 알고리즘을 추가할 경우, 기존 코드를 수정할 필요 없이 새로운 클래스를 추가하기만 하면 된다.
이와 같이 OCP를 적용하면 시스템은 확장에는 유연하게 대응하면서도 기존 코드의 안정성을 유지할 수 있다.
7.4. 리스코프 교체의 원리
리스코프 교체의 원리는 하위 클래스는 상위 클래스를 대체할 수 있어야 한다는 원리이다. 즉, 어떤 객체를 상위 클래스 타입으로 사용하던 코드는 하위 클래스 객체로 바꾸더라도 프로그램의 동작이 동일하게 유지되어야 한다.
이 원리는 단순한 상속 관계를 넘엇, 상위 클래스가 정의한 동작을 하위 클래스가 반드시 지켜야 한다는 의미를 가진다.

위 그림에 대해서 설명하면 다음과 같다.
- Animal 클래스는 makeNoise() 메서드를 제공하며, 일반적으로 Animal 타입을 사용하여 makeNoise()를 호출하면 정상적으로 소리가 난다고 기대한다.
- MuteMouse에서 makeNoise() 호출 시 IMakeNoiseException을 발생시키게 된다.
- 즉 부모(Anminal)은 소리가 출력되는 정상 동작을 기대하지만, 자식(MuteMouse)는 예외가 발생해 동작을 실패한다.
위 경우 Animal을 MuteMouse로 대체하면 클라이언트 코드에서 예상하지 못한 오류가 발생하게 된다.
7.5. 인터페이스 분리의 원리
인터페이스 분리 원칙은 클라이언트가 사용하지 않는 인터페이스에 의존하도록 강요해서는 안 된다는 원칙이다.
즉, 하나의 큰 인터페이스를 여러 개의 작은 인터페이스로 분리하여 각 클라이언트가 필요한 기능만 의존하도록 설계해야 한다.
인터페이스를 설계할 때 모든 기능을 하나의 인터페이스에 포함시키면, 일부 클래스는 사용하지 않는 메서드까지 구현해야 하는 상황이 발생한다. 이러한 인터페이스를 비만 인터페이스(fat interface) 또는 오염된 인터페이스(polluted interface)라고 한다.
비만 인터페이스(또는 오염된 인터페이스)를 방지하기 위해서는 하나의 큰 비만 인터페이스 대신에 다수의 작은 인터페이스를 만들어 필요한 것만 사용하도록 해야 한다. 다음 그림을 한 번 살펴보자.

그림에 대해서 설명하면 다음과 같다.
- 왼쪽의 Animal에서는 fly(), walk(), eat(), work() 메서드가 존재한다.
- 이때 People 클래스는 fly() 기능이 필요 없음에도 불구하고 해당 메서드를 강제로 구현해야 한다.
- 이는 불필요한 의존성을 만들고 설계를 복잡하게 만든다.
- 오른쪽에서는 왼쪽 경우와 달리 인터페이스를 People, Animal, Bird로 분리한다.
- 이렇게 하면 각 클래스는 자신에게 필요한 인터페이스만 구현하면 된다.
이렇게 설계하면 불필요한 다수의 책임이 잇는 클래스에 의존하지 않고 작은 필요한 책임으로 분리된 시스템이 되어 결합력이 느슨해진다.
7.6. 의존 관계 역전의 원리
의존 관계 역전의 원리는 고수준 모듈이 저수준 모듈에 의존해서는 안되며 둘 다 추상화에 의존해야 한다는 원리이다. 또한 추상화는 구체적인 구현에 의존해서는 안되고, 구체적인 구현이 추상화에 의존해야 한다.

키보드에서 데이터를 읽어 프린터에서 출력하는 Copy 모듈을 생각해보자.
- 왼쪽 그림은 Copy가 ReadKeyboard나 WritePrinter에 직접 의존하는 모습이다.
- 이 경우, 출력 장치를 프린터에서 디스크로 변경하면 Copy 모듈도 함께 수정해야 한다는 문제가 발생한다.
- 오른쪽 그림은 Copy와 낮은 수준의 두 모듈 사이를 분리하는 추상화를 도입한 모습이다.
- 이 경우, Copy는 인터페이스에 의존하므로 출력 장치가 바뀌어도 Copy는 수정할 필요가 없다.
결과적으로, 추상화된 모듈이 구체화된 모듈에 의존하면 안되고 구체화된 모듈이 추상화된 모듈에게 의존하도록 설계되어야 한다는 것이다.
8. 설계 메트릭
설계 단계에서는 요구 분석에서 도출된 문제에 대해 다양한 해결 방안을 고려한다. 설계를 마친 후에는 해당 설꼐가 설계 원리를 잘 반영했는지, 그리고 품질 요구사항을 만족하는지를 평가해야 한다.
이를 위해 여러 설계 후보 중에서 품질 요구와 제약 조건을 가장 잘 만족하는 설계를 선택하는 과정이 필요하다.
8.1. 전통적인 설계 메트릭
전통적인 설계 메트릭은 시스템의 구조적 특성을 정량적으로 평가하기 위한 지표이다.
- 크기: 시스템 규모를 나타내는 지표로 모듈의 개수나 모듈 간 인터페이스 수 등을 통해 측정
- 복잡도: 시스템 내부 요소들이 얼마나 복잡하게 연결되어 있는지를 나타내는 지표로, 제어 흐름이나 구조적 복잡성 포함
- 결합도: 모듈 사이에 실제적으로 어느 정도 연결되어 있는지 나타내는 척도(입출력 매개변수, 전역 변수, 호출된 모듈 개수)
- 응집도: 하나의 모듈이 단일 목적을 위해 얼마나 밀접하게 구성되어 있는지를 나타내는 지표
- 정보 흐름: 시스템 내에서 데이터가 얼마나 많이 전달되고 처리되는지를 나타내는 지표(매개변수, 전역 변수, 입출력 수)
8.2. 객체지향 메트릭
객체지향은 캡슐화, 상속, 다형성, 동적 바인딩 등의 특성을 반영한 메트릭이 사용된다. 대표적으로 Chidamber, Kemerer 매트릭이 있으며 다음 표와 같다.

'학교공부 > [소프트웨어공학]' 카테고리의 다른 글
| [소프트웨어공학] - 요구 모델링 (0) | 2026.04.21 |
|---|---|
| [소프트웨어공학] - 요구 분석 (0) | 2026.04.21 |
| [소프트웨어공학] - 프로젝트 관리와 계획 (0) | 2026.04.21 |
| [소프트웨어공학] - 프로세스와 방법론 (1) | 2026.04.21 |