
CQRS(Command Query Responsibility Segregation)는 쓰기(Command)와 읽기(Query)의 책임을 분리해 각각을 독립적으로 최적화·확장하는 아키텍처 스타일이다. 대규모 트래픽에서 읽기 사이드의 확장과 캐싱, 읽기 전용 모델 운영이 특히 유리하다.
Application Layer
Domain Layer
Infrastructure Layer
@Service
public class OrderApplicationService {
private final OrderDomainService orderDomainService;
private final PaymentService paymentService;
public OrderApplicationService(OrderDomainService orderDomainService, PaymentService paymentService) {
this.orderDomainService = orderDomainService;
this.paymentService = paymentService;
}
@Transactional
public void createOrder(CreateOrderDTO dto) {
Order order = orderDomainService.createOrder(dto);
PaymentResponse pay = paymentService.processPayment(order);
orderDomainService.updateOrderPaymentStatus(order, pay);
orderDomainService.saveOrder(order);
}
}
@Service
public class OrderDomainService {
private final OrderRepository orderRepository;
public OrderDomainService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public Order createOrder(CreateOrderDTO dto) {
Order order = new Order(dto.getCustomerId(), dto.getItems());
order.applyDiscount(dto.getDiscountCode());
return order;
}
public void updateOrderPaymentStatus(Order order, PaymentResponse res) {
if (res.isSuccessful()) order.markAsPaid();
else order.markAsPaymentFailed();
}
public void saveOrder(Order order) {
orderRepository.save(order);
}
}
@Service
public class UserService {
private final UserRepository repo;
public UserService(UserRepository repo) { this.repo = repo; }
public User getUserById(Long id) {
return repo.findById(id).orElseThrow(() -> new UserNotFoundException(id));
}
public void createUser(CreateUserDTO dto) {
repo.save(new User(dto.getName(), dto.getEmail()));
}
}
// Domain Layer
public interface OrderRepository {
Order findById(Long id);
Order save(Order order);
}
// Infrastructure Layer(JPA 구현)
@Repository
public interface JpaOrderRepository extends OrderRepository, JpaRepository<Order, Long> { }
com.ohgiraffers.cqrs.product
├─ command
│ ├─ controller/ProductCommandController.java
│ ├─ dto
│ │ ├─ request/ProductCreateRequest.java
│ │ └─ request/ProductUpdateRequest.java
│ │ └─ response/ProductCommandResponse.java
│ ├─ service/ProductCommandService.java
│ ├─ domain/aggregate/Product.java
│ └─ repository/ProductRepository.java // extends JpaRepository<...>
└─ query
├─ controller/ProductQueryController.java
├─ dto
│ ├─ request/ProductSearchRequest.java // 선택
│ └─ response/ProductListResponse.java
│ └─ response/ProductDetailResponse.java
├─ service/ProductQueryService.java
└─ mapper/ProductMapper.java // MyBatis 등 조회 특화
계정/DB 생성
CREATE USER 'swcamp'@'%' IDENTIFIED BY 'swcamp';
CREATE DATABASE cqrs;
GRANT ALL PRIVILEGES ON cqrs.* TO 'swcamp'@'%';
테이블
CREATE TABLE IF NOT EXISTS tbl_category
(
category_code BIGINT AUTO_INCREMENT COMMENT '카테고리코드',
category_name VARCHAR(50) NOT NULL COMMENT '카테고리명',
CONSTRAINT pk_category_code PRIMARY KEY (category_code)
) ENGINE=INNODB COMMENT '상품카테고리';
CREATE TABLE IF NOT EXISTS tbl_product
(
product_code BIGINT AUTO_INCREMENT COMMENT '상품코드',
product_name VARCHAR(100) NOT NULL COMMENT '상품명',
product_price VARCHAR(100) NOT NULL COMMENT '상품가격',
product_description VARCHAR(1000) NOT NULL COMMENT '상품설명',
category_code BIGINT COMMENT '카테고리코드',
product_image_url VARCHAR(100) NOT NULL COMMENT '상품이미지경로',
product_stock BIGINT NOT NULL COMMENT '상품재고',
created_at DATETIME NOT NULL DEFAULT now() COMMENT '생성일시',
modified_at DATETIME NOT NULL DEFAULT now() COMMENT '수정일시',
status VARCHAR(10) NOT NULL DEFAULT 'USABLE' COMMENT '상태',
CONSTRAINT pk_product_code PRIMARY KEY (product_code),
CONSTRAINT fk_category_code FOREIGN KEY (category_code) REFERENCES tbl_category (category_code)
) ENGINE=INNODB COMMENT '상품';
샘플 데이터(일부)
INSERT INTO tbl_category (category_name) VALUES ('식사'), ('디저트'), ('음료');
INSERT INTO tbl_product
(product_name, product_price, product_description, category_code, product_image_url, product_stock)
VALUES
('열무김치라떼', 4500, '열무로 만든 김치 라떼', 3, 'http://localhost:8080/productimgs/06a0060a.PNG', 10),
('우럭스무디', 5000, '우럭으로 만든 스무디', 3, 'http://localhost:8080/productimgs/fcb3e0c8.PNG', 15),
('앙버터김치찜', 13000, '가장 먹을만한 김치찜', 1, 'http://localhost:8080/productimgs/7580adcf.PNG', 19);
예제 요청 바디
{
"productName": "새상품",
"productPrice": 10000,
"productDescription": "상품설명",
"categoryCode": 1,
"productStock": 10
}