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

 

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를 따르지 않았다면 변경자체가 불가능한 부분이다.

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

 

C++ 부터 java를 거치면서 수 많은 시행 착오를 거치면서 나름데로 OOP 설계/구현 원칙을 강요(?)해왔다. 이런 설계 원칙을 누군가 외쿡사람이 정의해 해왔네 이걸 실무적으로 얘기해 보고자 한다.

 

 

  • S - Single-Responsiblity Principle (SRP)
    • 하나의 클래스는 하나의 일만 해야한다는 얘긴데...
    • 이건 실무적으로도 중요한 개념이다.
    • 하나의 기능에 관한 data / method / error등을 모아 둠으로써 변경에 대한 응집성을 높여서 변경에 대한 실수를 줄이고 코드에 대한 가독성 또한 높여준다.
    • 필수사항
  • O - Open-Closed Principle (OCP)
    • OOP의 지향점인 추상화와 다형성에 대한 원칙
    • 쉽게 확장가능해야하고 변경발생시 다른 확장들이 영향이 미치면 안된다? 요렇게 정의해 볼까한다.
    • 간단히 얘기하면 if{ } else if{ }  else if{ } else {} 이런거 하지 말라는 말이다.
    • 요건 좀 아키텍트로서는 고민되는 부분이다. 대부분의 도메인들이 extend할수있을 만큼 constant하지 않기 때문이다.
    • 동적 데이터에 대한 switch, if-else pattern을 벗어 나려면 "Adapter pattern"을 통해서 OCP 부족한 점을 채울 수 있을듯...
    • 어찌되었던 디자인 패턴이니까 최대한 OOP의 다형성을 활용하자
  • L - Liskov Substitution Principle (LSP)
    • sub class는 base class로 언제든 변환될 수 있어야 한다는 얘긴데...
    • 대부분의 언어들이 제공하는 casting을 통해서 sub class의 type을 몰라도 base class의 public property나 method에 접근가능해야한다는 얘긴데.....
    • java같은 signature가 있는 언어는 간단한데 C++같은건 조금 커스텀한 테크닉이 필요하다. 기본적으로 heap를 사용해야해서 C++같은 언어에서는 조금 유연하게 적용해도 될듯하다.
  • I - Interface Segregation Principle (ISP)
    • interface의 구현도 통합하지 말고 기능별로 분리해라 라는 얘기입니다.
    • 최신의 신규 언어들도 언어적으로 지향하는 원칙으로 지키는것이 좋습니다.
    • 너무 많이 나누면 interface관리 자체가 burden이 될것입니다. 
    • 기능을 잘 분리하여 적절히 나눕니다.
    • scale / rust는 이런 이유로 interface를 trait 이라는 특성으로 분리 합니다.
  • D - Dependency Inversion Principle (DIP)
    • 이것 역시도 상속을 통한 Event 수신을 하던 아주 오래된 패턴이다.
    • 공통로직을 base class에 담고 하위 class의 method을 참조할 필요가 있을때 추상화된 overriable method을 사용하여 하위 class에 종속된 기능을 추상화 하자는 얘기다.
    • 일반적으로 사용하는 callback, handler등이 이것에 해당된다.
    • 공통로직을 base class에 모아두고 sub class 특성을 담은 method만 추상화하여 로직의 통합한경우 즉 abstract class pattern들이 여기에 해당된다.

'architecture' 카테고리의 다른 글

PKI ( Private Key Infrastructure ) + X.509 + SSL  (0) 2023.05.31
Proxy ( Forward & Reverse )  (0) 2023.05.31
Agile(Scrum)에 대한 ...  (0) 2023.05.11
TDD ( Test Driven Development )  (0) 2023.05.11
EDD ( Event Driven Development )  (0) 2023.05.10

+ Recent posts