0. 설계 개요
-
설계
- 요구 분석(“무엇을 만들 것인가”) 이후 “어떻게 실현할 것인가”를 결정하는 활동
-
설계 종류
- 아키텍처(구조) 설계: 시스템 전체 구조, 모듈의 역할과 인터페이스 정의
- 상세 설계: 모듈 내부의 알고리즘, 데이터 명세화
-
적용 설계 원리
- 전통적: 분할 정복, 추상화, 모듈화 등
- 객체지향: 정보 은닉, 인터페이스 분리, 의존관계 역전 등
1. 설계 기본 개념
1.1. 분할과 정복
- 분할과 정복 (Divide and Conquer)
- 가장 기본적인 소프트웨어 설계 전략
- 큰 문제를 소 단위로 나누어 처리 후 결합
- 분할 계층: 시스템 → 서브시스템 → 패키지 → 클래스 → 메서드
1.2. 아키텍처 설계
| 핵심 개념 | 설명 |
|---|---|
| 모듈 | 클래스·패키지로 구성된 구현 단위 (정적 구조) |
| 컴포넌트 | 모듈·패키지로 구성된 실행 단위 (런타임 배포) |
| 서브시스템 | 기능 단위인 컴포넌트의 집합 |
| 3가지 설계 관점 | 설명 |
|---|---|
| 모듈 관점 | 코드 단위인 모듈과 그 관계 |
| 컴포넌트 관점 | 실행 시 동작하는 요소와 상호작용 |
| 할당 관점 | 하드웨어 설치, 작업 할당, 데이터 저장 관점 |
-
아키텍처 설계 과정
- 설계 목표(품질 목표) 설정
- 시스템 스타일(아키텍처 스타일) 결정
- 서브시스템 기능 및 인터페이스 명세
- 설계 리뷰 (피드백 반영)
-
아키텍처적으로 중요한 요구사항 (ASR)
- 설계 목적(비즈니스 목적), 품질 속성, 최우선 기능, 아키텍처 관심사, 제약사항
-
아키텍트 필요 역량
- 리더십, 의사소통, 협상, 기술적 능력, 프로젝트 기술, 분석 능력
2. 품질 목표
- 이해관계자의 요구를 반영하여 품질 속성을 결정한다.
| ISO 25010 품질 속성 | 영문 |
|---|---|
| 성능 | Performance |
| 상호운영성 | Interoperability |
| 사용용이성 | Usability |
| 신뢰성 | Reliability |
| 보안성 | Security |
| 유지보수성 | Maintainability |
| 이식성 | Portability |
3. 전통적인 설계 원리
- 핵심 요소
- 효율성(처리 시간·공간)
- 단순성(유지보수성에 가장 중요)
3.1. 추상화 (Abstraction)
- 목적과 관련된 정보에 집중하고 나머지는 무시
- 복잡도를 줄여 문제 분할의 근본
- 종류: 자료 추상화 / 절차·동작 추상화
3.2. 캡슐화 (Encapsulation)
- 속성(attribute)과 행위(method)를 하나로 묶고 일부를 정보 은닉(information hiding)
- 내부 변경에 대한 외부 영향을 최소화
3.3. 모듈화 (Modularization)
- 문제를 소프트웨어 구성 요소 수준으로 분할
- 모듈 = 클래스, 패키지 (독립적인 기능 단위)
- 추상화 → 캡슐화 → 모듈화 순으로 통째로 구성된 시스템을 잘 모듈화된 시스템으로 완성
(1) 결합도 (Coupling)
낮을수록 좋다
| 결합 종류 | 설명 | 강도 |
|---|---|---|
| 내용 결합 (Content) | 다른 모듈의 내부를 직접 참조 | 강함 ↑ |
| 공통 결합 (Common) | 전역 변수를 공유 | - |
| 제어 결합 (Control) | 제어 흐름을 결정 | - |
| 스탬프 결합 (Stamp) | 필요 없는 복합 자료구조 전달 | - |
| 데이터 결합 (Data) | 단순 타입 매개변수 전달 | 약함 ↓ |
개선 방향: 모듈 간 인터페이스 수를 줄이고, 간단한 정보를 매개변수로 전달
(2) 응집도 (Cohesion)
높을수록 좋다
| 응집 종류 | 설명 | 강도 |
|---|---|---|
| 우연적 (Coincidental) | 요소 간 의미적 관계 없음 | 약함 ↑ |
| 논리적 (Logical) | 같은 범주로 논리적 분류 | - |
| 시간적 (Temporal) | 특정 시간에 함께 처리 | - |
| 절차적 (Procedural) | 수행 순서와 관련 | - |
| 교환적 (Communicational) | 동일 입출력 데이터 사용 | - |
| 기능적 (Functional) | 하나의 기능에 모두 기여 | - |
| 정보적 (Informational) | 모든 기능이 같은 데이터에 실행 | 강함 ↓ |
모듈은 단일 책임을 가져야 한다.
4. 객체지향 설계 원리 (SOLID)
Martin이 제안한 5가지 원칙
| 원칙 | 키워드 | 핵심 한 줄 요약 |
|---|---|---|
| SRP | 단일 책임 | 클래스는 변경 이유가 하나뿐이어야 한다 |
| OCP | 개방 폐쇄 | 확장엔 열려 있고, 수정엔 닫혀 있어야 한다 |
| LSP | 리스코프 교체 | 자식 클래스는 부모 클래스를 대체할 수 있어야 한다 |
| ISP | 인터페이스 분리 | 사용하지 않는 인터페이스를 강제 구현해서는 안 된다 |
| DIP | 의존관계 역전 | 구체 클래스가 아닌 추상화(인터페이스)에 의존해야 한다 |
4.1. SRP — 단일 책임의 원리
Single Responsibility Principle
- 클래스는 하나의 책임(기능)만 가져야 한다
- 변경 이유를 하나로 제한 → 응집도 ↑, 결합도 ↓
4.2. OCP — 개방 폐쇄의 원리
Open Close Principle
- 확장(상속)에는 열려 있고, 수정에는 닫혀 있어야 한다
- 추상 클래스·인터페이스로 변화 부분을 추상화하고 다형성으로 기능 확장
- Open: 하위 클래스를 만들어 새 기능 추가
- Close: 기존 코드는 수정하지 않음
4.3. LSP — 리스코프 교체의 원리
Liskov Substitution Principle
- 상위 타입의 객체를 하위 타입으로 치환해도 프로그램이 정상 동작
- 상속은 코드 재사용이 아니라 행위의 일관성 보장
- LSP 위반 시 OCP도 연쇄 위반
// LSP 위반 예시
class Bicycle extends Transportation {
public void startEngine() {
throw new RuntimeException("자전거는 엔진이 없어요!"); // 위반!
}
}
4.4. ISP — 인터페이스 분리의 원리
Interface Segregation Principle
- 클라이언트가 사용하지 않는 인터페이스를 강제 구현해서는 안 됨
- 비만 인터페이스(fat interface)를 기능별로 분리하여 필요한 것만 구현
SRP: 클래스 내부 책임 분리 / ISP: 인터페이스 책임 분리
4.5. DIP — 의존관계 역전의 원리
Dependency Inversion Principle
- 고수준 모듈이 저수준 모듈에 직접 의존하지 않고, 둘 다 추상화(인터페이스)에 의존
- 변화하기 쉬운 구체 클래스보다 변화하기 어려운 인터페이스에 의존
- 전통 관계:
DataAnalyzer → BubbleSort - 역전 관계:
DataAnalyzer → SortAlgorithm ← BubbleSort
5. 컴포넌트 구성 원리
- 핵심 원칙
- 같이 변경·재사용·배포되는 것끼리 묶어 컴포넌트 구성
- 컴포넌트 간에도 DIP 적용: 인터페이스를 통해 종속성 관리
- 컴포넌트 내부: 높은 응집도 / 컴포넌트 간: 낮은 결합도
5.1. 의존성 주입 (Dependency Injection)
- 객체가 필요한 의존 객체를 직접 생성하지 않고 외부에서 주입 받는 방식
- DIP를 실제로 구현하는 가장 대표적인 방법
// DI 적용 예시
class OrderService {
private PaymentMethod paymentMethod;
public OrderService(PaymentMethod paymentMethod) { // 외부 주입
this.paymentMethod = paymentMethod;
}
}
5.2. IoC (Inversion of Control, 제어 역전)
- 인스턴스 생성과 의존 관계 연결을 개발자가 아닌 외부 프레임워크(DI 컨테이너) 가 담당
- DI 컨테이너가 객체를 생성하고 필요한 의존성 주입
- IoC를 구현하는 디자인 패턴 = 의존성 주입(DI)
5.3. 컴포넌트 DIP의 장점
- 독립적 배포: 하위 컴포넌트 교체 시 상위 컴포넌트 재컴파일 불필요
- 병렬 개발: 인터페이스(규격)만 정해지면 각 팀이 동시 개발 가능
- 테스트 용이: 실제 컴포넌트 대신 Mock 컴포넌트로 테스트 가능