소프트웨어 아키텍처와 설계 원칙
아키텍처 스타일, 모듈화, 응집도·결합도, SOLID와 대표 디자인 패턴을 학습한다.
1. 아키텍처와 상세 설계
소프트웨어 아키텍처는 시스템을 이루는 주요 구성요소, 책임, 관계, 통신 방식과 중요한 설계 결정을 다룬다. 상세 설계는 각 모듈·클래스·함수의 구체 구조와 알고리즘을 정한다.
품질·업무 요구
↓
아키텍처 결정
- 구성요소 분할
- 통신 방식
- 데이터 소유권
- 배포 구조
↓
상세 설계
- 클래스·인터페이스
- 알고리즘·데이터 구조
- 오류 처리
↓
구현
중요한 품질속성은 성능, 가용성, 보안, 변경 용이성, 확장성, 시험 용이성, 사용성 등이다. 한 속성을 높이는 결정이 다른 속성에 비용을 줄 수 있으므로 트레이드오프를 고려한다.
2. 대표 아키텍처 스타일
계층형
Presentation
↓
Application
↓
Domain
↓
Infrastructure
책임을 계층별로 분리한다. 이해와 테스트가 쉬울 수 있지만 계층을 과도하게 통과하면 성능·복잡성 문제가 생길 수 있다.
클라이언트-서버
클라이언트가 서비스를 요청하고 서버가 처리한다. 2계층·3계층·다계층으로 확장할 수 있다.
MVC
사용자 → Controller → Model
│ │
└────► View ◄┘
↓
사용자
- Model: 상태와 업무 규칙
- View: 표현
- Controller: 입력 해석과 흐름 조정
구현 방식에 따라 실제 의존 방향은 달라질 수 있으므로 단순 화살표 하나만 암기하지 않는다.
저장소
여러 구성요소가 중앙 데이터 저장소를 통해 정보를 공유한다. 통합과 일관성에 유리하지만 저장소가 병목·단일장애점이 될 수 있다.
이벤트 기반
Producer ─ 이벤트 ─► Event Bus ─► Consumer A
└──► Consumer B
구성요소 결합을 줄이고 비동기 확장에 유리하지만 순서·중복·실패·관찰 가능성을 관리해야 한다.
마이크로서비스
서비스를 업무 능력 단위로 나누고 독립 배포·확장을 지향한다. 작은 서비스 수 자체가 목적은 아니다. 분산 통신, 데이터 일관성, 운영 자동화 비용이 증가할 수 있다.
3. 모듈화와 정보은닉
좋은 모듈은 책임이 명확하고 내부 변경이 외부로 덜 퍼진다.
- 추상화
- 정보은닉
- 관심사 분리
- 인터페이스와 구현 분리
- 높은 응집도
- 낮은 결합도
모듈을 무조건 작게 나누기보다 변경 이유와 책임 경계를 기준으로 분할한다.
4. 결합도
결합도는 모듈 사이 의존 정도이며 일반적으로 낮을수록 변경 파급이 작다.
약함
자료 결합
→ 스탬프 결합
→ 제어 결합
→ 외부 결합
→ 공통 결합
→ 내용 결합
강함
| 결합도 | 설명 |
|---|---|
| 자료 | 필요한 단순 데이터만 매개변수로 전달 |
| 스탬프 | 구조체·레코드 전체를 전달하고 일부만 사용 |
| 제어 | 다른 모듈의 흐름을 정하는 제어값 전달 |
| 외부 | 외부 형식·장치·프로토콜에 공동 의존 |
| 공통 | 전역 데이터 영역을 공유 |
| 내용 | 다른 모듈 내부를 직접 참조·수정 |
최근 객체지향 설계에서는 메시지 결합처럼 더 느슨한 관계를 별도로 설명하기도 한다. 분류 방식에는 차이가 있지만, 의존 정보의 범위가 커질수록 결합이 강해진다는 점이 핵심이다.
5. 응집도
응집도는 한 모듈 내부 요소가 하나의 목적에 얼마나 밀접한지 나타내며 일반적으로 높을수록 좋다.
강함
기능적
→ 순차적
→ 교환적(통신적)
→ 절차적
→ 시간적
→ 논리적
→ 우연적
약함
- 기능적: 하나의 명확한 기능 수행
- 순차적: 앞 단계 출력이 다음 단계 입력
- 교환적: 같은 데이터 입출력을 이용
- 절차적: 순서상 관련된 여러 기능
- 시간적: 같은 시점에 수행되는 기능
- 논리적: 비슷한 종류의 기능을 제어값으로 선택
- 우연적: 관련 없는 기능이 한 모듈에 모임
6. SOLID 원칙
| 원칙 | 핵심 질문 |
|---|---|
| SRP | 한 모듈의 변경 이유가 지나치게 많지 않은가? |
| OCP | 기존 코드를 광범위하게 수정하지 않고 확장 가능한가? |
| LSP | 하위 타입을 상위 타입 자리에 넣어도 계약이 깨지지 않는가? |
| ISP | 사용하지 않는 거대한 인터페이스에 의존하지 않는가? |
| DIP | 상위 정책이 구체 구현보다 추상화에 의존하는가? |
SOLID는 기계적으로 클래스 수를 늘리는 규칙이 아니라 변경과 대체 가능성을 높이기 위한 판단 기준이다.
7. 대표 디자인 패턴
생성 패턴
- Factory Method: 생성 책임을 하위 타입이나 별도 생성자에 위임
- Abstract Factory: 관련 객체 제품군 생성
- Builder: 복잡한 객체 생성 과정을 단계적으로 분리
구조 패턴
- Adapter: 호환되지 않는 인터페이스 변환
- Decorator: 객체를 감싸 기능을 동적으로 추가
- Facade: 복잡한 하위 시스템에 단순한 진입점 제공
행위 패턴
- Strategy: 알고리즘을 객체로 분리해 교체
- Observer: 상태 변화 시 구독자에게 통지
- Command: 요청을 객체로 캡슐화
- Template Method: 전체 알고리즘 뼈대는 상위에서 정하고 일부 단계를 하위에서 구현
Context ──► Strategy 인터페이스
├─ FastStrategy
└─ SafeStrategy
패턴 이름보다 해결하려는 문제, 참여 객체와 장단점을 이해해야 한다.
8. Strategy 예시
interface DiscountPolicy {
int discount(int price);
}
class FixedDiscount implements DiscountPolicy {
public int discount(int price) { return 1000; }
}
class RateDiscount implements DiscountPolicy {
public int discount(int price) { return price / 10; }
}
class OrderService {
private final DiscountPolicy policy;
OrderService(DiscountPolicy policy) {
this.policy = policy;
}
int finalPrice(int price) {
return price - policy.discount(price);
}
}
OrderService는 할인 구체 구현이 아니라 추상 계약에 의존한다. 새로운 정책을 추가할 때 기존 가격 계산 흐름을 크게 바꾸지 않을 수 있다.
9. 인터페이스·API 설계
인터페이스는 입력, 출력, 오류, 권한, 버전, 성능과 같은 계약을 명확히 해야 한다.
요청
- 입력 형식·필수값
- 인증·권한
- 멱등성·중복 처리
처리
- 업무 규칙
- 트랜잭션 경계
- 타임아웃·재시도
응답
- 정상 결과
- 오류 코드·메시지
- 부분 실패와 재처리 조건
숨겨진 부수효과가 적고 실패 조건이 명확해야 호출자가 안전하게 사용할 수 있다.
10. 품질속성 트레이드오프 예시
캐시 도입
├─ 장점: 응답시간·처리량 개선
└─ 비용: 데이터 일관성·무효화·메모리 관리 복잡성 증가
동기 복제
├─ 장점: 강한 일관성·데이터 안전성
└─ 비용: 쓰기 지연·가용성 저하 가능
하나의 설계가 모든 품질을 동시에 최대로 만들지는 않는다.
11. 품질속성 시나리오와 전술
아키텍처는 “성능이 좋아야 한다”는 추상 목표를 구체적 시나리오와 전술로 연결한다.
자극: 정상 부하의 10배 트래픽
환경: 한 인스턴스 장애 중
대상: 주문 API
응답: 요청을 다른 인스턴스로 분산
측정: 95백분위 2초, 오류율 0.1% 이하
| 품질속성 | 대표 전술 예 |
|---|---|
| 가용성 | 장애 감지, 중복화, 페일오버, 상태 복구 |
| 성능 | 캐시, 병렬 처리, 자원 풀, 부하 분산 |
| 변경 용이성 | 정보은닉, 인터페이스 안정화, 의존성 제한 |
| 보안 | 인증·인가, 최소 권한, 입력 검증, 감사 |
전술은 비용과 부작용을 가지므로 측정 가능한 시나리오로 평가한다.
12. 아키텍처 뷰와 관심사
하나의 다이어그램으로 모든 이해관계자의 관심사를 표현하기 어렵다.
논리 뷰 : 주요 기능·도메인 구조
프로세스 뷰 : 실행 중 프로세스·동시성·통신
개발 뷰 : 코드·모듈·패키지 구성
물리 뷰 : 노드·배포·네트워크
시나리오 : 위 뷰를 연결해 주요 흐름 검증
뷰와 관점은 표준·프레임워크에 따라 이름이 달라질 수 있으므로 “누구의 어떤 관심사를 설명하는가”를 본다.
13. 이벤트 중복과 멱등 소비자
메시지 전달이 at-least-once이면 같은 이벤트가 두 번 이상 도착할 수 있다.
이벤트 ID 확인 → 이미 처리했는가?
├─ 예: 결과 재사용 또는 무시
└─ 아니오: 업무 처리 + 처리 ID 원자적 기록
단순 재시도만 추가하면 중복 결제·중복 주문이 생길 수 있다. 멱등 키, 처리 이력, 원자적 상태 전이를 설계해야 한다.
14. 분산 데이터와 보상
마이크로서비스가 각자 데이터를 소유하면 하나의 로컬 ACID 트랜잭션으로 전체 업무를 묶기 어렵다. Saga는 여러 로컬 트랜잭션과 실패 시 보상 동작으로 장기 업무를 조정하는 방식이다.
주문 생성 → 결제 승인 → 재고 차감
실패 시 결제 취소 ← 재고 복원
보상은 과거를 완전히 지우는 rollback과 같지 않을 수 있으며 외부 효과·동시 변경·재시도까지 고려해야 한다.
15. 순환 의존과 안정적 계약
모듈 A→B→C→A의 순환 의존은 독립 빌드·테스트·배포를 어렵게 한다. 책임 재분할, 인터페이스 추출, 이벤트 도입으로 의존 방향을 끊을 수 있다. API 변경에서는 기존 호출자와의 호환성, 버전 전략, 단계적 폐기를 고려한다.
확인 문제
- 품질속성 시나리오에서 “95백분위 2초”는 어떤 역할을 하는가?
- 코드 모듈·패키지 구성을 주로 설명하는 아키텍처 뷰는 무엇인가?
- at-least-once 메시지 전달에서 소비자가 갖춰야 할 핵심 성질은 무엇인가?
- Saga의 보상 동작은 데이터베이스 rollback과 항상 동일한가?
- A→B→C→A 순환 의존이 주는 대표적인 문제는 무엇인가?