우당탕탕 우리네 개발생활

[도메인 주도 설계의 사실과 오해 5기] 2일차 내용 정리 본문

tech

[도메인 주도 설계의 사실과 오해 5기] 2일차 내용 정리

미스터카멜레온 2024. 9. 2. 16:58
조영호 님의 <도메인 주도 설계의 사실과 오해 5기> 오프라인 강의를 참여하면서 정리한 내용입니다. 조영호 님께서 강의해 주신 내용들을 제 입맛에 맞게 각색했기에 조영호 님께서 온전히 말씀해 주신 내용들과는 차이가 있음을 분명히 말씀드립니다. 내용에 오해 없으시길 바라겠습니다.

 

2일 차에서는 DDD의 구현파트에 집중하여 실제 구현 사례들로 개념들을 설명해 주셨습니다.

파트 1. 엔티티(Entity) & 값 객체(VO)

  1. DDD의 양극단
    1. DDD를 주니어가 해? DDD는 과해
    2. DDD를 하면 모든 게 해결이 가능해
    3. 결국 목적성이 너무 중요하다
    4. (중요) 적용하는 거 자체가 목표가 되면 안 된다
    5. 장점과 단점을 잘 고려해서 사용하기
  2. 단순히 엔티티와 값객체를 쓴다고 해서 DDD가 아니다
    1. 최대한 도메인 모델과 코드의 간극을 줄이는 게 DDD의 핵심
  3. 궁극적인 목표 = 복잡성을 낮추기
  4. 불변식을 추출하는 시기는 결국 요구사항이 왔을 때
    1. 이게 곧 애그리게이트의 경계
  5. DDD에서의 기능 구현(구현관점에서의 DDD는 사실상 이게 다다)
    1. 기능 요구사항 + 불변식
    2. 이걸 애그리게이트로 구현
  6. 엔티티와 값 객체
    1. 예를 들어, 쌍둥이가 있고 거의 모든 속성이 같은 상황에서
      1. 그냥 식별 자체가 중요하다면 엔티티로 구현
      2. 속성에 대한 비교만 중요하다면 값 객체로 구현
    2. 엔티티는 계속 트래킹이 가능해야 함
    3. 엔티티는 속성이 어떻든 상관없어 식별이 제일 중요해
      1. 얘가 걔야?
      2. 오직 식별자만 비교(동일성)
    4. 값 객체는 식별성은 모르겠고 속성이 같으면 같다
      1. 모든 속성을 비교하고 그 모든 속성이 같아야 함(동등성)
    5. 값 객체와 DTO를 헷갈린다?
      1. 값 객체는 데이터가 아니다
      2. primitive object들(String, Number 등) 은 전부 값 객체
      3. DTO는 불안정한 데이터들을 검증하기 전에 전부 허용해
      4. 하지만 엔티티나 값 객체는 절대 불안정하게 만들어지면 안 돼
      5. 엔티티의 속성을 줄이는 방법이 바로 값 객체를 활용하는 것
        1. JPA와 이를 매핑했을 때 값 객체는 임베더블과 같다
        2. 연관관계와 값객체를 헷갈리지 말자
    6. 가변객체 vs 불변객체
      1. 값 객체는 가급적 불변객체로 만들어라
        1. 값 객체의 목적 자체가 엔티티의 복잡성을 줄이는 것이라서
        2. 추적성이 없는 값 객체는 복잡성을 줄이는 데 기여함
      2. 엔티티는 어쩔 수 없이 가변객체야
      3. 아마 엔티티가 점점 커지면서 어떠한 속성을 알고리즘으로 설명하고 있다? 그러면 그게 값 객체로 분리될 가능성이 높아

파트 2. 애그리게이트(Aggregate)

  1. 불변식
    1. 트랜잭셔널 컨시턴시(트랜잭션 일관성)가 중요한 거고
    2. 이벤츄얼리 컨시턴시는 불변식에 해당되지 않는다
      1. 이벤츄얼 컨시턴시 vs 트랜잭션 컨시턴시
        1. 실시간과 관련되어 있음
        2. 이것들도 경계를 명확히 하기가 어렵다
        3. 트랜잭션 컨시턴시는 반드시 어떤 상태가 된 이후에 다음 스텝이 진행되어야 하는 것
        4. 이벤츄얼 컨시턴시는 시점에 상관없이 어떤 상태가 되기만 하면 되는 것이라 위와 느낌이 다름
  2. 애그리게이트
    1. 데이터 변경 단위
    2. 경계 바깥의 객체들은 해당 애그리게이트의 구성요소 중 루트만 참조가능
  3. 리파지토리
    1. DDD에서의 리파지토리의 취지
      1. 데이터의 출처가 어디든(레디스든, DB든 등) 관계없이 어플리케이션 메모리 상에 모든 객체들이 있어서 이를 이용할 수 있다고 착각하도록 하는 것이 원래 취지
      2. 현재는 그냥 저장소를 래핑하는 역할 정도?
      3. 요즘에는 모든 저장소를 아우를 수 있도록까지 리파지토리를 추상화하진 않는다. 각 저장소마다의 유즈케이스가 생각보다 다 다르고 성능도 다 다르기 때문에
      4. 애그리게이트 단위로 리파지토리를 추가
      5. 프로젝트 초반에 애그리게이트의 경계를 명확히 찾기란 쉽지 않다.
  4. 연관관계
    1. 도메인의 개념만 봤을 땐 아마 대부분의 케이스가 양방향의존으로 보일 것이다
      1. 이걸 탐색 빈도를 살펴서 결국 단방향으로 만드는 게 좋고, 몇 안 되는 어색한 경우들은 이를 따르는 편이 좋다
    2. 일대다 vs 다대일
      1. 웬만하면 다대일로 하는 것이 좋다?
      2. 그럼 애그리게이트는?
      3. 애그리게이트 내부에서는 무조건 일대다로 가는 경우가 많을 것이다
      4. 생명주기가 루트랑 같기 때문
    3. DDD는 앞서 얘기했었지만 CUD를 전제로 하고 있다.
      1. 조회에는 애그리게이트라는 용어 적용이 안돼
      2. CUD를 기준으로 애그리게이트를 얘기한다
    4. 애그리게이트 내부는 강결합으로 연관관계가 되어있고 나머지 연관관계는 리파지토리로 약한 결합을 하는 경우가 DDD의 방식이다
      1. 이를 통해 제대로 된 애그리게이트의 코드 구현은 한 눈에 의존성관계를 파악할 수 있음
        1. 루트 애그리게이트 엔티티의 코드에 인스턴스를 직접 멤버로 갖는 경우에는 그 인스턴스멤버들은 애그리게이트 내부 인스턴스들일 가능성이 높음
        2. 그냥 id 만 존재하는 경우엔 애그리게이트 내부 인스턴스 멤버는 아니고 의존성이 약하게 있는 다른 루트 애그리게이트 엔티티일 가능성이 높음
    5. 다른 루트 애그리게이트들의 id를 멤버로 갖고 이 id를 통해 리파지토리에서 조회
    6. 조회는 애그리게이트가 아니고 그냥 데이터로 봐야 한다.
      1. 조회 방법
        1. 리포지토리로 애그리게이트 단위로 조회한 다음에 이걸 DTO에 매핑해서 내리기
        2. 쿼리로 바로 DTO에 매핑해서 내리기
        3. CQRS를 철저하게 지켜서 read용 모델을 따로 만들어서 DTO에 매핑해서 내리기

파트 3. 애그리게이트 경계 설정 & 애그리게이트 구현하기

  1. 애그리게이트 안은 룰(의존성 방향에 대한 룰)이 조금 느슨할 수 있어
    1. 하지만 애그리게이트 밖의 경우(루트 애그리게이트끼리)는 반드시 의존성이 단방향 이어야 해
    2. 진짜 필요해서 양방향 거는 경우는 위임을 통해서 정도?
  2. 도메인 룰에 따라 애그리게이트 단위는 언제든 바뀔 수 있어
    1. 요구사항, 불변식 룰이 바뀜에 따라
    2. 애그리게이트 단위의 변경이 너무 과하다 싶으면 상황에 맞게 트레이드오프를 가져가도 돼
  3. DDD는 가급적 지연 로딩(Lazy Loading)을 하지 말자 주의
    1. 지연 로딩은 주로 다대일의 경우인데 애그리게이트 내부는 일대다로 이루어지기 때문에 지연 로딩이 맞지 않음
  4. 애그리게이트 내부는 유연하게 협력하면서 메시지를 보낸다
  5. 엔티티 vs 값 객체 모델링
    1. 식별자를 이용하는 게 편하다 하면 엔티티로 모델링하면 되고 단순하게 사용할 용도라면 값 객체로 모델링해도 된다. 상황에 맞게 모델링을 하면 되는 것
    2. 다 지우고 새로 다 밀어 넣어도 괜찮은 경우엔 값 객체를 사용해도 된다
    3. 세밀하게 조절해야 하는 경우 즉, 참조가 어플리케이션 내 여기저기에 있는 경우엔 엔티티로 모델링하는 게 낫다
    4. 값 객체는 웬만하면 멤버가 매우 적어야 하며 테이블로 분리할 정도면 다루기가 곤란 해질 수 있다

파트 4. 리파지토리 구현하기 & 팩토리와 서비스

  1. 리파지토리
    1. JpaRepository를 웬만하면 그냥 상속받아서 사용하는 경우가 많다. 더 순수하게 베이스리포를 만들어서 이를 구현하면서 가는 게 복잡한 경우가 많아
  2. 팩토리
    1. 의존성을 몰빵 할 때 팩토리를 사용
    2. 결국 생성자를 이용한 의존이 가장 강력한 의존인데 어느 한 곳에서는 책임을 몰빵 할 필요가 있음
  3. 서비스
    1. 도메인 서비스
      1. 도메인 로직인데 엔티티 안에 가두기는 애매한 경우
    2. 어플리케이션 서비스
      1. 이게 우리가 보통 얘기하는 서비스
      2. 어플리케이션 플로우를 관리
    3. 도메인 로직 vs 어플리케이션 로직
      1. 컴퓨터를 전부 빼고 설명가능 한 것(DB, 카프카 뭐 http 등등) vs 컴퓨터를 포함해서 설명할 수 있는
    4. 인프라스트럭쳐 서비스
    5. 도메인 서비스로의 분리는 가끔은 좀 미뤄도 좋은 경우가 많다
    6. 웬만하면 어플리케이션 서비스에 둬도 괜찮다(도메인 로직들을 잘 분리해 놨다는 전제하에)

Q&A 또는 TMI

  1. DDD와 Test코드의 상관관계
    1. 애그리게이트 단위로 테스트를 하게 되면 맞닿아있는 부분이 확실해서 테스트가 쉬울 것이다
  2. 애그리게이트와 동시성 이슈
    1. 애그리게이트가 동시성 이슈를 완벽히 방지할 수는 없다
    2. 다만 Lost 업데이트는 되면서 불변식은 지켜주는 경우다
    3. 애그리게이트로 취급한 데이터가 동시성이슈가 발생하는 경우는 잘 없다
  3. 객체지향이란 메모리에 객체들이 올라가 있을 때를 전제로 하는 철학이다
  4. 객체 멤버엔 null이 가급적 들어가면 안 된다
  5. VO를 사용할 때 메모리이슈?
    1. 가비지컬렉션의 성능이 워낙 좋고 요즘 리소스가 부족한 경우는 잘 없어서 이슈는 없다
    2. 애그리게이트 단위의 CUD를 다룰 때 보통 사용자향인 경우가 많기 때문에 그렇게 큰 사이즈의 데이터인 경우가 잘 없다
  6. id들을 값 객체로 매핑하는 이유?
    1. Long으로만 설정하는 것 보다 값 객체로 사용하게 되면 Long인 값들은 모두 들어갈 수 있는 부분에서 버그 발생이 가능
    2. 표현력이나 유지보수 측면에서 좋다
  7. 애그리게이트 내부는 무조건 1:N이 맞다
    1. 애그리게이트와 애그리게이트 사이는 무조건 방향성을 잘 지켜주는 게 좋다
  8. 애그리게이트의 fetch성 수정도 애그리게이트 단위로 전부 데이터를 끌어온 다음에 객체 생성을 하는 게 맞다
  9. 실제로 정규화를 엄격하게 하는지?
    1. 가장 엄격하게 할 경우 class와 table이 1:1이 되는 경우가 이상적. nullable이 안 생김
    2. 그러나 실질적으로 관리측면에서 어렵기 때문에 역정규화를 하는 경우도 많다
  10. 이벤트 소싱과 CQRS 간의 관계
    1. 이벤트 소싱은 조회를 기본적으로 염두에 두고 있지 않아
    2. 조회를 할 때는 조회에 맞도록 만들어놓은 데이터를 이용해야 함
    3. 그래서 CQRS를 엮어야 한다는 말임
  11. 레거시 단위테스트는 일단 가져와서 new 해보는 것부터
    1. 만약 의존성 문제가 생긴다면 여기서부터 이제 의존성 어디 끊고 갈지 고민