소개: 모듈형 모놀리스의 데이터베이스
데이터베이스 설계는 모듈식 모놀리스에서 가장 중요한 아키텍처 결정 중 하나입니다. 각 서비스에 자체 데이터베이스가 있는 마이크로서비스와 달리 모듈식 모놀리스 i에서는 모듈은 동일한 프로세스와 잠재적으로 동일한 물리적 데이터베이스를 공유합니다. 도전은 유지하다 데이터 소유권 데이터베이스를 활용하면서 모듈당 모듈 간 ACID 트랜잭션과 같은 공유.
이 기사에서는 두 가지 주요 접근 방식을 살펴보겠습니다. 공유 스키마 e 모듈 다이어그램, 각각의 절충안이 있습니다. 경영 패턴을 살펴보겠습니다. 트랜잭션의 최종 일관성, 계획에서 점진적으로 마이그레이션하기 위한 전략 별도의 계획으로 공유됩니다.
이 기사에서 배울 내용
- 공유 스키마와 모듈별 스키마: 장점과 단점
- 데이터 소유권: 국경 무결성 유지를 위한 규칙
- 프로세스 내 ACID 트랜잭션과 최종 일관성 비교
- 장기 실행 트랜잭션을 위한 Saga 패턴
- 일관성을 위한 대체 모델로서의 이벤트 소싱
- 모듈 간 데이터를 동기화하기 위해 데이터 캡처 변경
- 마이그레이션 전략: 공유에서 분리로
- 성능: 컨텍스트 간 쿼리 최적화
공유 스키마: 하나의 데이터베이스, 별도의 논리적 스키마
접근 방식에서는 공유 스키마, 모든 모듈은 동일한 데이터베이스를 공유합니다. 물리적이고 잠재적으로 동일한 패턴입니다. 그러나 각 모듈에는 다음이 포함된 자체 테이블이 있습니다. 접두사 또는 전용 PostgreSQL 스키마. 데이터에 대한 액세스는 애플리케이션 수준에서 규제됩니다. 각 모듈은 자체 저장소를 통해서만 자체 테이블에 액세스합니다.
공유 스키마의 장점
- ACID 거래: 단일 트랜잭션으로 모듈 간 일관성을 보장할 수 있습니다.
- 운영 단순성: 관리, 모니터링, 백업을 위한 단일 데이터베이스
- 모듈 간 쿼리: 필요시 JOIN으로 가능 (보고, 분석용)
- 단순 마이그레이션: 추가적인 인프라가 필요하지 않습니다.
공유 스키마의 단점
- 데이터 계층 결합: 다른 모듈의 테이블에 직접 접근할 위험이 있음
- 제한된 확장: 데이터베이스는 단일 확장 지점입니다.
- 진화 계획: 마이그레이션은 모든 모듈에 영향을 미칠 수 있습니다.
-- Shared schema con prefissi per modulo
-- Ogni modulo ha il proprio prefisso nelle tabelle
-- Modulo Order
CREATE TABLE order_orders (
id UUID PRIMARY KEY,
user_id UUID NOT NULL, -- riferimento, non FK esterna
status VARCHAR(20) NOT NULL,
total_amount DECIMAL(10,2),
total_currency VARCHAR(3),
created_at TIMESTAMP NOT NULL
);
CREATE TABLE order_items (
id UUID PRIMARY KEY,
order_id UUID REFERENCES order_orders(id),
product_id UUID NOT NULL, -- riferimento, non FK esterna
quantity INT NOT NULL,
unit_price DECIMAL(10,2)
);
-- Modulo Catalog
CREATE TABLE catalog_products (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL,
category_id UUID,
is_available BOOLEAN DEFAULT true
);
-- Modulo User
CREATE TABLE user_accounts (
id UUID PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
display_name VARCHAR(100),
created_at TIMESTAMP NOT NULL
);
-- NOTA: order_orders.user_id NON ha una FK verso user_accounts
-- Questo e intenzionale: i moduli comunicano via API, non via FK
데이터베이스 황금률
생성하지 않음 서로 다른 모듈의 테이블 사이의 외래 키. 모듈 간 참조 ID(UUID)만 사용합니다. 모듈 간의 참조 일관성은 애플리케이션 수준에서 관리됩니다. 데이터베이스 수준이 아닙니다. 이것이 가능한 추출을 가능하게 하는 절충안이다 마이크로서비스로서의 모듈.
모듈 다이어그램: 완전한 절연
접근 방식에서는 모듈 다이어그램, 각 모듈에는 자체 PostgreSQL 스키마가 있습니다. (또는 자신의 논리적 데이터베이스). 이는 데이터 계층에서 완전한 격리를 제공하지만 모듈 간의 일관성을 관리하기 위한 특정 패턴.
// Configurazione multi-schema in Spring Boot
// Ogni modulo ha il proprio schema PostgreSQL
// application.yml
// spring:
// datasource:
// url: jdbc:postgresql://localhost:5432/ecommerce
// Modulo Order: accede solo allo schema 'orders'
@Configuration
class OrderDatabaseConfig {
@Bean
public LocalContainerEntityManagerFactoryBean orderEntityManager(
DataSource dataSource) {
LocalContainerEntityManagerFactoryBean em =
new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("com.ecommerce.order.internal");
Map<String, Object> props = new HashMap<>();
props.put(
"hibernate.default_schema", "orders"
);
em.setJpaPropertyMap(props);
return em;
}
}
// Modulo Catalog: accede solo allo schema 'catalog'
@Configuration
class CatalogDatabaseConfig {
@Bean
public LocalContainerEntityManagerFactoryBean catalogEntityManager(
DataSource dataSource) {
LocalContainerEntityManagerFactoryBean em =
new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("com.ecommerce.catalog.internal");
Map<String, Object> props = new HashMap<>();
props.put(
"hibernate.default_schema", "catalog"
);
em.setJpaPropertyMap(props);
return em;
}
}
거래 관리
모듈형 모놀리스에서는 거래가 상황에 따라 다양한 수준에서 관리될 수 있습니다. 일관성 요구사항:
모듈 내 ACID 트랜잭션
단일 모듈 내에서 ACID 트랜잭션은 정상적으로 작동합니다. 싱글
주석 @Transactional 원자성, 일관성, 절연성 및 내구성을 보장합니다.
모든 모듈 작업에 사용됩니다.
최종 일관성 교차 모듈
서로 다른 모듈 간에 일관성이 관리됩니다. 도메인 이벤트. 모듈이 트랜잭션을 완료하면 이벤트를 게시합니다. 다른 모듈은 반응합니다 별도의 거래를 통해 해당 이벤트에 대한 보장 최종 일관성.
// Pattern: Transactional Outbox per garantire
// la pubblicazione affidabile degli eventi
@Entity
@Table(name = "order_outbox_events")
public class OutboxEvent {
@Id
private UUID id;
private String eventType;
private String payload; // JSON serializzato
private Instant createdAt;
private boolean processed;
}
@Service
class OrderServiceImpl implements OrderModuleApi {
@Transactional
public OrderDto createOrder(CreateOrderCommand cmd) {
// 1. Salva l'ordine
Order order = Order.create(cmd);
orderRepository.save(order);
// 2. Salva l'evento nella outbox table
// NELLA STESSA TRANSAZIONE dell'ordine
OutboxEvent event = new OutboxEvent(
UUID.randomUUID(),
"OrderCreated",
JsonUtil.toJson(new OrderCreatedEvent(
order.getId(), order.getUserId()
)),
Instant.now(),
false
);
outboxRepository.save(event);
return order.toDto();
// La transazione include sia l'ordine che l'evento
}
}
// Scheduler che processa gli eventi dalla outbox
@Scheduled(fixedDelay = 1000)
public void processOutboxEvents() {
List<OutboxEvent> events = outboxRepo.findUnprocessed();
for (OutboxEvent event : events) {
eventPublisher.publish(event.toEvent());
event.markProcessed();
outboxRepo.save(event);
}
}
사가 패턴: 장기 실행 트랜잭션
Il 사가 패턴 여러 모듈을 교차하는 트랜잭션을 시퀀스로 처리합니다. 각각 고유한 로컬 거래 보상 실패의 경우. 단계가 실패하면 보상 작업을 통해 이전 단계가 취소됩니다.
// Saga: Creazione Ordine con compensazione
// Orchestration-based Saga
@Service
class CreateOrderSaga {
private final OrderModuleApi orderModule;
private final PaymentModuleApi paymentModule;
private final InventoryModuleApi inventoryModule;
public OrderDto execute(CreateOrderCommand cmd) {
OrderDto order = null;
PaymentDto payment = null;
try {
// Step 1: Crea ordine (stato PENDING)
order = orderModule.createOrder(cmd);
// Step 2: Riserva inventario
inventoryModule.reserveStock(
order.id(), order.items()
);
// Step 3: Processa pagamento
payment = paymentModule.processPayment(
order.userId(), order.total()
);
// Step 4: Conferma ordine
orderModule.confirmOrder(order.id());
return order;
} catch (PaymentFailedException e) {
// Compensazione: rilascia inventario
inventoryModule.releaseStock(order.id());
// Compensazione: annulla ordine
orderModule.cancelOrder(order.id());
throw new OrderCreationFailedException(e);
} catch (InsufficientStockException e) {
// Compensazione: annulla ordine
orderModule.cancelOrder(order.id());
throw new OrderCreationFailedException(e);
}
}
}
이벤트 소싱: 대안 모델
L'이벤트 소싱 엔터티의 상태를 재구성하는 패턴입니다. 그것을 변화시킨 일련의 사건들에 의해. 현재 상태를 저장하는 대신 자신을 저장합니다. 모든 이벤트. 이 접근 방식은 다음을 제공합니다.
- 완전한 감사 추적: 모든 변경 사항이 이벤트로 기록됩니다.
- 국가 재건: 언제든지 상태를 재구성할 수 있는 가능성
- 자연스러운 통합: 모듈 간 통신을 위한 이벤트가 이미 사용 가능합니다.
- 복잡성: 효율적인 쿼리를 위해 추가 패턴(CQRS, 프로젝션, 스냅샷)이 필요합니다.
// Event Sourcing: l'ordine e ricostruito dagli eventi
public class OrderAggregate {
private UUID id;
private OrderStatus status;
private List<OrderItem> items;
private Money total;
// Ricostruisci lo stato dagli eventi
public static OrderAggregate rebuild(List<DomainEvent> events) {
OrderAggregate order = new OrderAggregate();
for (DomainEvent event : events) {
order.apply(event);
}
return order;
}
private void apply(DomainEvent event) {
if (event instanceof OrderCreated e) {
this.id = e.orderId();
this.status = OrderStatus.CREATED;
this.items = e.items();
} else if (event instanceof ItemAdded e) {
this.items.add(e.item());
this.recalculateTotal();
} else if (event instanceof OrderConfirmed e) {
this.status = OrderStatus.CONFIRMED;
} else if (event instanceof OrderCancelled e) {
this.status = OrderStatus.CANCELLED;
}
}
}
// Event Store: salva la sequenza di eventi
@Repository
class EventStore {
void append(UUID aggregateId, DomainEvent event);
List<DomainEvent> loadEvents(UUID aggregateId);
}
변경 데이터 캡처(CDC)
Il 변경 데이터 캡처 데이터베이스 변경 사항을 캡처하는 기술과 다른 소비자에게 이벤트로 전파됩니다. 다음과 같은 도구 데베지움 그들은 읽습니다 데이터베이스 트랜잭션 로그를 작성하고 각 INSERT, UPDATE 및 DELETE에 대한 이벤트를 생성합니다.
모듈식 모놀리스의 맥락에서 CDC는 다음과 같은 용도로 유용합니다.
- 구체화된 뷰 동기화 직접 결합 없이 모듈 간
- 피드 읽기 모델 예측 효율적인 교차 모듈 쿼리를 위해
- 마이그레이션 준비: 모듈이 마이크로서비스로 체크아웃되면 CDC는 전환 중에 데이터를 동기화할 수 있습니다.
성능: 컨텍스트 간 쿼리
모듈식 모놀리스의 과제 중 하나는 여러 모듈에서 데이터를 요청하는 쿼리를 관리하는 것입니다. 주요 전략은 다음과 같습니다.
1. API 구성
소비자는 여러 모듈의 API를 호출하고 결과를 메모리에 구성합니다. 간단하지만 대용량 데이터에는 비효율적일 수 있습니다.
2. 모델/구체화된 뷰 읽기
보고 모듈은 다음을 유지합니다. 구체화된 뷰 다음에서 데이터를 집계합니다. 더 많은 모듈. 보기는 도메인 이벤트를 통해 업데이트됩니다. 읽기 쿼리가 빠릅니다. 왜냐하면 데이터가 이미 사전에 집계되어 있기 때문입니다.
3. CQRS(명령 쿼리 책임 분리)
쓰기 모델(트랜잭션에 최적화됨)과 읽기 모델(최적화됨)을 분리합니다. 쿼리당). 명령은 모듈 API를 통해 전달됩니다. 쿼리 액세스 예측 이벤트를 통해 비정규화 업데이트되었습니다.
// CQRS: Read Model per dashboard ordini
// Aggiornato tramite eventi, denormalizzato per query veloci
@Entity
@Table(name = "reporting_order_summary")
public class OrderSummaryView {
@Id
private UUID orderId;
private String customerName; // dal modulo User
private String customerEmail; // dal modulo User
private int itemCount; // dal modulo Order
private BigDecimal totalAmount; // dal modulo Order
private String paymentStatus; // dal modulo Payment
private Instant createdAt;
}
// Handler che aggiorna il read model reagendo agli eventi
@EventListener
class OrderSummaryProjection {
void on(OrderCreatedEvent e) {
OrderSummaryView view = new OrderSummaryView();
view.setOrderId(e.orderId());
view.setItemCount(e.itemCount());
view.setTotalAmount(e.total());
view.setCreatedAt(e.timestamp());
summaryRepo.save(view);
}
void on(PaymentCompletedEvent e) {
OrderSummaryView view = summaryRepo.findById(e.orderId());
view.setPaymentStatus("COMPLETED");
summaryRepo.save(view);
}
}
마이그레이션 전략: 공유에서 분리로
공유 스키마로 시작하고 나중에 스키마를 분리하기로 결정한 경우 다음 접근 방식 중 하나를 따르세요. 점진적이고 안전함:
- 접두사 추가: 모듈별 접두사를 사용하여 테이블 이름 바꾸기(
order_,catalog_) - 교차 모듈 FK 제거: 모듈 간의 외래 키를 ID별 참조로 교체합니다.
- 별도의 패턴 만들기: 테이블을 전용 PostgreSQL 스키마로 이동합니다.
- 연결 업데이트: 자체 스키마에 액세스하도록 각 모듈을 구성합니다.
- 확인하다: 통합 테스트를 실행하여 모든 것이 작동하는지 확인합니다.
다음 기사
다음 기사에서는 의사소통 패턴 모듈 간: 동기 호출, 비동기 메시지, 진행 중인 이벤트 버스 및 전체 CQRS 패턴. Spring Boot로 각 패턴을 구현하는 방법과 둘 중 하나를 선택하는 시기를 살펴보겠습니다.







