기록

[JpaTest] CQRS와 서비스 레이어에서의 트랜잭션 관리 본문

교육/강의

[JpaTest] CQRS와 서비스 레이어에서의 트랜잭션 관리

youngyin 2025. 2. 5. 00:00

CQRS 개념

CQRS(Command Query Responsibility Segregation)는 소프트웨어 아키텍처 스타일 중 하나로, 시스템의 명령(Command)과 조회(Query)를 분리하여 각자의 책임을 명확히 정의합니다. 이는 대규모 애플리케이션에서 복잡성을 줄이고 성능을 최적화하기 위한 강력한 설계 방식으로, 다음과 같은 특징이 있습니다:

  • 명령(Command): 데이터 변경 작업을 처리하며, 데이터베이스에 새로운 데이터를 삽입하거나 기존 데이터를 수정 및 삭제하는 역할을 합니다. 이러한 작업은 주로 비동기적으로 처리되며, 트랜잭션을 통해 데이터의 일관성을 보장합니다.
  • 조회(Query): 데이터를 읽는 작업으로, 시스템 상태를 변경하지 않고 필요한 정보를 클라이언트에게 반환합니다. 조회 작업은 일반적으로 고성능이 요구되며 읽기 전용 트랜잭션에서 수행됩니다.

CQRS는 명령과 조회를 물리적으로 분리함으로써 성능 병목을 최소화하고, 데이터 모델을 각 작업에 최적화할 수 있는 유연성을 제공합니다.

읽기 전용 트랜잭션과 기본 설정

@Transactional(readOnly = true) 어노테이션은 JPA와 같은 ORM 도구를 사용할 때 읽기 전용 작업의 성능을 최적화하는 데 중요한 역할을 합니다. 이 설정은 다음과 같은 이유로 기본값으로 사용됩니다:

  1. 성능 향상: 읽기 전용 트랜잭션은 플러시(flush) 호출을 방지하여 데이터베이스 리소스를 절약하고 처리 속도를 증가시킵니다. 예를 들어, 대규모 전자상거래 플랫폼에서는 상품 조회와 검색 요청이 전체 트래픽의 80% 이상을 차지하는 경우가 일반적입니다. 이러한 읽기 중심 시스템에서는 readOnly 설정이 응답 시간을 단축시키는 데 효과적입니다.
  2. 데이터 무결성 보장: 읽기 전용 트랜잭션은 데이터 변경 작업이 발생하지 않도록 보장하여 실수로 데이터베이스 상태가 변경되는 상황을 방지합니다.

쓰기 작업과 트랜잭션 관리

쓰기 작업은 데이터베이스 상태를 변경해야 하므로, 별도의 @Transactional 어노테이션이 필요합니다. 이는 다음과 같은 이유에서 중요합니다:

  • 읽기 전용 트랜잭션에서는 데이터 변경이 불가능하며, 변경된 데이터를 데이터베이스에 영속적으로 저장하거나 커밋하는 과정이 필요하기 때문입니다.
  • 쓰기 작업은 종종 여러 테이블에 걸쳐 일관된 상태 변경을 요구하며, 이를 위해 트랜잭션 경계 내에서 처리해야 합니다.

서비스 레이어에서의 트랜잭션 관리

다음은 CQRS 원칙을 준수하며 서비스 레이어에서 읽기 및 쓰기 작업을 관리하는 예제 코드입니다:

@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class ProductService {
    private final ProductRepository productRepository;
    private final ProductNumberFactory productNumberFactory;

    // 읽기 작업: readOnly 트랜잭션 적용
    public List<ProductResponse> getSellingProducts() {
        List<Product> products = productRepository.findAllBySellingStatusIn(ProductSellingStatus.forDisplay());

        return products.stream().map(ProductResponse::of)
                .collect(Collectors.toList());
    }

    // 쓰기 작업: 기본 트랜잭션(readOnly=false)
    @Transactional
    public ProductResponse createProduct(ProductCreateServiceRequest request) {
        String nextProductNumber = productNumberFactory.createNextProductNumber();

        Product product = request.toEntity(nextProductNumber);
        Product savedProduct = productRepository.save(product);

        return ProductResponse.builder()
                .id(savedProduct.getId())
                .productNumber(nextProductNumber)
                .type(savedProduct.getType())
                .price(savedProduct.getPrice())
                .sellingStatus(savedProduct.getSellingStatus())
                .name(savedProduct.getName())
                .build();
    }
}

CQRS와 트랜잭션 관리의 중요성

서비스 레이어에서 @Transactional(readOnly = true)를 기본값으로 설정하고 쓰기 작업에만 명시적으로 트랜잭션을 추가하는 접근 방식은 다음과 같은 이점을 제공합니다:

  1. 책임 분리: CQRS의 원칙에 따라 명령과 조회의 책임을 분리하면, 각 작업의 역할과 성능 요구사항에 최적화된 설계를 구현할 수 있습니다.
  2. 성능 최적화: 읽기 작업에서 불필요한 데이터베이스 작업(예: 플러시 호출)을 제거하여 리소스를 효율적으로 사용합니다.
  3. 안정성 향상: 읽기 작업 중 데이터 변경을 방지함으로써 안정성을 보장합니다.
  4. 확장성 강화: 읽기와 쓰기 작업을 물리적으로 분리하면 트래픽 급증 시 성능 병목 현상을 완화하고 확장성을 향상시킬 수 있습니다.

결론

CQRS는 명령과 조회를 분리하여 성능과 유지보수성을 동시에 향상시키는 강력한 설계 패턴입니다. 서비스 레이어에서 읽기 작업을 위한 @Transactional(readOnly = true)를 기본값으로 설정하고, 쓰기 작업에 별도의 트랜잭션을 명시적으로 정의하는 것은 CQRS 원칙을 실천하는 접근 방식입니다.

Comments