좋은 소프트웨어 아키텍처란, 경계를 설정하는 기술이다.
경계는 요소(컴포넌트, 객체)를 분리하고, 불필요한 조기 결정을 늦춤으로써 핵심 업무 로직을 오염시키지 않도록 하는 데 목적이 있다.
이른 결정을 피해야 하는 이유는, 그러한 결정이 시스템의 유지 효율성을 급격히 떨어뜨리는 결합도를 야기하기 때문이다.
특히 업무 요구사항과 무관한 결정(예: 프레임워크, DB, DI 프레임, 라이브러리 등)은 가능한 한 늦게 내리는 것이 바람직하다.
좋은 아키텍처는 이러한 조기 결정을 유예할 수 있게 설계되며, 설령 나중에 결정하더라도 시스템 전체에 영향을 주지 않도록 한다.
📌 사례 1: 너무 이른 결정이 만든 과도한 복잡성
사내에서 사용하는 단순한 휴가 신청 시스템을 만들기로 했다.
요구사항은 간단했다. 직원이 휴가를 신청하고, 팀장이 승인하면 끝이었다.
👉 그러나 개발팀은 너무 앞서 나갔다
- '나중에 외부 고객도 쓸 수 있지 않을까?'라는 생각에 로그인 시스템을 OAuth 기반으로 설계
- '나중에 팀 구조가 복잡해질 수도 있어'라는 이유로 부서 계층을 트리 구조로 구현
👉 결과적으로 생긴 문제
- OAuth를 도입하면서 토큰 발급, 리프레시 처리, 외부 인증 연동 등 불필요한 복잡성이 생겼고, 로그인 문제 발생 시 디버깅 난이도도 상승함
(사실 내부 직원 20명만 쓰는 서비스였고, 사내 계정 기반 단순 로그인으로 충분했음) - 부서 구조를 트리로 구성하면서 팀장 탐색 로직이 복잡해짐
(반면 단일 테이블 구조였다면 '팀장 ID' 컬럼만 추가해 SELECT * FROM users WHERE id = 팀장_ID 쿼리 한 줄이면 됐음)
📌 사례 2: 늦춘 결정이 만든 유연한 시스템
한 팀이 메모 관리 앱을 만들고 있다.
이 앱은 메모 작성 및 태그 검색 기능만 필요했다.
👉 그래서 다음과 같은 방식으로 설계했다:
- 저장은 처음에는 필요하지 않으므로, 인메모리 저장소(InMemoryMemoRepository) 로 시작
- 저장소 인터페이스는 정의만 해두고, 더미 구현으로 대체
- 어떤 DB를 쓸지는 나중에 결정 → JSON 파일을 사용
👉 얻은 유연성
- 초기 개발에 집중 → 저장 로직 없이 프론트 동작만 빠르게 반복 가능
- 저장이 필요해졌을 때 JSON 또는 MySQL로 자연스럽게 확장 가능
- DB 미사용으로 로컬 테스트가 쉬움 (트랜잭션, 스키마 설계, 접속 설정 불필요)
🔧 어떻게 어떤 데이터베이스든 나중에 붙일 수 있을까?
1. 저장소 인터페이스 정의
interface MemoRepository {
findById(id: string): Promise<Memo>
save(memo: Memo): Promise<void>
delete(id: string): Promise<void>
}
2. 인메모리 저장소 스텁 구현
class InMemoryMemoRepository implements MemoRepository {
private data = new Map<string, Memo>()
...
}
3. 나중에 다른 구현체로 교체 가능
class JsonMemoRepository implements MemoRepository {...}
class MySQLMemoRepository implements MemoRepository {...}
4. 앱은 구현체에 의존하지 않도록 구성
class MemoService {
constructor(private repo: MemoRepository) {...}
}
✒️ 언제 어디에 선을 그어야 하는가?
관련 없는 것들 사이에 선을 그어야 한다.
- GUI ↔ Business Rules → 분리 필요
- DB ↔ GUI → 분리 필요
- DB ↔ Business Rules → 분리 필요
💿 업무 규칙과 DB는 분리되어야 한다
업무 규칙은 DB가 어떻게 동작하는지 몰라도 된다.
단지 “데이터를 저장하고 불러올 수 있다”는 추상적 계약만 있으면 된다.
이 추상화는 인터페이스로 구현 가능하며, 실제 구현은 감출 수 있다.
Business Rules
↓
Database Interface
↑
DB Controller → DB
이 구조에서는 Database Controller를 의존하는 객체가 없기 때문에, 해당 지점을 경계선으로 삼을 수 있다.
이렇게 경계를 나누면 다음과 같은 일이 가능하다:
- Business Rules는 DB 구현체를 몰라도 된다 → DB 종류나 구조가 바뀌어도 영향 없음
- 테스트할 때는 DB 없이도 가능 → InMemory 저장소만으로 단위 테스트 가능
- 유지보수와 교체가 쉬움 → DB Controller만 바꾸면 다른 저장소로 쉽게 전환
👁️ GUI도 마찬가지로 분리되어야 한다
Business Rules
↑
GUI
- GUI는 표현 방식일 뿐이며, 변경이 자주 일어난다
- Business Rules는 GUI에 대해 아무것도 알 필요 없다
- 덕분에 CLI, 모바일, 웹 등 다양한 UI로 교체 가능
🔌 플러그인 아키텍처로 일반화하기
GUI와 DB는 Business Rules에 의존하는 방향으로 연결된다.
이 구조는 곧 플러그인 아키텍처다.
- Business Rules는 Core (변경에 강함)
- GUI / DB는 Plugin (변경이 잦음)
플러그인 구조는 다음과 같은 장점이 있다:
- UI 변경에 영향을 받지 않음
- DB 교체에 유연하게 대응 가능
- 업무 규칙은 독립적으로 테스트 가능
이러한 구조는 같은 이유로의 변경을 기준으로 컴포넌트를 나누는 것이며, 단일 책임 원칙(SRP) 과도 일맥상통한다.
🧩 마무리 정리
소프트웨어 아키텍처에서 경계를 설정하려면 먼저 시스템을 컴포넌트 단위로 나눠야 한다.
- 핵심 컴포넌트: 비즈니스 로직 중심 (Business Rules)
- 플러그인 컴포넌트: 표현(GUI), 저장소(DB)
의존성은 항상 핵심을 향해야 하며, 인터페이스를 기준으로 방향이 바깥에서 안으로 흐르도록 설계되어야 한다.
이는 다음 두 가지 원칙을 따른다:
- DIP (의존성 역전 원칙)
- SAP (안정된 추상화 원칙)
이러한 설계는 시스템을 더 유연하고 견고하게 만든다.
변경이 자주 발생하는 컴포넌트가 시스템의 중심을 침범하지 않도록, 우리는 경계선을 명확히 그어야 한다.
※ 본 글은 『Clean Architecture』(로버트 C. 마틴 저) 5부 17장을 기반으로 학습 목적으로 요약한 글입니다.
※ 이 글은 책의 내용을 요약한 것으로, 원문 없이 읽을 경우 오해의 여지가 있을 수 있습니다. 정확한 이해를 위해 원서의 정독을 권장합니다.
'아키텍처' 카테고리의 다른 글
Clean Architecture 정리 - 5부 20장 아키텍처: 엔티티, 유스케이스 (2) | 2025.07.24 |
---|---|
Clean Architecture 정리 – 5부 19장 아키텍처: 정책과 수준 (0) | 2025.07.23 |
Clean Architecture 정리 – 5부 16장 아키텍처의 독립성 (4) | 2025.07.19 |
Clean Architecture 정리 – 5부 15장 아키텍처 (0) | 2025.07.13 |
Clean Architecture 정리 - 컴포넌트 원칙 (2) (1) | 2025.07.05 |