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