kok202
오브젝트 디자인 스타일 가이드 (2/2) CQRS, 추상화, 이벤트, 책임

2022. 6. 6. 18:33[공부] 독서/오브젝트 디자인 스타일 가이드

http://www.yes24.com/Product/Goods/91167539

 

오브젝트 디자인 스타일 가이드 - YES24

잘 작성한 객체지향 코드는 읽고 변경하고 디버그하기 즐겁다. 이 책에서 보여주는 객체 디자인에 대한 보편적 모범 사례를 익혀 코딩 스타일을 향상하자. 이 명확한 규칙은 어떤 객체지향 언어

www.yes24.com

코드 레벨에 대한 설명이 많아 이는 생략하고 보편적인 가치 위주로 정리합니다.

(😛) 은 사견입니다.

 

변경 메소드가 반환 값을 갖게 하지마라

  • 변경과 반환이 둘다 있으면 클라이언트 입장에서 혼란스럽다.
  • 메소드는 항상 명령 메소드 이거나 질의 메소드 이어야한다. (CQRS)
  • 테스트를 위해 종종 command 메소드가 return this 를 하는 경우도 있는데, 이것도 피해야할 패턴중 하나다. 명령 메소드의 반환 타입은 void 여야한다는 규칙을 위반한다.

 

변경 메소드 on VO

VO 의 명령 메소드는 서술형이어야한다.

// 이것보다
final class Position
{
    public function moveLeft(int steps): Position
    {
    }
}

// 이게 낫다
final class Position
{
    public function toTheLeft(int steps): Position
    {
    }
}

(😛 moveLeft는 속성을 변경할 것 같지만, toTheLeft는 새로운 값이 될것 같음을 암시한다. 메소드 이름을 전치사로 시작해보려 하면 좋을 듯하다)

 

변경 메소드 on Entity

  • Entity 는 VO 와 다르게 조작을 허용하는 메소드가 있다.
  • 변경 가능 객체의 변경 메소드는 명령 메소드이어야한다. (반환 값이 void 타입의 메소드)
  • 변경 메소드는 상태 변경 요청이 유효한지까지 확인해야한다.
  • 변경 메소드의 테스트는 테스트를 위해 getter 를 만들지 말고 event record 방식으로 검증해라.
public function moveLeft_테스트(): void
{
    // given
    player = new Player(new Position(10, 20));
    
    // when
    player.moveLeft(4);
    
    // then
    assertTrue(player.recordedEvents().contains(new PlayerMoved(new Position(6, 20))));
}

 

Fluent interface

변경 가능한 객체에는 fluent interface 를 구현하지 않는다.

(😛 fluent interface 에서 체이닝을 위해 반환할 경우 변경가능한 객체를 반환해서는 안된다.) 

queryBuilder = QueryBuilder.create(); // fluent interface 를 이렇게 사용할 경우

qb1 = queryBuilder 
    .select(/* ... */)
    .from(/* ... */)
    .where(/* ... */)
    .orderBy(/* ... */);

qb2 = queryBuilder // 제대로 동작할지 예측할 수 없다.
   .select(/* ... */)
   .from(/* ... */)
   .where(/* ... */)
   .orderBy(/* ... */);

 

CQRS

  • 메소드는 항상 명령 메소드 이거나 질의 메소드 이어야한다.
  • 명령 메소드는 반환 타입이 void 여야한다.
  • 명령 메소드의 이름은 클라이언트가 객체에 나타내는 작업을 수행하기 지시할 수있는 명령형 이름을 사용하는 것이 좋다.
  • 명령 메소드가 너무 많은 일을 하지 않는 것이 좋다. 부차적인 작업이 있다면 유효 범위를 제한하고 이벤트를 pub/sub 하는 것이 좋다.
  • 질의 메소드는 반환 타입이 단일 타입이어야한다.
  • 데이터 수집에는 질의 메소드를 사용하고 다음 단계로 진행할 때는 명령 메소드를 사용한다.
  • 항상 CQRS 가 해답인 것은 아니다.

 

이벤트

이벤트를 사용한 설계는 다음과 같은 장점이 있다.

  1. 원본 메소드를 변경하지 않고도 더 많은 부수 효과를 추가할 수 있다.
  2. 부수적인 효과에만 필요한 의존성을 원본 객체에 주입하지 않으므로 원본 객채를 더욱 분리할 수 있다.
  3. 원한다면 백그라운드 프로세스에서 부수 효과를 처리할 수 있다.

 

시스템 경계

시스템 경계를 넘는 명령에는 추상화를 정의한다.

 

find vs get

메소드 이름으로 불확실성을 보여주기 위해 find 를 쓸 수 있다. 

 

get 접두어를 남발하지 마라.

get 접두어는 갖고 있는 정보를 제공한다는 의미다. 찾아오라는 지시가 아니다.

class Products {
    public int sumPrice() {
        return this.products.stream().mapToLong(Product::getPrice).sum();
    }
}

class Products {
    // BAD
    public int getSumPrice() {
        return this.products.stream().mapToLong(Product::getPrice).sum();
    }
}

 

Mock 프레임워크

⭐⭐⭐ Mock framework 는 일반적으로 좋은 디자인이 나오는 것을 방해한다.

반복적인 코드 몇 줄을 줄일 수 있지만 코드를 읽고 유지하기 어려운 비용이 생긴다.

 

명령 메소드 호출은 Mock 으로만 검증한다.

명령 메소드가 다른 명령 메소드를 호출할 때는 후자를 Mock 으로 만드는 것이 좋다. 명령을 적어도 한 번은 호출해야 한다 하더라도 두 번 이상 호출해서는 안된다.

 

테스트 대역의 분류

사용처 이름 설명
질의 메소드에 사용 Dummy 타입만 바른 비기능 객체
Stub 타입이 올바르고 지정된 값을 반환하는 객체
Fake 타입이 올바르고 자체 논리가 있는 객체
명령 메소드에 사용 Mock 메소드 호출을 확인하기 위한 객체
Spy 메소드 호출을 나중에 확인하기 위한 객체 (호출된 내용을 기록함)

(😛 이 5가지에 대해서는 항상 포스팅이나 책마다 내용이 조금씩 달라서 혼란스러운데, 일단 이 책의 정의가 제일 단순하고 와닿는 듯.)

 

책임 나누기

읽기와 쓰기 모델을 분리한다.

Entity를 변경할 권한이 없는 클라이언트에 변경할 수 있는 Entity 를 전달해서는 절대 안된다. 이렇게되면 어느날 갑자기 클라이언트가 Entity를 수정하려할 수도 있다. 이는 결국 프로그램에 일어난 일을 파악하기 어렵게 만든다. 디자인 개선을 위해 가장 먼저 해야할 일이 쓰기 모델과 읽기 모델을 분리해야하는 이유다.

 

읽기 모델과 쓰기 모델을 분리하는 과정

책임을 분리하는 과정을 Step by step 으로 나눔.

읽기 모델은 쓰기 모델에서 직접 생성할 수 있다. 하지만 더 효율적인 방법은 쓰기 모델에서 사용하는 데이터 근원에서 생성하는 것이다. 이렇게 할 수 없거나 읽기 모델을 효율적인 방법으로 생성할 수 없으면 도메인 이벤ㅌ를 사용해 오랜 시간에 걸쳐 읽기 모델을 구축한다.

 

이벤트 소싱

읽기 모델을 구축하는 데 이벤트를 사용하는 것 외에도 쓰기 모델을 재구성하는데 이벤트를 사용하면 상황이 훨씬 더 복잡해진다. 이 기법을 이벤트 소싱이라 하는데, 쓰기 모델과 읽기 모델을 분리하는 발상에는 매우 잘 들어맞으나 객체 사이에 책임을 나누는 더 좋은 방법만 찾는다면 굳이 필요하지는 않다.

 

서비스 행위 변경하기

  • 클래스를 변경하는 일반적인 대안은 매소드를 재정의하는 것인데, 이는 더 많은 문제를 일으킬 수 있다.
  • 일반적으로 클래스 코드 대신 객체 그래프 구조를 변경하는게 더 낫다.
  • 일부를 변경하는 것보다 대체하는 게 더 낫다.

 

상속을 지양해라.

흔히 코드를 재사용하기 위해 상속을 사용하지만 코드를 재사용하는 데 더 강력한 형태는 Composition 이다. 하지만 경우에 따라 의존성 주입을 지원하지 못하는 경우라면 trait 을 사용하길 권한다. trait 은 평범한 코드 재사용, 즈 컴파일러 수준의 코드 복사/붙여넣기다.

(😛 서비스 행위를 변경하고 싶다면 상속보다 추상화된 Comosition 을 사용하자)

(😛 상속은 리스코프 치환 원칙을 고려하면서 진행되야한다. 굳이 어려운 치환 원칙까지 고려해서 상속할 생각하기보다 그냥 Composition 을 사용해라.)

 

클래스는 기본적으로 final 로 선언해라.

  • 서비스는 클래스를 fianl 로 표시해야함을 이미 주장했다. 상속대신 객체 구성을 사용해 행위를 변경하는게 더 좋고 유연한 방법이다.
  • Entity, VO 와 마찬가지로 다른 객체 타입에도 유사한 질문을 해야한다. final 이어야할까? 그래야한다.
  • 이 규칙의 유일한 예외는 객체 상속 계통을 선언할 때다. 이때는 부모 클래스를 확장해 이런 객체 사이 관계를 나타낼 수 있다.
  • 그리고 공개 인터페이스가 아닌 모든 메소드는 private 으로 만들어라.

 

이벤트 디스패처

이벤트 디스패처 방식을 사용할 때 장점은 기존 논리를 변경하지 않고 서비스에 행위를 추가할 수 있다는 점이다.

이벤트 디스패처를 사용할 때 단점은 이름이 매우 일반적이라는 점인데, 이벤트를 발행할 때 하는 dispatch 라는 메소드가 이면에 어떤일이 일어나는지를 명확하게 하지 못한다. 또한 어떤 이벤트에 어느 subscriber 가 응답하는지 알아내는 것이 어려울 수도 있다. 대안은 추상화를 도입하는 것이다.

 

추상화 vs 구현체

  • Controller -> concrete 
  • Service -> concrete
  • Entity, VO -> concrete
  • Repository -> interface

 

기타

  • 객체에 담긴 데이터를 상태라고도 한다.
  • SUT (Subject undert test): 테스트할 대상 (😛: System under test 가 좀 더 보편적인 어휘인 듯)
  • 규칙을 따른 넋이 중요하지만 품질이 그리 중요하지 않은 예외적인 상황에는 유연하게 임한다. 때로는 공들인 만큼 보상이 따르지 않을 수 있디 때문이다.
  • 대부분의 개발자는 객체가 아니라 클래스를 테스트하는 경향이 있다. 객체를 테스트하라. 클래스 테스트는 클래스 자체가 바뀔 때마다 항상 바뀐다. 객체 테스트는 테스트하는 객체의 구현 내용과 더욱 분리되어 있다. 객체 테스트가 장기적으로 유용하다.

 

서평

서비스란 무엇인가에대한 생각이 많이 담긴 책이었습니다. 목차만 읽어도 도움되는 내용이 많은 책이라고 생각합니다. 자바랑 스프링에 대한 내용은 한번도 언급하지 않는데, 오히려 스프링을 이해하는데 더 도움을 주었습니다. 매번 설계 관련 책을 읽을 때마다 느끼는 것은 DDD, TDD, 함수형 프로그래밍들이 사실 OOP 하나를 바라보고 있다는 것입니다. 전혀 기대 안했는데, 근래에 읽은 책중에 또 영감을 많이 준 책이라 감명 받아 정리글을 남깁니다.

 

아래는 원서를 공개한 것으로 추정되는 사이트인데요. 영어를 잘하신다면 여기서도 읽을 수 있을 것 같아 좋아보입니다.

https://www.manning.com/books/object-design-style-guide

 

Object Design Style Guide

Elevate your coding style by mastering the universal best practices for object design. Explore techniques for creating pro-quality OO code that can stand the test of time.

www.manning.com