
CQRS(Command Query Responsibility Segregation) 패턴에서 Read Model과 Write Model을 분리한다는 것은 단순히 DAO(Data Access Object)와 VO(Value Object)를 나누는 것이 아니라, 데이터 저장 및 조회 로직을 명확히 분리하는 것을 의미함.
즉:
Write Model: 데이터를 변경하는 역할 (Command) → @Transactional을 적극 활용
Read Model: 데이터를 조회하는 역할 (Query) → 캐싱, Read 전용 DB 등을 활용 가능
아래 예제는 Spring Boot에서 CQRS 패턴을 적용하는 기본적인 구조를 보여줌.
import jakarta.persistence.*;
import lombok.Getter;
@Entity
@Getter
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int price;
protected Product() {}
public Product(String name, int price) {
this.name = name;
this.price = price;
}
public void updatePrice(int newPrice) {
this.price = newPrice;
}
}
import lombok.Getter;
@Getter
public class CreateProductCommand {
private String name;
private int price;
public CreateProductCommand(String name, int price) {
this.name = name;
this.price = price;
}
}
import lombok.Getter;
@Getter
public class UpdateProductPriceCommand {
private int newPrice;
public UpdateProductPriceCommand(int newPrice) {
this.newPrice = newPrice;
}
}
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
}
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class ProductCommandService {
private final ProductRepository productRepository;
public ProductCommandService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Product createProduct(CreateProductCommand command) {
Product product = new Product(command.getName(), command.getPrice());
return productRepository.save(product);
}
public void updatePrice(Long productId, UpdateProductPriceCommand command) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new IllegalArgumentException("Product not found"));
product.updatePrice(command.getNewPrice());
}
}
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/products")
public class ProductCommandController {
private final ProductCommandService productCommandService;
public ProductCommandController(ProductCommandService productCommandService) {
this.productCommandService = productCommandService;
}
@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody CreateProductCommand command) {
return ResponseEntity.ok(productCommandService.createProduct(command));
}
@PutMapping("/{id}/price")
public ResponseEntity<Void> updateProductPrice(@PathVariable Long id, @RequestBody UpdateProductPriceCommand command) {
productCommandService.updatePrice(id, command);
return ResponseEntity.noContent().build();
}
}
import lombok.Getter;
@Getter
public class ProductView {
private Long id;
private String name;
private int price;
public ProductView(Long id, String name, int price) {
this.id = id;
this.name = name;
this.price = price;
}
}
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface ProductQueryRepository extends JpaRepository<Product, Long> {
@Query("SELECT new com.example.dto.ProductView(p.id, p.name, p.price) FROM Product p")
List<ProductView> findAllProducts();
}
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProductQueryService {
private final ProductQueryRepository productQueryRepository;
public ProductQueryService(ProductQueryRepository productQueryRepository) {
this.productQueryRepository = productQueryRepository;
}
public List<ProductView> getProducts() {
return productQueryRepository.findAllProducts();
}
}
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/products")
public class ProductQueryController {
private final ProductQueryService productQueryService;
public ProductQueryController(ProductQueryService productQueryService) {
this.productQueryService = productQueryService;
}
@GetMapping
public List<ProductView> getProducts() {
return productQueryService.getProducts();
}
}
조회와 변경 로직을 완전히 분리하여 유지보수성이 높아짐
읽기 모델을 최적화하여 성능 개선 가능 (예: 캐싱, Read Replica DB 활용)