PART 3 · 강의 1/4

Clean Architecture
핵심 개념

Robert C. Martin의 Clean Architecture - 의존성 규칙과 4개의 원으로 이해하는 깨끗한 아키텍처

01

의존성 규칙 (Dependency Rule)

Clean Architecture의 가장 핵심적인 원칙

외부 레이어
Frameworks
중간 레이어
Adapters
중간 레이어
Use Cases
내부 레이어
Entities
핵심 원칙

"소스 코드의 의존성은 반드시 안쪽 방향으로만 향해야 한다."

외부 레이어는 내부 레이어를 알 수 있지만, 내부 레이어는 외부 레이어에 대해 아무것도 알지 못해야 합니다. 즉, 내부 레이어의 코드에서 외부 레이어의 이름(함수, 클래스, 변수 등)을 언급해서는 안 됩니다.

📌 의존성 규칙이 중요한 이유
  • 변경 격리 — 외부 프레임워크 변경이 비즈니스 로직에 영향을 주지 않음
  • 테스트 용이성 — 핵심 로직을 독립적으로 테스트 가능
  • 유연성 — 데이터베이스, UI 프레임워크 등을 쉽게 교체 가능
  • 이해하기 쉬움 — 의존성 방향이 명확하여 코드 이해가 쉬움
02

4개의 원 (Four Layers)

Clean Architecture의 동심원 구조

Frameworks & Drivers
Web, DB, UI, External Interfaces
Interface Adapters
Controllers, Presenters, Gateways
Use Cases
Application Business Rules
Entities
Enterprise Business Rules

1 Entities (엔티티)

기업 전체의 핵심 비즈니스 규칙을 캡슐화합니다. 가장 일반적이고 높은 수준의 규칙으로, 외부 변화에 가장 영향을 받지 않습니다.

Order Customer Product Invoice

2 Use Cases (유스케이스)

애플리케이션의 비즈니스 규칙을 캡슐화합니다. 시스템의 모든 유스케이스를 구현하며, 엔티티들 간의 데이터 흐름을 조정합니다.

CreateOrder ProcessPayment SendNotification

3 Interface Adapters (인터페이스 어댑터)

데이터를 유스케이스와 엔티티에 가장 편리한 형식에서 외부 에이전시에 가장 편리한 형식으로 변환합니다.

Controller Presenter Repository Impl DTO

4 Frameworks & Drivers (프레임워크 & 드라이버)

가장 외부의 레이어로 데이터베이스, 웹 프레임워크 등 세부 사항으로 구성됩니다. 내부 레이어와 통신하는 접착 코드 외에는 많은 코드를 작성하지 않습니다.

Spring Boot PostgreSQL React Redis
03

실제 코드 비교

의존성 규칙을 지키지 않은 코드 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>;
}
💡 의존성 역전 원칙 (DIP)

Clean Architecture는 의존성 역전 원칙(Dependency Inversion Principle)을 적극 활용합니다. 상위 레이어에서 인터페이스를 정의하고, 하위 레이어에서 구현하면 의존성 방향을 역전시킬 수 있습니다.

04

테스트 용이성

Clean Architecture가 제공하는 테스트 이점

🎯

독립적 단위 테스트

비즈니스 로직을 DB, UI 없이 순수하게 테스트 가능

빠른 테스트 실행

외부 의존성 없이 메모리에서 빠르게 실행

🔄

Mock 용이성

인터페이스 기반으로 쉽게 Mock 객체 주입

🧪
Test
📝
UseCase
🗄️
Real DB
📧
Real Email

⚠️ 테스트가 느리고 외부 서비스에 의존하여 불안정함

🧪
Test
📝
UseCase
🎭
Mock Repo
🎭
Mock Email

✓ 빠르고 안정적인 테스트 - 외부 의존성 없음

테스트 코드 예시
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);
  });
});
Mock Repository 구현
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);
  }
}
SUMMARY

핵심 요약

  • 의존성 규칙 — 소스 코드 의존성은 반드시 안쪽(고수준)을 향해야 한다
  • 4개의 원 — Entities, Use Cases, Interface Adapters, Frameworks & Drivers
  • Entities — 핵심 비즈니스 규칙, 가장 안정적이고 변경이 적음
  • Use Cases — 애플리케이션 비즈니스 규칙, 엔티티들의 데이터 흐름 조정
  • 테스트 용이성 — 인터페이스 기반 설계로 Mock을 통한 빠른 테스트 가능
  • 유연성 — 프레임워크, DB 등 세부사항을 쉽게 교체 가능