0. 들어가며
지금까지 BASIC 시리즈를 통해 우리는 마이크로서비스 아키텍처의 기본 개념부터 시작하여 서비스 분해, 동기/비동기 통신, API Gateway 패턴까지 살펴보았다. 이제 ADVANCED 시리즈의 첫 주제로, MSA에서 가장 까다로운 영역 중 하나인 데이터 관리 전략에 대해 알아볼 차례다.
모놀리식 아키텍처에서는 하나의 중앙 데이터베이스를 사용하는 것이 일반적이었다. 하지만 마이크로서비스에서는 각 서비스가 독립적인 데이터 저장소를 가져야 한다는 원칙(Database per Service)이 있다. 이 원칙은 서비스 간 결합도를 낮추고 독립적인 확장을 가능하게 하지만, 동시에 데이터 일관성, 조회, 트랜잭션 관리 등 새로운 과제를 안겨준다.
이번 글에서는 Database per Service 패턴의 개념과 구현 방법부터 시작하여, 분산 환경에서 데이터를 효율적으로 관리하기 위한 CQRS, 이벤트 소싱, 그리고 수평적 확장을 위한 샤딩(Sharding) 전략까지 상세히 살펴본다.
1. Database per Service 패턴
1.1. 패턴의 개념과 필요성
Database per Service 패턴은 각 마이크로서비스가 자신만의 데이터베이스를 독립적으로 소유하고 관리하는 패턴이다. 다른 서비스의 데이터베이스에 직접 접근할 수 없으며, 반드시 API를 통해서만 데이터를 주고받아야 한다.
패턴의 핵심 원칙:
- 각 서비스는 자신의 데이터베이스만 직접 접근 가능
- 다른 서비스의 데이터는 API를 통해서만 조회/변경
- 서비스 간 데이터베이스 공유 금지
- 각 서비스는 자신의 데이터 스키마를 독립적으로 변경 가능
// ❌ 잘못된 예: 다른 서비스의 DB에 직접 접근
@Repository
public class OrderRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
public User findUser(String userId) {
// 주문 서비스가 사용자 DB에 직접 접근 (위반!)
return jdbcTemplate.queryForObject(
"SELECT * FROM user_db.users WHERE id = ?",
userRowMapper, userId
);
}
}
// ✅ 올바른 예: API를 통한 접근
@Service
public class OrderService {
private final UserServiceClient userServiceClient; // Feign Client
public OrderDto getOrderWithUser(String orderId) {
Order order = orderRepository.findById(orderId);
// 사용자 서비스 API를 통해서만 접근
UserDto user = userServiceClient.getUser(order.getUserId());
return combine(order, user);
}
}
1.2. 패턴의 장점
1. 서비스 간 결합도 감소
- 데이터베이스 스키마 변경이 다른 서비스에 영향 없음
- 각 서비스가 독립적으로 데이터베이스 기술 선택 가능
2. 독립적인 확장
- 서비스별로 데이터베이스 확장 전략 수립 가능
- 트래픽이 많은 서비스만 별도로 DB 리소스 할당
3. 보안 강화
- 데이터 접근 제어를 서비스 레벨에서 관리
- API를 통한 통제된 접근만 허용
4. 기술 다양성 (Polyglot Persistence)
- 서비스 특성에 맞는 데이터베이스 선택 가능
// 주문 서비스 - 관계형 DB (트랜잭션 중요)
@EnableJpaRepositories(basePackages = "com.eshop.order.repository")
public class OrderService {
// MySQL, PostgreSQL 등
}
// 검색 서비스 - 전문 검색 엔진
@EnableElasticsearchRepositories
public class SearchService {
// Elasticsearch
}
// 추천 서비스 - 그래프 DB
public class RecommendationService {
// Neo4j
}
// 로그 서비스 - 시계열 DB
public class LoggingService {
// InfluxDB, TimescaleDB
}
1.3. 패턴의 단점과 해결 방안
1. 분산 트랜잭션 문제
- 여러 서비스에 걸친 트랜잭션 처리 어려움
- → SAGA 패턴으로 해결 (다음 글에서 상세 다룸)
2. 조인(Join) 쿼리 불가능
- 여러 서비스의 데이터를 조합하려면 API 호출 필요
- → API Gateway에서 조합(Aggregation) 또는 CQRS 도입
3. 데이터 중복
- 서비스 간 데이터 중복 저장 필요할 수 있음
- → 이벤트 기반 동기화로 해결
4. 일관성 유지 어려움
- 최종 일관성(Eventual Consistency)만 보장 가능
- → 비즈니스 요구사항에 따라 트레이드오프 수용
2. Polyglot Persistence
2.1. 개념
Polyglot Persistence는 각 서비스의 데이터 특성에 맞는 최적화된 데이터 저장 기술을 선택하는 접근 방식이다. 관계형 DB, NoSQL, 검색 엔진, 그래프 DB 등 다양한 데이터베이스를 혼용하여 사용한다.
2.2. 데이터 특성에 따른 DB 선택 기준
| 데이터 유형 | 적합한 DB | 사용 사례 |
| 트랜잭션 데이터, 정형 데이터 | RDBMS (MySQL, PostgreSQL) | 주문, 결제, 사용자 정보 |
| 대용량 로그, 시계열 데이터 | 시계열 DB (InfluxDB, TimescaleDB) | 모니터링, 로그 수집 |
| 전문 검색 | 검색 엔진 (Elasticsearch) | 상품 검색, 로그 검색 |
| 키-값 캐시 | 인메모리 DB (Redis) | 세션, 캐시, 장바구니 |
| 그래프 데이터 | 그래프 DB (Neo4j) | 추천, 소셜 관계 |
| 문서 데이터 | 문서형 DB (MongoDB) | 상품 카탈로그, 콘텐츠 |
2.3. 이커머스에서의 Polyglot Persistence 예시
# 이커머스 시스템의 데이터 저장소 구성
services:
# 주문 서비스 - ACID 트랜잭션 중요
order-service:
database: PostgreSQL
reason: "강한 일관성, 트랜잭션, 조인 쿼리 필요"
# 상품 카탈로그 - 스키마 유연성, 대용량
catalog-service:
database: MongoDB
reason: "상품 속성 다양성, 스키마 변경 용이"
# 검색 서비스 - 전문 검색
search-service:
database: Elasticsearch
reason: "빠른 전문 검색, 형태소 분석"
# 추천 서비스 - 실시간 분석
recommendation-service:
database: Redis + TensorFlow
reason: "실시간 처리, 인메모리 속도"
# 사용자 세션 - 빠른 읽기/쓰기
session-service:
database: Redis
reason: "초고속 액세스, TTL 만료"
# 로그 수집 - 시계열 데이터
logging-service:
database: Elasticsearch + Kibana
reason: "로그 검색, 시각화"
3. CAP 이론
3.1. CAP 이론의 이해
CAP 이론은 분산 시스템에서 일관성(Consistency), 가용성(Availability), 분할 내성(Partition Tolerance) 세 가지 속성을 동시에 만족할 수 없다는 이론이다.
세 가지 속성:
- 일관성(C): 모든 노드가 같은 시간에 같은 데이터를 보여줌
- 가용성(A): 모든 요청이 성공 또는 실패 응답을 받음
- 분할 내성(P): 네트워크 분할이 발생해도 시스템 계속 동작
3.2. CAP 트레이드오프
[네트워크 분할 발생 시 선택]
↓
[CP 시스템] [AP 시스템]
(일관성 선택) (가용성 선택)
↓ ↓
데이터 정확성 서비스 지속성
트랜잭션 중요 사용자 경험 중요
CP 시스템 (일관성 + 분할 내성):
- 네트워크 분할 시 일관성을 위해 가용성 희생
- 예: 은행 시스템, 재고 관리 (정확성이 최우선)
AP 시스템 (가용성 + 분할 내성):
- 네트워크 분할 시 가용성을 위해 일관성 희생
- 예: 소셜 미디어, 상품 조회 (응답 속도가 최우선)
3.3. 데이터베이스별 CAP 분류
| 데이터베이스 | 유형 | 특징 |
| 관계형 DB (MySQL, PostgreSQL) | CA/CP | 단일 노드: CA, 분산: CP |
| MongoDB | CP | 복제셋에서 일관성 우선 |
| Cassandra | AP | 가용성 우선, 최종 일관성 |
| Redis | CP | 단일 노드: CA, 클러스터: CP |
| DynamoDB | AP | 가용성 우선 (설정에 따라 조정 가능) |
3.4. CAP 트레이드오프 설계 예시
// 재고 서비스 - CP 설계 (정확성 중요)
@Service
public class InventoryService {
@Transactional(isolation = Isolation.SERIALIZABLE)
public void deductStock(String productId, int quantity) {
// 강한 일관성 필요 - 재고 오차가 큰 문제를 일으킴
int currentStock = inventoryRepository.getStock(productId);
if (currentStock < quantity) {
throw new InsufficientStockException();
}
inventoryRepository.updateStock(productId, currentStock - quantity);
}
}
// 상품 조회 서비스 - AP 설계 (응답 속도 중요)
@Service
public class ProductService {
@Cacheable(value = "products", key = "#productId")
public Product getProduct(String productId) {
// 약간의 데이터 지연은 허용 (최종 일관성)
// 캐시된 데이터가 1초 정도 오래되어도 문제 없음
return productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException());
}
}
4. 데이터 파티셔닝과 샤딩
4.1. 데이터 파티셔닝 개념
데이터 파티셔닝은 대용량 데이터를 여러 노드에 분산 저장하는 기술이다. 수평적 확장(Scale-Out)을 가능하게 한다.
파티셔닝의 두 가지 방식:
- 수평 파티셔닝(샤딩): 행(Row) 단위로 분할
- 수직 파티셔닝: 열(Column) 단위로 분할 (정규화)
4.2. 샤딩(Sharding) 전략
샤딩은 데이터를 여러 데이터베이스에 분산 저장하는 기술이다. 각 샤드는 독립적인 데이터베이스 서버에서 동작한다.
샤딩 키 선택 기준:
- 데이터가 균등하게 분배되어야 함
- 대부분의 쿼리가 샤딩 키를 포함해야 함
- 샤딩 키는 변경되지 않는 값이어야 함
// 샤딩 키 기반 라우팅 예시
@Component
public class ShardRouter {
private static final int SHARD_COUNT = 4;
public String getShardKey(String userId) {
// userId의 해시값을 기반으로 샤드 결정
int shard = Math.abs(userId.hashCode() % SHARD_COUNT);
return "shard-" + shard;
}
public String getConnectionString(String userId) {
String shardKey = getShardKey(userId);
return "jdbc:mysql://" + shardKey + ".database.example.com:3306/orderdb";
}
}
// 샤딩 적용 예시
@Service
public class OrderService {
@Autowired
private ShardRouter shardRouter;
@Autowired
private OrderRepository orderRepository;
public Order getOrder(String orderId, String userId) {
// 사용자 ID로 샤드 결정
String shardKey = shardRouter.getShardKey(userId);
// 동적 데이터소스 라우팅
return orderRepository.findByOrderId(orderId, shardKey);
}
}
4.3. 샤딩 전략 유형
1. 모듈러 샤딩 (Modular Sharding)
// 해시 기반 분할
shardId = hashCode(key) % numberOfShards
2. 범위 기반 샤딩 (Range Based Sharding)
-- 사용자 ID 범위로 분할
shard1: users 1-10000
shard2: users 10001-20000
shard3: users 20001-30000
3. 디렉토리 기반 샤딩 (Directory Based Sharding)
// lookup 테이블 사용
Map<String, Integer> shardMapping = new HashMap<>();
shardMapping.put("user-1", 1);
shardMapping.put("user-2", 1);
shardMapping.put("user-3", 2);
4.4. 샤딩의 장단점
장점:
- 무제한에 가까운 확장성
- 읽기/쓰기 성능 향상
- 장애 격리 (한 샤드 장애가 전체에 영향 없음)
단점:
- 복잡한 애플리케이션 로직 필요
- 조인 쿼리 어려움
- 데이터 재분배(Resharding)의 어려움
- 분산 트랜잭션 복잡성
5. CQRS 패턴
5.1. CQRS 개념
CQRS(Command Query Responsibility Segregation)는 명령(Command)과 조회(Query)의 책임을 분리하는 패턴이다.
핵심 아이디어:
- 명령(Command): 데이터를 변경하는 작업 (Create, Update, Delete)
- 조회(Query): 데이터를 읽는 작업 (Read)
// 전통적인 CRUD - 하나의 모델로 읽기/쓰기 처리
@Entity
public class Order {
// 쓰기와 읽기가 동일한 모델 사용
}
// CQRS - 읽기/쓰기 모델 분리
// 쓰기 모델 (Command)
@Entity
@Table(name = "orders")
public class OrderCommand {
// 복잡한 비즈니스 로직, 정규화된 구조
}
// 읽기 모델 (Query)
@Document(index = "orders")
public class OrderQuery {
// 비정규화된, 조회에 최적화된 구조
private String orderId;
private String userName; // 사용자 서비스에서 조인된 데이터
private String productNames; // 상품 서비스에서 조인된 데이터
}
5.2. CQRS가 필요한 상황
CQRS 도입 고려 조건:
- 읽기와 쓰기의 성능 요구사항이 다른 경우
- 복잡한 비즈니스 로직과 다양한 조회 요구사항
- 팀을 명령 측과 조회 측으로 분리하고 싶은 경우
- 이벤트 소싱(Event Sourcing)과 함께 사용할 때
5.3. CQRS + Event Sourcing 아키텍처
[Command] → [Command Handler] → [Event Store]
↓
[Event Processor]
↓
[Read Model Updater] → [Query Database]
↓
[Query Handler] ← [Query]
5.4. [실습 13~14] CQRS + Event Sourcing 구현
1. 이벤트 정의
// 공통 이벤트 인터페이스
public interface DomainEvent {
String getEventId();
String getAggregateId();
LocalDateTime getOccurredAt();
}
// 주문 생성 이벤트
@Value
public class OrderCreatedEvent implements DomainEvent {
String eventId = UUID.randomUUID().toString();
String aggregateId; // orderId
String userId;
String productId;
int quantity;
Money price;
LocalDateTime occurredAt = LocalDateTime.now();
}
// 주문 상태 변경 이벤트
@Value
public class OrderStatusChangedEvent implements DomainEvent {
String eventId = UUID.randomUUID().toString();
String aggregateId; // orderId
OrderStatus oldStatus;
OrderStatus newStatus;
LocalDateTime occurredAt = LocalDateTime.now();
}
2. 이벤트 저장소 (Command Side)
@Entity
@Table(name = "event_store")
@Data
public class EventEntity {
@Id
private String eventId;
private String aggregateId;
private String aggregateType;
private String eventType;
private String eventData; // JSON
private int version;
private LocalDateTime timestamp;
}
@Repository
public interface EventStoreRepository extends JpaRepository<EventEntity, String> {
List<EventEntity> findByAggregateIdOrderByVersionAsc(String aggregateId);
}
@Service
@RequiredArgsConstructor
public class EventStore {
private final EventStoreRepository repository;
private final ObjectMapper objectMapper;
@Transactional
public void saveEvents(String aggregateId, List<DomainEvent> events, int expectedVersion) {
// 낙관적 락 검증
List<EventEntity> existing = repository.findByAggregateIdOrderByVersionAsc(aggregateId);
if (existing.size() != expectedVersion) {
throw new OptimisticLockingException();
}
// 이벤트 저장
for (int i = 0; i < events.size(); i++) {
DomainEvent event = events.get(i);
EventEntity entity = new EventEntity();
entity.setEventId(event.getEventId());
entity.setAggregateId(aggregateId);
entity.setAggregateType("Order");
entity.setEventType(event.getClass().getSimpleName());
entity.setEventData(objectMapper.writeValueAsString(event));
entity.setVersion(expectedVersion + i + 1);
entity.setTimestamp(event.getOccurredAt());
repository.save(entity);
}
}
public List<DomainEvent> loadEvents(String aggregateId) {
return repository.findByAggregateIdOrderByVersionAsc(aggregateId)
.stream()
.map(this::deserialize)
.collect(Collectors.toList());
}
private DomainEvent deserialize(EventEntity entity) {
// JSON을 실제 이벤트 객체로 변환
try {
Class<?> eventClass = Class.forName("com.eshop.event." + entity.getEventType());
return (DomainEvent) objectMapper.readValue(entity.getEventData(), eventClass);
} catch (Exception e) {
throw new RuntimeException("Failed to deserialize event", e);
}
}
}
3. 애그리게이트 (Command Model)
public class OrderAggregate {
private String id;
private String userId;
private OrderStatus status;
private List<OrderItem> items;
private int version;
private List<DomainEvent> changes = new ArrayList<>();
// 새로운 주문 생성
public static OrderAggregate create(String orderId, String userId, List<OrderItem> items) {
OrderAggregate aggregate = new OrderAggregate();
aggregate.applyChange(new OrderCreatedEvent(orderId, userId, items));
return aggregate;
}
// 저장된 이벤트로부터 복원
public static OrderAggregate recreateFrom(List<DomainEvent> events) {
OrderAggregate aggregate = new OrderAggregate();
events.forEach(aggregate::applyEvent);
aggregate.changes.clear();
return aggregate;
}
// 명령 처리
public void changeStatus(OrderStatus newStatus) {
if (this.status == newStatus) return;
// 비즈니스 규칙 검증
if (this.status == OrderStatus.COMPLETED) {
throw new IllegalStateException("Completed order cannot change status");
}
applyChange(new OrderStatusChangedEvent(this.id, this.status, newStatus));
}
// 이벤트 적용 (내부 상태 변경)
private void applyEvent(DomainEvent event) {
if (event instanceof OrderCreatedEvent) {
apply((OrderCreatedEvent) event);
} else if (event instanceof OrderStatusChangedEvent) {
apply((OrderStatusChangedEvent) event);
}
version++;
}
private void apply(OrderCreatedEvent event) {
this.id = event.getAggregateId();
this.userId = event.getUserId();
this.items = event.getItems();
this.status = OrderStatus.CREATED;
}
private void apply(OrderStatusChangedEvent event) {
this.status = event.getNewStatus();
}
private void applyChange(DomainEvent event) {
applyEvent(event);
changes.add(event);
}
public List<DomainEvent> getChanges() {
return changes;
}
public int getVersion() {
return version;
}
}
4. 명령 처리 서비스 (Command Side)
@Service
@RequiredArgsConstructor
public class OrderCommandService {
private final EventStore eventStore;
private final EventPublisher eventPublisher;
@Transactional
public String createOrder(CreateOrderCommand command) {
String orderId = UUID.randomUUID().toString();
OrderAggregate order = OrderAggregate.create(
orderId,
command.getUserId(),
command.getItems()
);
eventStore.saveEvents(orderId, order.getChanges(), 0);
// 이벤트 발행 (다른 서비스에 전파)
order.getChanges().forEach(eventPublisher::publish);
return orderId;
}
@Transactional
public void updateOrderStatus(String orderId, OrderStatus newStatus) {
List<DomainEvent> events = eventStore.loadEvents(orderId);
OrderAggregate order = OrderAggregate.recreateFrom(events);
int currentVersion = order.getVersion();
order.changeStatus(newStatus);
eventStore.saveEvents(orderId, order.getChanges(), currentVersion);
order.getChanges().forEach(eventPublisher::publish);
}
}
5. 읽기 모델 (Query Side)
@Entity
@Table(name = "order_view")
@Data
public class OrderView {
@Id
private String orderId;
private String userId;
private String userName; // 사용자 서비스에서 가져온 데이터
private List<OrderItemView> items;
private Money totalAmount;
private String status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private int version;
}
@Embeddable
@Data
public class OrderItemView {
private String productId;
private String productName; // 상품 서비스에서 가져온 데이터
private int quantity;
private Money price;
}
@Repository
public interface OrderViewRepository extends JpaRepository<OrderView, String> {
List<OrderView> findByUserId(String userId);
List<OrderView> findByStatus(String status);
@Query("SELECT o FROM OrderView o WHERE o.createdAt BETWEEN :start AND :end")
List<OrderView> findByDateRange(LocalDateTime start, LocalDateTime end);
}
6. 읽기 모델 업데이트 (Event Processor)
@Component
@RequiredArgsConstructor
public class OrderViewUpdater {
private final OrderViewRepository viewRepository;
private final UserServiceClient userServiceClient;
private final ProductServiceClient productServiceClient;
@EventListener
@Transactional
public void onOrderCreated(OrderCreatedEvent event) {
OrderView view = new OrderView();
view.setOrderId(event.getAggregateId());
view.setUserId(event.getUserId());
view.setStatus("CREATED");
view.setCreatedAt(event.getOccurredAt());
view.setUpdatedAt(event.getOccurredAt());
view.setVersion(1);
// 사용자 정보 조회 (다른 서비스 API 호출)
try {
UserDto user = userServiceClient.getUser(event.getUserId());
view.setUserName(user.getName());
} catch (Exception e) {
// 실패 처리 - 나중에 재시도하거나 기본값 설정
view.setUserName("Unknown");
}
// 상품 정보 조회 및 아이템 구성
List<OrderItemView> itemViews = event.getItems().stream()
.map(item -> {
OrderItemView itemView = new OrderItemView();
itemView.setProductId(item.getProductId());
itemView.setQuantity(item.getQuantity());
itemView.setPrice(item.getPrice());
try {
ProductDto product = productServiceClient.getProduct(item.getProductId());
itemView.setProductName(product.getName());
} catch (Exception e) {
itemView.setProductName("Unknown");
}
return itemView;
})
.collect(Collectors.toList());
view.setItems(itemViews);
view.setTotalAmount(calculateTotal(itemViews));
viewRepository.save(view);
}
@EventListener
@Transactional
public void onOrderStatusChanged(OrderStatusChangedEvent event) {
OrderView view = viewRepository.findById(event.getAggregateId())
.orElseThrow(() -> new IllegalStateException("Order view not found"));
view.setStatus(event.getNewStatus().name());
view.setUpdatedAt(event.getOccurredAt());
view.setVersion(view.getVersion() + 1);
viewRepository.save(view);
}
private Money calculateTotal(List<OrderItemView> items) {
return items.stream()
.map(item -> item.getPrice().multiply(item.getQuantity()))
.reduce(Money.ZERO, Money::add);
}
}
7. 조회 서비스 (Query Side)
@Service
@RequiredArgsConstructor
public class OrderQueryService {
private final OrderViewRepository viewRepository;
public OrderView getOrder(String orderId) {
return viewRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
public List<OrderView> getOrdersByUser(String userId) {
return viewRepository.findByUserId(userId);
}
public Page<OrderView> searchOrders(OrderSearchCriteria criteria, Pageable pageable) {
Specification<OrderView> spec = Specification.where(null);
if (criteria.getUserId() != null) {
spec = spec.and((root, query, cb) ->
cb.equal(root.get("userId"), criteria.getUserId()));
}
if (criteria.getStatus() != null) {
spec = spec.and((root, query, cb) ->
cb.equal(root.get("status"), criteria.getStatus()));
}
if (criteria.getStartDate() != null && criteria.getEndDate() != null) {
spec = spec.and((root, query, cb) ->
cb.between(root.get("createdAt"),
criteria.getStartDate(), criteria.getEndDate()));
}
return viewRepository.findAll(spec, pageable);
}
}
6. [실습 요약] MariaDB Sharding 처리
6.1. 실습 11~12: 2개의 MariaDB를 이용한 Sharding
샤딩 구성:
# 샤드 1: 사용자 ID 1-10000
shard1:
url: jdbc:mariadb://shard1.example.com:3306/orderdb
username: app_user
password: password
# 샤드 2: 사용자 ID 10001-20000
shard2:
url: jdbc:mariadb://shard2.example.com:3306/orderdb
username: app_user
password: password
# 샤드 3: 사용자 ID 20001-30000
shard3:
url: jdbc:mariadb://shard3.example.com:3306/orderdb
username: app_user
password: password
샤딩 라우터 구현:
@Component
@RequiredArgsConstructor
public class ShardingDataSourceRouter extends AbstractRoutingDataSource {
private final ShardKeyHolder shardKeyHolder;
@Override
protected Object determineCurrentLookupKey() {
String userId = shardKeyHolder.getUserId();
if (userId == null) {
return "default";
}
// 사용자 ID의 마지막 4자리 숫자로 샤드 결정
int shardId = Integer.parseInt(userId.substring(userId.length() - 4)) % 3 + 1;
return "shard" + shardId;
}
}
7. 정리
7.1. 데이터 관리 패턴 선택 가이드
| 상황 | 추천 패턴 |
| 강한 일관성이 필요한 경우 | Database per Service + ACID |
| 읽기/쓰기 성능이 다른 경우 | CQRS |
| 완전한 감사 추적이 필요한 경우 | Event Sourcing |
| 대용량 데이터 확장이 필요한 경우 | Sharding |
| 다양한 데이터 저장소가 필요한 경우 | Polyglot Persistence |
7.2. 주의사항
- 과도한 설계 금지: 모든 서비스에 CQRS나 이벤트 소싱을 적용할 필요는 없다. 필요한 곳에만 적용하자.
- 복잡성 대비 이득 고려: 패턴 도입으로 인한 복잡성이 얻는 이득보다 크지 않은지 검토하자.
- 점진적 도입: 한 번에 모든 것을 바꾸려 하지 말고, 중요한 서비스부터 단계적으로 도입하자.
- 모니터링 필수: 분산 환경에서는 데이터 흐름 추적과 모니터링이 더욱 중요하다.
7.3. 다음 글 예고
다음 글에서는 분산 환경에서 가장 까다로운 주제 중 하나인 분산 트랜잭션과 SAGA 패턴에 대해 자세히 알아볼 예정이다. 2PC의 한계, SAGA의 두 가지 구현 방식(코레오그래피/오케스트레이션), Outbox 패턴, CDC(Change Data Capture)까지 실무에서 꼭 필요한 내용을 다룰 것이다.
'MSA > MSA 아키텍처' 카테고리의 다른 글
| [ADVANCED #3] MSA 회복성, 관측성, 모니터링 전략 (0) | 2026.03.02 |
|---|---|
| [ADVANCED #2] 분산 트랜잭션과 SAGA 패턴 완전 정리 (0) | 2026.03.02 |
| [BASIC #4] 비동기 통신과 Event-Driven Architecture (0) | 2026.03.02 |
| [BASIC #3] MSA 동기 통신 전략과 API Gateway 패턴 (0) | 2026.03.02 |
| [BASIC #2] Microservice Architecture 개요와 서비스 분해 전략 (0) | 2026.03.02 |
