DesignPatterns

CQRS 패턴: 데이터 읽기와 쓰기를 분리하여 성능과 확장성 극대화하기

누구세연 2024. 11. 14. 07:03

복잡한 애플리케이션은 데이터 읽기와 쓰기를 분리해야 하는 경우가 생깁니다. 최근에 수많은 조회와 데이터 변경 요청이 발생하며, 트래픽과 확장성 측면에서 읽기와 쓰기를 분리할 필요가 생기면서 CQRS에 대해 알게 되었는데요! 🫠
CQRS는 전통적인 애플리케이션 구조와는 다르게, 명령(Command)과 조회(Query)의 책임을 구분하여 각기 다른 모델로 설계함으로써 성능, 확장성, 보안성을 향상하는 데 초점을 맞추는 디자인 패턴입니다.
 
이 글에서는 CQRS 패턴에 대해 정리해보겠습니다!👀
 

CQRS 패턴이란?

CQRS는 Command Query Responsibility Segregation의 약자이며 명령과 조회를 분리하여 처리하는 패턴입니다.
데이터 읽기와 쓰기를 작업을 분리함으로써 성능, 확장성, 데이터 일관성을 최적화하는 데 도움을 줍니다.
 

CQRS 패턴 다이어그램

  • 명령(Command): 데이터의 변경을 유발하는 작업(예: 데이터 생성, 수정, 삭제)
  • 조회(Query): 데이터 읽기만을 수행하는 작업(데이터 변경 없음)

 

CQRS 패턴의 기본 원칙

  • 명령(Command): 시스템에 데이터를 변경하는 작업입니다. 예를 들어, 데이터 생성, 수정, 삭제 작업이 이에 해당합니다. CQRS에서는 데이터를 수정하는 부분은 별도의 모델로 관리하여, 쓰기 전용 모델을 사용합니다.
  • 조회(Query): 데이터를 조회하고 읽기만 하는 작업입니다. 조회는 별도의 읽기 전용 모델을 사용하여 데이터를 가져오는 데 집중합니다.

각 작업을 독립적으로 관리하여 읽기 모델과 쓰기 모델을 분리하는 것이 특징입니다.
 

CQRS 패턴의 장단점

장점

  • 성능 최적화: 쓰기와 읽기 모델을 따로 최적화할 수 있기 때문에 트래픽이 높은 시스템에서 유리합니다.
  • 확장성: 읽기와 쓰기 모델을 독립적으로 확장할 수 있어, 읽기 요청이 많은 시스템에서는 읽기 전용 복제본을 추가하는 방식으로 확장할 수 있습니다.
  • 보안: 읽기와 쓰기 작업에 대해 접근 제어를 따로 설정할 수 있어 보안적으로도 유리합니다.
  • 유연성: 읽기와 쓰기에 맞춘 다른 데이터 저장소나 데이터 모델을 사용할 수 있습니다.

단점

  • 구현 복잡도: 읽기와 쓰기 모델을 따로 설계해야 하므로 코드 구조가 복잡해질 수 있습니다.
  • 데이터 일관성 관리: 쓰기 작업이 수행된 후에 읽기 모델에 반영되기까지 지연이 발생할 수 있습니다. 이를 이벤트 소싱과 함께 사용해 해결하는 경우가 많습니다.
  • 추가 인프라 필요: 데이터 일관성을 보장하기 위해 이벤트 시스템을 추가로 도입하는 경우, 인프라가 복잡해질 수 있습니다.

 

CQRS 패턴을 사용하는 경우

  • 복잡한 비즈니스 로직이 많고 읽기와 쓰기 요구 사항이 명확히 다른 경우.
  • 읽기 트래픽과 쓰기 트래픽이 불균형한 시스템, 예를 들어 조회 요청이 월등히 많은 경우.
  • 데이터 일관성 지연이 용인 가능한 경우 또는 이벤트 소싱을 통해 일관성을 보장할 수 있는 경우.

 

CQRS 패턴과 이벤트 소싱

이벤트 소싱(Event Sourcing)은 CQRS와 자주 함께 사용됩니다. 이는 데이터의 변경 사항을 이벤트 형태로 저장하여 데이터의 상태를 재생성하는 방식입니다. 모든 변경 사항이 이벤트로 저장되기 때문에, 이를 통해 현재 상태를 추적하고 과거 상태로 복구하는 것이 가능합니다.

이벤트 소싱의 장점과 단점

  • 장점: 모든 상태 변화를 저장해 비즈니스 흐름을 추적할 수 있고, 데이터 일관성을 보장하기 쉽습니다.
  • 단점: 이벤트를 저장하고 재생하는 추가 인프라와 로직이 필요해 시스템이 복잡해질 수 있습니다.

 

Command 예제 - 사용자 정보 수정 클래스

UpdateUserCommand 클래스는 사용자 정보를 수정하는 작업을 수행합니다. 사용자의 ID와 새로운 이메일 주소를 인자로 받아, 데이터베이스에서 해당 사용자를 찾아 정보를 업데이트합니다.

// 명령(Command) 클래스: 사용자 정보를 수정하는 예제
public class UpdateUserCommand {
    private final String userId;   // 수정할 사용자의 ID
    private final String newEmail; // 사용자의 새로운 이메일

    // 생성자: 사용자의 ID와 새로운 이메일을 받아 객체 생성
    public UpdateUserCommand(String userId, String newEmail) {
        this.userId = userId;
        this.newEmail = newEmail;
    }

    // 명령 실행 메서드: 데이터베이스에서 사용자 이메일을 업데이트
    public void execute() {
        // userRepository를 통해 데이터베이스 접근
        userRepository.updateEmail(userId, newEmail);
    }
}

 

  1. 필드 선언:
    • userId: 수정하려는 사용자의 ID를 나타내는 String 타입의 필드입니다.
    • newEmail: 변경할 이메일을 저장하는 String 타입의 필드입니다.
  2. 생성자:
    • UpdateUserCommand(String userId, String newEmail): 수정할 사용자의 ID와 새로운 이메일 주소를 매개변수로 받아 객체를 생성합니다. 이로써 Command 인스턴스가 변경해야 할 데이터를 담게 됩니다.
  3. execute() 메서드:
    • execute() 메서드는 Command를 수행하는 함수로, userRepository를 통해 사용자 데이터를 업데이트하는 역할을 합니다.
    • 이 메서드는 userRepository.updateEmail(userId, newEmail)을 호출하여 사용자의 이메일을 새로운 값으로 변경합니다.
userRepository는 데이터베이스와 상호작용하는 역할을 담당하는 클래스입니다. UpdateUserCommand는 userRepository가 제공하는 업데이트 메서드를 호출하여 실제 쓰기 작업을 수행합니다.

Query 예제 - 사용자 정보 조회 클래스

UserQuery 클래스는 사용자 정보를 읽어오는 작업을 담당하며, 데이터베이스에서 사용자 정보를 조회하여 반환합니다. 이 클래스는 읽기 전용 작업이므로, 데이터 변경을 수행하지 않습니다.

// 조회(Query) 클래스: 사용자 정보를 조회하는 예제
public class UserQuery {
    private final String userId;  // 조회할 사용자의 ID

    // 생성자: 조회할 사용자의 ID를 받아 객체 생성
    public UserQuery(String userId) {
        this.userId = userId;
    }

    // 조회 실행 메서드: 읽기 전용 데이터베이스에서 사용자 정보 조회
    public User execute() {
        // readOnlyUserRepository를 통해 데이터베이스 접근
        return readOnlyUserRepository.findById(userId);
    }
}

 

  1. 필드 선언:
    • userId: 조회하고자 하는 사용자의 ID를 저장하는 String 타입의 필드입니다.
  2. 생성자:
    • UserQuery(String userId): 조회할 사용자의 ID를 매개변수로 받아 객체를 생성합니다. 이를 통해 Query 인스턴스가 조회할 대상을 담게 됩니다.
  3. execute() 메서드:
    • execute() 메서드는 Query를 수행하는 함수로, readOnlyUserRepository를 사용하여 데이터를 읽는 작업을 수행합니다.
    • readOnlyUserRepository.findById(userId)를 호출하여 데이터베이스에서 사용자의 정보를 조회하고, 조회된 User 객체를 반환합니다.
readOnlyUserRepository는 읽기 전용 저장소로, 데이터 조회만 가능하도록 설계된 클래스입니다. 이와 같이 CQRS 패턴에서는 쓰기와 읽기를 위한 저장소를 분리하여, 각 작업이 최적화된 환경에서 수행될 수 있도록 돕습니다.

 
 

CQRS 패턴의 실제 적용 사례

  • e-커머스 플랫폼이나 은행 시스템처럼 읽기와 쓰기 트래픽이 다르고 데이터 일관성 요구가 높은 시스템에서 많이 사용됩니다.
  • 대규모 애플리케이션에서 읽기와 쓰기 데이터가 다른 워크로드를 가지는 경우 CQRS가 적합합니다.

 

 

💡 CQRS는 디자인 패턴의 일종으로, 읽기와 쓰기 작업의 책임을 분리하여 애플리케이션의 성능과 확장성을 높이는 데 중점을 둡니다. 도메인 모델 패턴이나 트랜잭션 스크립트 패턴과는 달리, 데이터 처리 모델을 분리하는 설계 방식으로, 복잡한 시스템의 성능을 최적화하는 데 유용한 패턴입니다.