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

y001·2025년 1월 24일
post-thumbnail

1. CQRS 개념과 트랜잭션 관리

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

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

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


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

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

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

3. 쓰기 작업과 트랜잭션 관리

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

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

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

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

@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class ParkingService {
    private final ParkingRepository parkingRepository;
    private final ParkingTicketFactory parkingTicketFactory;

    // 읽기 작업: readOnly 트랜잭션 적용
    public List<ParkingTicketResponse> getActiveParkingTickets() {
        List<ParkingTicket> tickets = parkingRepository.findAllByStatus(ParkingStatus.ACTIVE);
        return tickets.stream().map(ParkingTicketResponse::of)
                .collect(Collectors.toList());
    }

    // 쓰기 작업: 기본 트랜잭션(readOnly=false)
    @Transactional
    public ParkingTicketResponse createParkingTicket(ParkingCreateRequest request) {
        String nextTicketNumber = parkingTicketFactory.createNextTicketNumber();

        ParkingTicket ticket = request.toEntity(nextTicketNumber);
        ParkingTicket savedTicket = parkingRepository.save(ticket);

        return ParkingTicketResponse.builder()
                .id(savedTicket.getId())
                .ticketNumber(nextTicketNumber)
                .status(savedTicket.getStatus())
                .licensePlate(savedTicket.getLicensePlate())
                .entryTime(savedTicket.getEntryTime())
                .build();
    }
}

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

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

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

6. 결론

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

이 방식을 적용하면 성능 최적화, 데이터 안정성 보장, 확장성 강화 등의 효과를 얻을 수 있으며, 대규모 트래픽을 처리하는 시스템에서 더욱 강력한 아키텍처를 구축할 수 있습니다.


📌 이 글은 TDD 강의를 학습한 내용을 바탕으로 재구성하였습니다. 문제가 되는 부분이 있다면 수정하겠습니다.

0개의 댓글