Skip to content

토이프로젝트로 시작하는 DDD - 도메인 주도 설계 첫걸음

토이프로젝트로 시작하는 DDD - 도메인 주도 설계 첫걸음




TL;DR

  • 문제: 클린 아키텍처를 적용했지만 “도메인 로직과 애플리케이션 로직의 경계”가 모호해서 Service에 다 몰림
  • 원인: 구조는 좋은데 비즈니스 도메인 정의가 불명확, “주문”이 뭔지 “결제”가 뭔지 명확한 정의 없음
  • 해결: DDD (Domain-Driven Design) - 비즈니스 도메인부터 정의, 유비쿼터스 언어, Aggregate 패턴
  • 효과: 도메인 경계 명확, 비즈니스 로직이 Entity에 응집, 팀 커뮤니케이션 개선
  • 한계: 초기 도메인 모델링 시간 (2주+), 학습 곡선 가파름, 간단한 CRUD엔 과함, 전문가 필요




DDD를 시작하게 된 계기

지난번 토이프로젝트에서 클린 아키텍처를 적용해봤습니다. 레이어를 명확히 분리하니 테스트도 쉬워지고 유지보수성이 확실히 좋아졌거든요.

그런데 한 가지 아쉬운 점이 있었습니다.

“어디까지가 도메인 로직이고, 어디서부터가 애플리케이션 로직인가?”

예를 들어, “주문 금액 계산”은 도메인 로직일까요? “주문 생성 후 알림 발송”은요? 막상 코드를 짜려고 하니 이 경계가 모호했습니다. 결국 Service 클래스에 이것저것 다 넣게 되더군요.

그러다 DDD(Domain-Driven Design, 도메인 주도 설계)를 접하게 됐습니다. 클린 아키텍처가 “구조”에 초점을 맞췄다면, DDD는 “비즈니스 도메인”에 초점을 맞추더군요. “주문”이란 무엇인가부터 시작해서 코드를 설계하는 방식이 신선했습니다.

이번 글에서는 제가 토이프로젝트에 DDD를 적용하면서 느낀 점을 공유합니다. 특히 “처음엔 과하다고 느꼈는데, 왜 결국 DDD가 맞았는지”에 대해 이야기하려고 합니다.





DDD가 뭐길래?

백과사전식 설명은 건너뛰고

DDD는 “비즈니스 도메인을 코드로 옮기는 방법론”입니다. 기술보다 비즈니스를 먼저 생각하라는 거죠.

실무에서는 이런 식으로 고민하게 됩니다.

  • “주문”이란 무엇인가?
  • “결제 완료”와 “배송 중”은 어떻게 다른가?
  • “재고 차감”은 언제 일어나야 하는가?

이런 질문들에 답하면서 코드를 작성하는 게 DDD의 핵심입니다.


기존 방식의 문제 - 빈약한 도메인 모델

대부분의 프로젝트는 이렇게 시작합니다.

// Bad - 빈약한 도메인 모델 (Anemic Domain Model)
public class Order {
    private Long orderId;
    private Long userId;
    private String status; // "PENDING", "PAID", "SHIPPED"
    private BigDecimal amount;

    // getter/setter만 잔뜩...
}

public class OrderService {
    public void processOrder(Long orderId) {
        Order order = orderRepository.findById(orderId);

        // 비즈니스 로직이 전부 Service에 흩어짐
        if ("PENDING".equals(order.getStatus())) {
            order.setStatus("PAID");
            order.setAmount(calculateAmount(order));
            orderRepository.save(order);

            // 재고 차감
            inventoryService.decrease(order.getProductId());

            // 알림 발송
            notificationService.send(order.getUserId());
        }
    }
}

이 코드의 문제점이 뭘까요?

  1. Order는 그냥 데이터 덩어리: getter/setter만 있고 비즈니스 로직은 없음
  2. 비즈니스 규칙이 서비스에 흩어짐: “결제 완료 후 재고 차감” 같은 규칙이 OrderService에만 있음
  3. 상태 변경이 불안정: order.setStatus("PAID")를 누구든 어디서든 호출 가능
  4. 오타에 취약: "PAOD"라고 쳐도 컴파일 에러 안 남

솔직히 저도 예전엔 이렇게 많이 짰습니다. 그때는 “도메인 모델은 DTO랑 비슷한 거 아닌가?”라고 생각했거든요.





DDD로 다시 설계하기

도메인 모델에 비즈니스 로직 담기

DDD에서는 Order 자체가 “주문”이라는 비즈니스 개념을 온전히 표현합니다.

// Good - 풍부한 도메인 모델 (Rich Domain Model)
public class Order {
    private OrderId id;
    private UserId userId;
    private OrderStatus status;
    private Money amount;
    private List<OrderLine> orderLines;

    // 생성자는 private, 정적 팩토리 메서드로 생성
    private Order(UserId userId, List<OrderLine> orderLines) {
        validate(orderLines);
        this.id = OrderId.generate();
        this.userId = userId;
        this.status = OrderStatus.PENDING;
        this.orderLines = orderLines;
        this.amount = calculateTotalAmount();
    }

    public static Order create(UserId userId, List<OrderLine> orderLines) {
        return new Order(userId, orderLines);
    }

    // 비즈니스 로직이 도메인 모델 안에 있음
    public void pay() {
        if (!this.status.isPending()) {
            throw new InvalidOrderStateException("결제 가능한 상태가 아닙니다.");
        }
        this.status = OrderStatus.PAID;
    }

    public void ship() {
        if (!this.status.isPaid()) {
            throw new InvalidOrderStateException("결제 완료 후 배송 가능합니다.");
        }
        this.status = OrderStatus.SHIPPED;
    }

    private Money calculateTotalAmount() {
        return orderLines.stream()
            .map(OrderLine::getAmount)
            .reduce(Money.ZERO, Money::add);
    }

    private void validate(List<OrderLine> orderLines) {
        if (orderLines == null || orderLines.isEmpty()) {
            throw new EmptyOrderException("주문 항목이 비어있습니다.");
        }
    }
}

이제 Order는 단순 데이터가 아닙니다. “주문”이라는 비즈니스 개념을 온전히 표현하고 있습니다. order.pay()를 호출하면 결제가 되고, order.ship()을 호출하면 배송이 시작됩니다.

핵심 변화:

  • setStatus()pay(), ship() (의도가 명확한 메서드)
  • 상태 검증 로직이 도메인 안에 캡슐화
  • 외부에서 함부로 상태 변경 불가

값 객체(Value Object)로 도메인 개념 명확히 하기

String status 대신 OrderStatus를 사용했습니다. 이게 값 객체(Value Object)입니다.

public enum OrderStatus {
    PENDING("결제 대기"),
    PAID("결제 완료"),
    SHIPPED("배송 중"),
    DELIVERED("배송 완료"),
    CANCELLED("취소됨");

    private final String description;

    OrderStatus(String description) {
        this.description = description;
    }

    public boolean isPending() {
        return this == PENDING;
    }

    public boolean isPaid() {
        return this == PAID;
    }

    public boolean canCancel() {
        return this == PENDING || this == PAID;
    }
}
public class Money {
    private final BigDecimal amount;

    public static final Money ZERO = new Money(BigDecimal.ZERO);

    private Money(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new InvalidMoneyException("금액은 0 이상이어야 합니다.");
        }
        this.amount = amount;
    }

    public static Money of(BigDecimal amount) {
        return new Money(amount);
    }

    public Money add(Money other) {
        return new Money(this.amount.add(other.amount));
    }

    public Money multiply(int quantity) {
        return new Money(this.amount.multiply(BigDecimal.valueOf(quantity)));
    }
}

이제 order.setStatus("PAOD")처럼 오타를 낼 일이 없습니다. 컴파일 타임에 잡히거든요. 그리고 Money는 항상 0 이상이라는 불변식이 보장됩니다.





토이프로젝트 패키지 구조

제가 적용한 패키지 구조는 이렇습니다.

src/main/java/com/example/shop
├── domain
│   ├── order
│   │   ├── Order.java           // 주문 Aggregate Root
│   │   ├── OrderLine.java       // 주문 항목 Entity
│   │   ├── OrderId.java         // 주문 ID Value Object
│   │   ├── OrderStatus.java     // 주문 상태 Value Object
│   │   ├── Money.java           // 금액 Value Object
│   │   ├── OrderRepository.java // Repository Interface
│   │   └── OrderService.java    // Domain Service (필요 시)
│   ├── product
│   │   ├── Product.java
│   │   ├── ProductId.java
│   │   └── ProductRepository.java
│   └── user
│       ├── User.java
│       ├── UserId.java
│       └── UserRepository.java
├── application
│   └── order
│       ├── OrderUseCase.java    // Use Case
│       ├── dto
│       │   ├── CreateOrderRequest.java
│       │   └── OrderResponse.java
│       └── event
│           └── OrderPaidEvent.java
└── infrastructure
    ├── persistence
    │   ├── OrderJpaRepository.java
    │   └── OrderRepositoryImpl.java
    └── event
        └── OrderEventPublisher.java

graph TB
    subgraph Application Layer
        UC[OrderUseCase]
        DTO[DTO]
        EVT[Event]
    end

    subgraph Domain Layer
        AGG[Order<br/>Aggregate Root]
        VO[Value Objects<br/>Money, OrderStatus]
        REPO[OrderRepository<br/>Interface]
    end

    subgraph Infrastructure Layer
        JPA[JPA Repository]
        IMPL[Repository Impl]
    end

    UC --> AGG
    UC --> REPO
    IMPL --> REPO
    IMPL --> JPA
    AGG --> VO

    style AGG fill:#e8f5e9
    style VO fill:#fff3e0
    style REPO fill:#e3f2fd

핵심은 domain 폴더

비즈니스 로직은 모두 domain 폴더에 있습니다. JPA, Spring 같은 프레임워크와 무관하게 순수 자바로 작성됩니다.

// domain 폴더에는 javax/jakarta, Spring 의존성이 없음
public interface OrderRepository {
    Order save(Order order);
    Optional<Order> findById(OrderId id);
}

실제 JPA 구현은 infrastructure에 숨깁니다.

// infrastructure 폴더에서 JPA 구현
@Repository
public class OrderRepositoryImpl implements OrderRepository {
    private final OrderJpaRepository jpaRepository;

    @Override
    public Order save(Order order) {
        OrderJpaEntity entity = OrderMapper.toEntity(order);
        OrderJpaEntity saved = jpaRepository.save(entity);
        return OrderMapper.toDomain(saved);
    }
}

이렇게 하면 도메인 모델이 JPA에 종속되지 않습니다. 나중에 MongoDB로 바꾸더라도 domain 폴더는 손대지 않아도 됩니다.





Aggregate와 경계 설정

Aggregate가 뭐죠?

Aggregate는 “함께 변경되는 객체들의 묶음”입니다. 처음엔 이게 뭔 소린가 싶었는데, 예를 들어 보면 바로 이해됩니다.

주문(Order)과 주문 항목(OrderLine)은 항상 함께 다룹니다. 주문 없이 주문 항목만 존재할 수 없거든요.

public class Order { // Aggregate Root
    private OrderId id;
    private List<OrderLine> orderLines; // Order의 일부

    // OrderLine은 Order를 통해서만 접근
    public void addOrderLine(ProductId productId, int quantity, Money price) {
        OrderLine line = new OrderLine(productId, quantity, price);
        this.orderLines.add(line);
        this.amount = calculateTotalAmount(); // 금액 재계산
    }

    public void removeOrderLine(int index) {
        if (this.status != OrderStatus.PENDING) {
            throw new InvalidOrderStateException("결제 전에만 항목 삭제 가능합니다.");
        }
        this.orderLines.remove(index);
        this.amount = calculateTotalAmount(); // 금액 재계산
    }
}

graph TB
    subgraph "Order Aggregate"
        ORDER[Order<br/>Aggregate Root]
        LINE1[OrderLine 1]
        LINE2[OrderLine 2]
        LINE3[OrderLine N]
    end

    subgraph "다른 Aggregate 참조"
        USERID[UserId<br/>ID 참조]
        PRODUCTID[ProductId<br/>ID 참조]
    end

    ORDER --> LINE1
    ORDER --> LINE2
    ORDER --> LINE3
    ORDER -.-> USERID
    LINE1 -.-> PRODUCTID

    style ORDER fill:#e8f5e9
    style LINE1 fill:#fff3e0
    style LINE2 fill:#fff3e0
    style LINE3 fill:#fff3e0
    style USERID fill:#e3f2fd
    style PRODUCTID fill:#e3f2fd

Aggregate Root인 Order만 Repository를 통해 저장/조회합니다. OrderLine은 직접 저장하지 않습니다.

// Good
Order order = orderRepository.findById(orderId);
order.addOrderLine(productId, quantity, price);
orderRepository.save(order); // OrderLine도 함께 저장됨

// Bad - OrderLine을 직접 저장하려고 하면 안 됨
OrderLine line = new OrderLine(...);
orderLineRepository.save(line); // 이런 Repository는 만들지 않음

경계를 어떻게 정할까?

저는 이런 기준으로 정했습니다.

1. 트랜잭션 경계

“이 둘은 항상 함께 저장되어야 하는가?”

  • Order와 OrderLine → 같은 Aggregate
  • Order와 User → 다른 Aggregate

주문을 저장할 때 사용자 정보까지 변경할 필요는 없습니다.

2. 불변식(Invariant) 유지

“이 규칙은 항상 지켜져야 하는가?”

public class Order {
    // 불변식: 주문 항목이 비어있으면 안 됨
    private void validate() {
        if (orderLines.isEmpty()) {
            throw new EmptyOrderException();
        }
    }

    // 불변식: 총 금액은 주문 항목의 합계와 일치해야 함
    public Money getTotalAmount() {
        Money calculated = orderLines.stream()
            .map(OrderLine::getAmount)
            .reduce(Money.ZERO, Money::add);

        if (!this.amount.equals(calculated)) {
            throw new InconsistentAmountException();
        }
        return this.amount;
    }
}

이런 불변식을 지켜야 할 객체들은 같은 Aggregate에 묶습니다.


주의: 너무 큰 Aggregate는 성능 문제

처음엔 Order에 User, Product까지 다 넣고 싶었습니다. 하지만 그러면 주문 하나 조회할 때 User, Product까지 다 가져와야 합니다.

Aggregate는 최소 단위로 유지하는 게 좋습니다. 참조는 ID로만 하고, 필요할 때 Repository로 조회하세요.

public class Order {
    private UserId userId; // User 객체가 아닌 ID만 참조

    // 필요할 때만 조회
    public User getUser(UserRepository userRepository) {
        return userRepository.findById(this.userId)
            .orElseThrow();
    }
}




실제 Use Case 구현

Application Layer에서 흐름 조율

Application Layer의 UseCase는 비즈니스 로직을 호출하는 역할만 합니다. 복잡한 로직은 도메인에 위임합니다.

@Service
@RequiredArgsConstructor
public class OrderUseCase {
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    private final ApplicationEventPublisher eventPublisher;

    @Transactional
    public OrderResponse createOrder(CreateOrderRequest request) {
        // 1. 상품 조회 (도메인 검증)
        List<Product> products = productRepository.findAllById(request.getProductIds());

        // 2. 주문 생성 (도메인 로직 호출)
        List<OrderLine> orderLines = products.stream()
            .map(p -> OrderLine.of(p.getId(), request.getQuantity(p.getId()), p.getPrice()))
            .toList();

        Order order = Order.create(request.getUserId(), orderLines);

        // 3. 저장
        Order saved = orderRepository.save(order);

        // 4. 이벤트 발행 (부가 기능)
        eventPublisher.publishEvent(new OrderCreatedEvent(saved.getId()));

        return OrderResponse.from(saved);
    }

    @Transactional
    public void payOrder(OrderId orderId) {
        // 1. 조회
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));

        // 2. 결제 (도메인 로직 - 상태 검증도 여기서)
        order.pay();

        // 3. 저장
        orderRepository.save(order);

        // 4. 이벤트 발행
        eventPublisher.publishEvent(new OrderPaidEvent(orderId));
    }
}

보시다시피 UseCase는 간결합니다. order.pay()한 줄에 결제 로직이 다 들어있거든요. 상태 검증도 Order 안에서 처리됩니다.


도메인 이벤트로 부가 기능 분리

결제 완료 후 “재고 차감”, “알림 발송” 같은 부가 기능은 이벤트로 처리합니다.

@Component
@RequiredArgsConstructor
public class OrderEventHandler {
    private final InventoryService inventoryService;
    private final NotificationService notificationService;

    @EventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handleOrderPaid(OrderPaidEvent event) {
        // 재고 차감
        inventoryService.decrease(event.getOrderId());
    }

    @EventListener
    @Async
    public void sendPaymentNotification(OrderPaidEvent event) {
        // 알림 발송 (비동기)
        notificationService.sendOrderPaidNotification(event.getOrderId());
    }
}

이제 OrderUseCase는 “결제”라는 핵심 로직에만 집중합니다. 재고 차감이나 알림 발송은 이벤트 핸들러가 알아서 처리하죠.





토이프로젝트에 적용하며 느낀 점

1. 처음엔 확실히 과해 보였다

솔직히 토이프로젝트에 이렇게까지 해야 하나 싶었습니다. OrderId, Money 같은 값 객체를 일일이 만드는 게 번거롭더군요. “그냥 Long이랑 BigDecimal 쓰면 안 되나?”라는 생각도 들었고요.

하지만 조금씩 코드가 쌓이니까 장점이 보였습니다.

// Before - String으로 주문 ID 관리
public Order findOrder(String orderId) {
    // orderId가 userId인지, productId인지 헷갈림
    // 실수로 userId를 넘겨도 컴파일 에러 안 남
}

// After - 타입으로 명확히 구분
public Order findOrder(OrderId orderId) {
    // 컴파일러가 타입 체크해줌
    // UserId를 넘기면 컴파일 에러
}

2. 테스트가 정말 쉬워졌다

도메인 모델은 순수 자바라서 테스트가 간단합니다. DB도, Spring도 필요 없습니다.

@Test
void 결제_완료_상태에서만_배송_가능() {
    // given
    Order order = Order.create(userId, orderLines);

    // when & then - 결제 전에는 배송 불가
    assertThatThrownBy(() -> order.ship())
        .isInstanceOf(InvalidOrderStateException.class)
        .hasMessage("결제 완료 후 배송 가능합니다.");

    // 결제 처리
    order.pay();

    // 이제 배송 가능
    assertThatCode(() -> order.ship())
        .doesNotThrowAnyException();
}

순수하게 비즈니스 로직만 테스트할 수 있거든요. Mock도 거의 필요 없습니다.


3. 리팩토링이 두렵지 않았다

JPA Entity를 바꾸려면 Repository, Service, Controller까지 다 고쳐야 합니다. 하지만 DDD에서는 도메인 모델과 JPA Entity가 분리돼 있습니다.

// JPA Entity 변경해도 도메인 모델은 영향 없음
@Entity
@Table(name = "orders")
class OrderJpaEntity {
    @Id
    private Long id; // DB에는 Long으로 저장

    @Column(name = "user_id")
    private Long userId;

    // ... JPA 전용 필드
}

Mapper만 조금 수정하면 됩니다. 도메인 로직 테스트는 그대로 통과합니다.


4. 경계 설정이 가장 어려웠다

“User와 Order는 같은 Aggregate인가?” “Product는 Order에 포함시켜야 하나?”

정답이 없어서 고민이 많았습니다. 결국 제가 내린 기준은 이겁니다.

“트랜잭션 단위와 불변식을 지켜야 할 범위”

  • Order와 OrderLine → 함께 저장, 불변식 공유 → 같은 Aggregate
  • Order와 User → 별도 저장, 독립적 변경 → 다른 Aggregate

처음엔 틀릴 수도 있습니다. 저도 Aggregate 경계를 잘못 나눴다가 나중에 합친 적이 있습니다. 토이프로젝트니까 실수해도 괜찮습니다.





DDD, 언제 쓰면 좋을까?

추천하는 경우

1. 비즈니스 규칙이 복잡할 때

“주문은 결제 완료 후에만 배송 가능” “재고가 부족하면 주문 불가” “프로모션 적용 시 10% 할인, 단 최대 할인 금액 제한”

이런 규칙들이 쌓일수록 DDD가 빛을 발합니다. Service에 if-else가 난무하는 것보다 도메인 모델에 규칙을 담는 게 훨씬 관리하기 좋습니다.

2. 도메인 지식이 중요할 때

회계, 물류, 의료 시스템처럼 도메인 전문 지식이 필요한 경우, 도메인 모델에 그 지식을 녹여내는 게 유리합니다. 개발자가 아닌 도메인 전문가와 대화할 때도 같은 언어(유비쿼터스 언어)를 쓸 수 있거든요.

3. 장기 운영 프로젝트

토이프로젝트라도 계속 확장할 계획이라면 처음부터 DDD로 시작하는 게 좋습니다. 나중에 리팩토링하기가 훨씬 어렵거든요.


굳이 안 써도 되는 경우

1. CRUD 위주의 단순한 시스템

게시판, 관리자 페이지처럼 단순한 CRUD라면 DDD는 과합니다. Spring Data JPA + Service 레이어로 충분합니다.

2. 빠른 프로토타이핑

일단 빨리 만들어서 검증해야 한다면 DDD는 오버엔지니어링입니다. MVP부터 만들고, 비즈니스가 복잡해지면 그때 적용해도 늦지 않습니다.

3. 팀원 학습 곡선

DDD는 개념 자체가 어렵습니다. 팀원 모두가 이해하고 있지 않으면 오히려 복잡도만 올라갑니다. 저도 처음엔 혼자 공부하면서 토이프로젝트에 적용해봤고, 어느 정도 익숙해진 후에야 팀에 제안할 수 있었습니다.





시스템 점검 체크리스트

저도 DDD를 적용할 때 이 항목들을 확인합니다. 도메인 주도 설계를 고려한다면 참고하시면 좋을 것 같습니다.

  • 도메인 모델링 완료: 비즈니스 규칙을 도메인 엔티티/Value Object로 표현했는가?
  • 유비쿼터스 언어: 팀 전체가 동일한 용어를 사용하는가? (코드와 기획 문서의 용어가 일치하는가)
  • Aggregate 경계: Aggregate Root를 통해서만 내부 엔티티에 접근하는가?
  • 불변 Value Object: 값 객체가 불변으로 설계되었는가? (setter 없음)
  • 도메인 이벤트: 상태 변경 시 도메인 이벤트를 발행하는가?



결론

DDD를 토이프로젝트에 적용하면서 많이 배웠습니다.

처음엔 “이거 너무 복잡한 거 아닌가?” 싶었는데, 결국 핵심은 “비즈니스를 코드로 정직하게 옮기자”는 거더군요.

orderService.updateStatus("PAID")보다 order.pay()가 훨씬 명확합니다. 코드를 읽으면 비즈니스가 보이거든요.


저는 이 과정을 통해 한 가지 깨달았습니다.

좋은 코드는 “어떻게”보다 “무엇을”에 집중합니다.

DDD는 구현 기술이 아니라 사고방식입니다. “이 비즈니스를 어떻게 코드로 표현할까?”를 끊임없이 고민하게 만들죠.


그리고 솔직히 말해서, 토이프로젝트였기 때문에 마음껏 실수할 수 있었습니다.

Aggregate 경계를 잘못 나눴다가 다시 합치기도 했고, 값 객체를 너무 많이 만들었다가 줄이기도 했습니다. 도메인 이벤트를 어디서 발행해야 하는지 한참 헷갈리기도 했고요.

실무에 바로 적용하기 전에 토이프로젝트로 연습하는 게 정답이었습니다.

DDD 책만 읽으면 “이론은 알겠는데 어떻게 적용하지?”라는 생각이 들거든요. 직접 코드를 짜봐야 감이 옵니다.

다음 토이프로젝트에서는 헥사고날 아키텍처와 DDD를 조합해볼 생각입니다. 둘이 잘 어울린다고 하더군요.





참고 :

https://martinfowler.com/bliki/DomainDrivenDesign.html
https://martinfowler.com/bliki/AnemicDomainModel.html
https://www.baeldung.com/hexagonal-architecture-ddd-spring
Eric Evans - “Domain-Driven Design” (Blue Book)
Vaughn Vernon - “Implementing Domain-Driven Design” (Red Book)
최범균 - “DDD START!”




읽어주셔서 감사합니다.🖐


Ramsbaby
Written byRamsbaby
이 블로그는 직접 개발/운영하는 블로그이므로 당신을 불쾌하게 만드는 불필요한 광고가 없습니다.

#My Github#소개 페이지#Blog OpenSource Github#Blog OpenSource Demo Site