Clean Architecture
핵심 개념
Robert C. Martin의 Clean Architecture - 의존성 규칙과 4개의 원으로 이해하는 깨끗한 아키텍처
의존성 규칙 (Dependency Rule)
Clean Architecture의 가장 핵심적인 원칙
Frameworks
Adapters
Use Cases
Entities
"소스 코드의 의존성은 반드시 안쪽 방향으로만 향해야 한다."
외부 레이어는 내부 레이어를 알 수 있지만, 내부 레이어는 외부 레이어에 대해 아무것도 알지 못해야 합니다.
즉, 내부 레이어의 코드에서 외부 레이어의 이름(함수, 클래스, 변수 등)을 언급해서는 안 됩니다.
- 변경 격리 — 외부 프레임워크 변경이 비즈니스 로직에 영향을 주지 않음
- 테스트 용이성 — 핵심 로직을 독립적으로 테스트 가능
- 유연성 — 데이터베이스, UI 프레임워크 등을 쉽게 교체 가능
- 이해하기 쉬움 — 의존성 방향이 명확하여 코드 이해가 쉬움
4개의 원 (Four Layers)
Clean Architecture의 동심원 구조
1 Entities (엔티티)
기업 전체의 핵심 비즈니스 규칙을 캡슐화합니다. 가장 일반적이고 높은 수준의 규칙으로, 외부 변화에 가장 영향을 받지 않습니다.
2 Use Cases (유스케이스)
애플리케이션의 비즈니스 규칙을 캡슐화합니다. 시스템의 모든 유스케이스를 구현하며, 엔티티들 간의 데이터 흐름을 조정합니다.
3 Interface Adapters (인터페이스 어댑터)
데이터를 유스케이스와 엔티티에 가장 편리한 형식에서 외부 에이전시에 가장 편리한 형식으로 변환합니다.
4 Frameworks & Drivers (프레임워크 & 드라이버)
가장 외부의 레이어로 데이터베이스, 웹 프레임워크 등 세부 사항으로 구성됩니다. 내부 레이어와 통신하는 접착 코드 외에는 많은 코드를 작성하지 않습니다.
실제 코드 비교
의존성 규칙을 지키지 않은 코드 vs 지킨 코드
// UseCase가 Framework에 직접 의존
class CreateOrderUseCase {
constructor(
private db: PostgreSQLDatabase, // Framework 직접 의존!
private emailService: SendGrid // 외부 서비스 직접 의존!
) {}
async execute(orderData: OrderDTO) {
// SQL 직접 작성
await this.db.query(
'INSERT INTO orders...'
);
// 외부 서비스 직접 호출
await this.emailService.send(...);
}
}
// UseCase는 추상화에 의존
class CreateOrderUseCase {
constructor(
private orderRepo: OrderRepository, // 인터페이스
private notifier: NotificationService // 인터페이스
) {}
async execute(command: CreateOrderCommand) {
const order = Order.create(command);
await this.orderRepo.save(order);
await this.notifier.notify(order);
return order;
}
}
// Interface (Use Cases 레이어에 정의)
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: OrderId): Promise<Order>;
}
Clean Architecture는 의존성 역전 원칙(Dependency Inversion Principle)을 적극 활용합니다. 상위 레이어에서 인터페이스를 정의하고, 하위 레이어에서 구현하면 의존성 방향을 역전시킬 수 있습니다.
테스트 용이성
Clean Architecture가 제공하는 테스트 이점
독립적 단위 테스트
비즈니스 로직을 DB, UI 없이 순수하게 테스트 가능
빠른 테스트 실행
외부 의존성 없이 메모리에서 빠르게 실행
Mock 용이성
인터페이스 기반으로 쉽게 Mock 객체 주입
⚠️ 테스트가 느리고 외부 서비스에 의존하여 불안정함
✓ 빠르고 안정적인 테스트 - 외부 의존성 없음
describe('CreateOrderUseCase', () => {
let useCase: CreateOrderUseCase;
let mockOrderRepo: MockOrderRepository;
let mockNotifier: MockNotificationService;
beforeEach(() => {
mockOrderRepo = new MockOrderRepository();
mockNotifier = new MockNotificationService();
useCase = new CreateOrderUseCase(
mockOrderRepo,
mockNotifier
);
});
it('should create order successfully', async () => {
const command = new CreateOrderCommand(...);
const result = await useCase.execute(command);
expect(result.isSuccess).toBe(true);
expect(mockOrderRepo.savedOrders).toHaveLength(1);
expect(mockNotifier.sentNotifications).toHaveLength(1);
});
});
class MockOrderRepository implements OrderRepository {
savedOrders: Order[] = [];
ordersToReturn: Map<string, Order> = new Map();
async save(order: Order): Promise<void> {
this.savedOrders.push(order);
}
async findById(id: OrderId): Promise<Order | null> {
return this.ordersToReturn.get(id.value) || null;
}
// 테스트 헬퍼 메서드
givenOrderExists(order: Order) {
this.ordersToReturn.set(order.id.value, order);
}
}
핵심 요약
- 의존성 규칙 — 소스 코드 의존성은 반드시 안쪽(고수준)을 향해야 한다
- 4개의 원 — Entities, Use Cases, Interface Adapters, Frameworks & Drivers
- Entities — 핵심 비즈니스 규칙, 가장 안정적이고 변경이 적음
- Use Cases — 애플리케이션 비즈니스 규칙, 엔티티들의 데이터 흐름 조정
- 테스트 용이성 — 인터페이스 기반 설계로 Mock을 통한 빠른 테스트 가능
- 유연성 — 프레임워크, DB 등 세부사항을 쉽게 교체 가능