푸념! 요즘은 과거 "성문법", "맨투맨"식 영어 교육시대를 살아가는 느낌입니다. 영어를 배우는건지 영어를 가르치기 위한 용어를 배우기 위해 공부를 하는지 알 수 없는 시대! 용어 보다는 실제로 경험해온 개발방법에 최근 정리된 방법론들을 해석해 봅니다. 

 

CQS ( Command Query Segregation )

CQRS를 이해하기 전에 고대 개발자들도 익숙하고 잘 사용하고 있는 CQS에 대해서 이해해보자.

CQS는 Command (변경) 와 Query ( 조회 )를 분리하자는 개념입니다. 

 

뭐 간단한 것부터 설명하면... 여러분이 아주 오랫동안 사용했던... DTO, VO들로 부터 찾을수있습니다.

class DataDTO{
	public int data;
}

위와 같은 메모리지 저장소(?^^)를 정의했다 합시다.

 

지금은 DTO저렇게 정의했다가는 뒷통수 맞겠지만 2Tier 시절에는 많이들 사용했지요.

이 DTO에서 data는 미지의 정제되지 않은 데이터입니다. 극단적으로 type이라도 변경되거나 하면 심각한 리페토링 이슈를 만들어 냅니다. 

 

그래서 간단히 read와 write를 분리하는 pattern을 만들어 냅니다. 

class DataDTO{
    private int data;
    
    public void setData(int value){
    	this.data = value;
    }
    
    public void getData(){
    	return this.data;
    }
}

data의 변경은 public method을 통하여서만 변경됨을 강제됨으로 더이상 data는 미지의 데이터가 아닙니다. 정제된 데이터라고 믿을수있겟지요. 여기서 조금더 확장해 봅시다.

 

class DataDTO{
    private int data;
    
    public void setData(int value){
    	this.data = value;
    }
    
    public void setData(String value){
    	this.data = Integer.parseInt(value);
    }
    
    public void getData(){
    	return this.data;
    }
}

data 변경함수가 추가되었지만... getData() 즉 query (read)부분은 영향받지 않습니다.

 

좀더 확장하면 query 용 dto와 update용 dto를 분리하면 역시 query / update의 변경으로 부터 query logic, update logic을 보호 할수있습니다. 

 

더 얘기하고 싶지만 글이 길어 질것 같으므로 각설하고... 

 

CQRS ( Command Query Reponsibility Segregation )

CQRS는 CQS에서 입/출을 강제 함으로써 변경에 대한 시스템 영향도를 줄이고자 했는데 개발해 보니 기-승-전-DB로 DB가 모든 성능저하의 주범이 되어 버립니다. 이에 아예 DB ( Store / Model / ... ) 도 분리해서 구현하면 어떻게느냐 하는것이 CQRS되겠다.

 

입력(command) -> 입력모델

출력(query) -> 출력모델

입력모델 -> [Aggregator] -> 출력모델

 

단순히 생각해봐도 Async 개념이 들어가 버렸다. 바로 "안될꺼 같은데 수많은 문제가 발생할것 같은데?"라는 생각을 하게될것이다. 

흥미와 빠른 이해를 위해서 실시간성이 들어간 CQRS를 구현한 예를 들어 보고자 한다.

 

다음과 같은 table이 존재한다.

 

[자산]

USER ID AMOUNT
1 1000
2 2000

Application은 위의 테이블을 사용하여 물품을 사고 팔때마다 amount를 조정하여 저장한다.

 

전통적 CRUD 방식으로 프로그램을 작성하면

COMMAND READ
void update(int userId, int change){
    # select amount from [자산] for update where user id = userId
   
    int changedAmount = amount + change;
   
    if(chnageAmount < 0){
        throw "잔액이 부족합니다."
    }

    #update 자산 set amount = chnageAmount
    #commit
}
int selectAmount(int userId){
    #select amount from [자산] where user_id = userId
}

simple합니다. 회원수가 1천, 5천, 1만, 100만 이렇게 늘어나고 1초당 100만건의 거래를 하는 사용자가 등장합니다.

 

update할때마다 lock을 걸기 때문에 조회에도 문제가 생깁니다.

 

하지만 문제 없습니다. 우리는 이미 CQS 패턴으로 시스템을 구성해 놓았기 때문에 "이벤트소싱"개념을 이용해서 물리적인 model을 분리하겠습니다.

 

COMMAND를 위한 테이블 하나를 추가하겠습니다.

 

[자산변동]

SEQ USER ID CHANGE
1 1 100
2 1 -50
3 1 -20

회원의 자산의 변동사항을 기록하는 테이블 입니다.

 

프로그램을 수정합니다.

COMMAND READ
void update(int userId, int change){
    # insert into [자산변동] values () 
}
int selectAmount(int userId){
    # select [자산].amount + SUM([자산변동].change)
        where user_id = userId
}

조회에 부담이 전가되었지만 더이상 LOCK은 없습니다.

 

어? [자산변동]이 무한정 늘어나면? READ에 문제가 생기겠는데? 단연합니다.

그래서 [자산변동] 테이블을 조금 더 변경합니다.

SEQ USER ID CHANGE MERGED
1 1 100 N
2 1 -50 N
3 1 -20 N

MERGED는 자산변동사항이 [자산]테이블에 반영되었음을 얘기합니다.

또 프로그램을 수정합니다. (이벤트 소싱 프로그램도 하나 추가합니다)

 

COMMAND READ Sourcing / Syncing
void update(int userId, int change){
    # insert into [자산변동] values () 
}
int selectAmount(int userId){
    # select [자산].amount + SUM([자산변동].change)
        where user_id = userId
            and merged = 'N'
}
int sync(int userId){
    # udpate
              set [자산].amount += SUM([자산변경].amount)
              set [자산변경].merged = 'Y'
          where user_id = userId
}

 

더이상의 read의 부담도 없고 lock의 부담도 획기적으로 줄었습니다.

 

[오늘의 한줄평!] 초기 구현부터 design pattern / architecture / framework 를 잘 정립하고 가자 나중에 변경과 장애 / 성능 이슈를 얼마나 빨리 해결하여 적시에 비지니스를 완성시키는 척도가 된다!!!

 

[덧붙여서] 사실 이 변경은 최초에 시스템이 CQS를 따르지 않았다면 변경자체가 불가능한 부분이다.

+ Recent posts