IT/JAVA

Java에서 CQRS 패턴 구현: 명령과 쿼리의 분리

KeepGooing 2024. 11. 28. 05:25
반응형

Java에서 CQRS 패턴 구현: 명령과 쿼리의 분리

 

1. CQRS의 기본 구조

CQRS 패턴의 핵심은 읽기 모델과 쓰기 모델을 분리하는 것입니다.

1.1 명령 모델 (Write Model)


public class AccountCommandModel {
    private String id;
    private BigDecimal balance;

    public void deposit(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Deposit amount must be positive");
        }
        this.balance = this.balance.add(amount);
    }

    public void withdraw(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Withdrawal amount must be positive");
        }
        if (this.balance.compareTo(amount) < 0) {
            throw new IllegalStateException("Insufficient funds");
        }
        this.balance = this.balance.subtract(amount);
    }

    // Getters and setters 여기에 추가
}

1.2 쿼리 모델 (Read Model)


public class AccountQueryModel {
    private String id;
    private BigDecimal balance;
    private List recentTransactions;

    // Getters 만 처리한다.
}

public class Transaction {
    private LocalDateTime timestamp;
    private BigDecimal amount;
    private String type; // "DEPOSIT" 또는 "WITHDRAWAL"

    // Constructor and getters 처리
}

 

팁: 쿼리 모델은 읽기 전용으로 설계하여 성능을 최적화하세요. 필요한 경우 비정규화된 데이터를 포함할 수 있습니다.

2. 명령 핸들러 구현

명령 핸들러는 시스템의 상태를 변경하는 작업을 처리합니다.


public class AccountCommandHandler {
    private final AccountRepository repository;
    private final EventPublisher eventPublisher;

    public AccountCommandHandler(AccountRepository repository, EventPublisher eventPublisher) {
        this.repository = repository;
        this.eventPublisher = eventPublisher;
    }

    public void handle(DepositCommand command) {
        AccountCommandModel account = repository.findById(command.getAccountId());
        account.deposit(command.getAmount());
        repository.save(account);
        eventPublisher.publish(new DepositedEvent(command.getAccountId(), command.getAmount()));
    }

    public void handle(WithdrawCommand command) {
        AccountCommandModel account = repository.findById(command.getAccountId());
        account.withdraw(command.getAmount());
        repository.save(account);
        eventPublisher.publish(new WithdrawnEvent(command.getAccountId(), command.getAmount()));
    }
}
        

 

팁: 명령 핸들러에서 도메인 로직을 실행하고, 결과로 발생한 이벤트를 발행하세요. 이는 이벤트 소싱과 CQRS를 결합할 때 특히 유용합니다.

3. 쿼리 핸들러 구현

쿼리 핸들러는 시스템의 현재 상태에 대한 정보를 제공합니다.


public class AccountQueryHandler {
    private final AccountQueryRepository repository;

    public AccountQueryHandler(AccountQueryRepository repository) {
        this.repository = repository;
    }

    public AccountQueryModel getAccount(String accountId) {
        return repository.findById(accountId);
    }

    public List getAccountsWithBalanceAbove(BigDecimal amount) {
        return repository.findByBalanceGreaterThan(amount);
    }
}
        

 

팁: 쿼리 모델은 필요에 따라 여러 개의 특화된 모델로 분리할 수 있습니다. 이를 통해 각 쿼리의 성능을 최적화할 수 있습니다.

4. 이벤트 핸들링

명령 측에서 발생한 이벤트를 처리하여 쿼리 모델을 업데이트합니다.


public class AccountEventHandler {
    private final AccountQueryRepository repository;

    public AccountEventHandler(AccountQueryRepository repository) {
        this.repository = repository;
    }

    @EventHandler
    public void on(DepositedEvent event) {
        AccountQueryModel account = repository.findById(event.getAccountId());
        account.setBalance(account.getBalance().add(event.getAmount()));
        account.getRecentTransactions().add(new Transaction(LocalDateTime.now(), event.getAmount(), "DEPOSIT"));
        repository.save(account);
    }

    @EventHandler
    public void on(WithdrawnEvent event) {
        AccountQueryModel account = repository.findById(event.getAccountId());
        account.setBalance(account.getBalance().subtract(event.getAmount()));
        account.getRecentTransactions().add(new Transaction(LocalDateTime.now(), event.getAmount(), "WITHDRAWAL"));
        repository.save(account);
    }
}
        

 

팁: 이벤트 핸들러는 비동기적으로 실행될 수 있으므로, 일시적인 불일치를 허용하는 설계가 필요합니다. 최종적 일관성(Eventual Consistency)을 고려하세요.

5. 인프라스트럭처 구성

CQRS 패턴을 지원하기 위한 인프라 구성 예시입니다.


@Configuration
public class CqrsConfig {
    @Bean
    public CommandBus commandBus() {
        return new SimpleCommandBus();
    }

    @Bean
    public QueryBus queryBus() {
        return new SimpleQueryBus();
    }

    @Bean
    public EventBus eventBus() {
        return new SimpleEventBus();
    }

    @Bean
    public AccountCommandHandler accountCommandHandler(AccountRepository repository, EventBus eventBus) {
        return new AccountCommandHandler(repository, eventBus);
    }

    @Bean
    public AccountQueryHandler accountQueryHandler(AccountQueryRepository repository) {
        return new AccountQueryHandler(repository);
    }

    @Bean
    public AccountEventHandler accountEventHandler(AccountQueryRepository repository) {
        return new AccountEventHandler(repository);
    }
}
        

 

팁: 명령 버스, 쿼리 버스, 이벤트 버스를 분리하여 각각의 처리 로직과 성능 요구사항을 독립적으로 관리할 수 있습니다.

6. CQRS 시스템 테스팅

CQRS 패턴을 적용한 시스템의 테스트 방법입니다.


@SpringBootTest
public class AccountCqrsTest {
    @Autowired
    private CommandBus commandBus;

    @Autowired
    private QueryBus queryBus;

    @Test
    public void testDepositAndQueryBalance() {
        String accountId = "acc123";
        BigDecimal depositAmount = new BigDecimal("100.00");

        commandBus.dispatch(new DepositCommand(accountId, depositAmount));

        AccountQueryModel account = queryBus.dispatch(new GetAccountQuery(accountId));
        assertEquals(depositAmount, account.getBalance());
        assertEquals(1, account.getRecentTransactions().size());
    }
}
        

 

팁: CQRS 시스템을 테스트할 때는 명령 실행 후 쿼리 모델이 적절히 업데이트되었는지 확인하는 것이 중요합니다. 비동기 이벤트 처리로 인한 지연을 고려하여 테스트를 설계하세요.

 

 

결론

CQRS 패턴은 복잡한 도메인 모델을 가진 시스템에서 큰 이점을 제공하는 패턴으로 읽기와 쓰기 모델을 분리함으로써 각각의 요구사항에 맞게 최적화할 수 있으며, 시스템의 확장성과 성능을 향상시킬 수 있습니다. 그러나 CQRS 적용시 구조가 갑자기 복잡해지기 때문에 적용 여부를 신중히 결정해야 합니다. 결국 시간 대비 성능 면에서 어느부분을 더 우선시하느냐가 관건이지 않나 싶습니다. 이런 부분은 사실 경험에 의존하는게 크다 보니 많은 시행착오를 통해서 느껴야 될 부분이 큽니다. 

 

추가 팁: CQRS를 구현할 때는 명령과 쿼리의 분리 수준을 프로젝트의 요구사항에 맞게 최대한 조절하는게 중요합니다. 간단한 애플리케이션에서는 동일한 데이터베이스를 사용하면서 모델만 분리하는 것으로 시작할 수 있고 규모가 큰 시스템에서는 완전히 별도의 데이터베이스를 사용하는 것도 고려해볼 수 있습니다. 또한, 이벤트 소싱과 CQRS를 함께 사용하면 더욱 강력한 아키텍처를 구축할 수 있습니다. 물론 이 또한 시스템의 복잡성을 크게 증가시킬 수 있으므로 고민해 볼 필요가 있겠습니다.

반응형