도메인 주도 개발 시작하기 요약정리

Sangwoon Park·2023년 4월 6일
0

https://www.hanbit.co.kr/store/books/look.php?p_code=B4309942517

1. 도메인 모델 시작하기

  • 도메인 모델이란 문제 해결 영역. 하위 도메인으로 나뉘어 질 수 있음
  • 도메인 전문가와의 대화가 중요. 개발자는 도메인에 대한 이해를 하고 있어야 함
  • 도메인 모델은 특정 도메인을 개념적으로 표현한 것. 객체 다이어그램이나 상태 다이어그램으로 모델링 가능. (그 외에도 가능) 모델링 표현 기법에 따른 구현 모델을 작성할 필요가 있음.
  • 하위 도메인도 모델을 만들어야 함.
  • 도메인 모델 패턴. 마틴 파울러가 제안한 아키텍쳐 패턴. 표현, 응용, 인프라스트럭쳐, 도메인 계층으로 나뉨.
  • 첫 설계 시에는 완전한 개념 모델보단 전반적인 개요를 알 수 있는 수준의 도메인 모델을 작성.
    • 프로젝트 초기엔 도메인의 전체적인 윤곽을 이해하는데 집중. 이후 구현모델로 점진적 발전.
  • 도메인 모델 도출. 핵심 구성요소, 규칙, 기능을 찾는 것부터 시작.
  • 도메인 모델은 엔티티밸류로 구분할 수 있음.
    • 엔티티의 가장 큰 특징은 식별자를 가진다는 것. 고유하고 변하지 않으므로 eqaul, hashcode 구현에 사용 가능.
    • 밸류 타입은 개념적으로 완전한 하나를 표현할 때 사용. 예를 들어 Receiver(addr, name, cellphone).밸류 타입은 불변 객체로 사용하는게 좋고, 변경이 필요할 경우 변경을 반영한 새로운 객체를 만드는 것을 추천. 비교가 필요할 경우 모든 속성이 같은지 비교.
  • 도메인 모델에 무분별한 setter 사용은 금물. 도메인 모델이 완전하지 않은 상태에서 사용될 수 있고, 어느 데이터가 변경되는지 확인하기 어려움. 객체 생성 시에 필요한 모든 데이터를 받는게 옳음.
  • 유비쿼터스 언어. 도메인 전문가, 관계자, 개발자 간에 도메인에 대한 공통 언어.

2. 아키텍쳐 개요

  • 표현 > 응용 > 도메인 > 인프라스트럭쳐
    • 표현영역은 사용자의 요청을 받아 응용 영역에 전달하고 결과를 받아 사용자에게 반환하는 역활.
    • 응용영역은 시스템이 사용자에게 제공해야하는 기능을 제공. 직접 로직을 수행하진 않고 도메인 모델에 로직 수행을 위임함.
    • 도메인 영역은 핵심로직을 구현
    • 인프라스트럭쳐 영역은 구현 기술에 대한 것을 다룸. ex) 메일 발송, db 연동
  • 일반적으로 상위 레이어가 하위 레이어에 의존하나, 인프라스트럭쳐 레이어에 의존하면 다른 구현 기술로 교체 시 코드 변경이 많이 필요 → DIP로 해결. 저수준 모듈이 고수준 모듈에 의존하게 만든다.
  • 단 인터페이스 추출 시 저수준 모듈 관점에서 추출하면 안되고 고수준 모듈 관점에서 추출.
  • 항상 DIP를 적용할 필요는 X. 도메인에 일부 인프라스트럭쳐 코드를 포함하는게 나을 수도 있음. 점진적으로 적용.
  • 애그리거트. 관련 객체들을 하나로 묶음. 군집을 관리하는 루트 엔티티를 갖음. 애그리거트를 사용하는 코드는 루트 엔티티를 통해 하위 엔티티 혹은 밸류에 간접 접근 해야함.
  • 모듈 구성은 ui, application, domain, infrastructure(domain layer에 injection 되도록 구현). 애그리거트 기준으로 각각 4개 레이어를 구성할 경우도 있음.

3. 애그리거트

  • 애그리거트는 일관성 관리 기준임. 한 애그리거트에 속한 객체들은 동일/유사한 라이프사이클을 가져야 함. 애그리거트는 경계를 갖는데 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다.
  • 함께 생성되는 구성요소는 한 애그리거트일 확률이 높음. 함께 변경되는 빈도가 높을 경우도. a has b 관계라고 같은 애그리거트이진 않음. 변경 주체가 다르다면.
  • 애그리거트 루트는 일관성 유지. 애그리거트 외부에선 애그리거트 루트의 메소드를 통해서만 접근하도록 구현 필요. 도메인 로직(상태 확인 로직) 또한 응용 서비스보단 애그리거트 루트를 통해 구현하는게 좋음.
    • 팁: 단순 set pulbic method를 만들지 않는다. 밸류 타입은 불변객체로 만든다.
  • 트랜잭션은 작으면 작을 수록 좋음. 한 트랜잭션에선 하나의 애그리거트만 수정. 따라서 애그리거트 내부에서 다른 애그리거트를 변경하는 기능을 실행(X). 필요 시엔 응용서비스 단에서 수정하도록 함.
  • 애그리거트 간은 ID를 통해 참조한다. 이로 인해 발생하는 조회 성능 관련 문제는 조회 전용 쿼리를 둬서 해결한다. → CQRS
  • 애그리거트 간 1:n 연관은 개념적으로 존재하더라도 실제 구현에 반영하지 않는다.
  • 애그리거트 루트는 팩토리 역활이 가능. 애그리거트의 데이터를 이용해 다른 애그리거트를 생성해야 한다면 애그리거트에 팩토리 메서드를 구현하는 것을 고려.

4. 리토지터리와 모델 구현

  • 구현 얘기라 딱히 정리할만한 내용은 업따
  • 앤티티는 @entity 로, 밸류는 @Embeddable 로, 밸류 타입 프로퍼티는 @Embedded로
  • 밸류 타입엔 기본 생성자가 필요없지만 클래스 매핑을 위해 제공해야함. protected로 구현하는게 맞음.
  • AttributeConverter를 통해 단일 컬럼에 밸류 타입 매핑 처리
  • 애그리거트 조회 시 완전한 상태일 필요는 없음. 그리고 하이버네이트의 eager fetch는 left outer join(cartesian) 이기에 fetch size가 너무 커지는 문제가 있음. 따라서 항상 eager 전략을 사용할 필요는 없음.
  • cascade 옵션으로 애그리거트에 속한 구성요소들을 한번에 저장/삭제 (CascadeType.PERSIST, Cascade.REMOVE)
  • dip 를 완벽히 지킬필요는 없다. jpa 특화된 태그를 사용해도 문제 없다는 뜻.

5. 스프링 데이터 JPA를 이용한 조회 기능

  • 가볍게 코드만 읽어보고 넘기면 좋음. p.190의 spec builder class는 유용할듯.
  • subselect 를 써먹을 곳이 있을 것 같기는 함. 단 join이 아닌 sub query의 형태로 운용하므로 성능 문제가 생길 수 있음을 유의

6. 응용 서비스와 표현 영역

  • 응용 서비스
    • 응용 서비스 메소드는 트랜잭션으로 묶여야 한다.
    • 응용 서비스에 도메인 로직을 넣지 않기. → 도메인 로직은 도메인 내부에 모아 응집도 상승
    • 응용 서비스 형태
      • 한 서비스 클래스에서 한 도메인의 모든 기능 구현 → 중복 제거가 쉬움. 코드 크기가 커짐.
      • 구분되는 기능별로 응용 서비스 클래스 따로 구현 → 클래스가 많아짐(헬퍼 static method로 해결), 연관된 코드들만 모여 코드 품질 상
    • 서비스의 인터페이스가 필요한 상황. 구현 클래스가 여러 개, 런타임에 구현 객체 교체 → 둘다 흔하지 않음. 따라서 필요성이 명확하지 않으면 클래스로 바로 구현하는게 나음.
    • 응용 서비스 메소드의 파라미터는 두 개 이상일 경우 dto 를 사용하는게 유리. 응용 서비스에서 애그리거트 자체를 반환하는 것은 피하는게 좋음.
    • 표현영역과 관련된 타입(구현 기술)을 응용 서비스에 파라미터로 전달하지 않기. (HttpServletRequest/Response) 세션 set 과 같은 동작을 응용 서비스에서 하지 않기.
  • 표현 영역
    • 주요 역활: 세션 관리, 응용 서비스 실행 결과를 알맞은 형식으로 제공.
    • 필수값, 값의 형식, 범위는 표현 영역에서 검사. 응용 서비스에선 논리적 오류만 검사. 응용 서비스에서 모두 검증 처리하는 경우도 있다. 이럴 경우 응용서비스의 완성도가 올라가지만 코드는 좀 복잡해진다.
    • 접근 제어하기 좋은 위치는 servlet filter. 인증 정보 생성, 검사하기 좋음. url 별 권한 검사 구현도 가능. 메서드 별 권한 검사가 필요할 경우 PreAuthorize를 통해 aop 적용. 각 도메인 객체 별 권한 검사가 필요한 경우 직접 구현 필요(응용 서비스 내부라던가…)
  • 조회 기능의 경우 응용 서비스가 필요하지 않을 수 있음(단순 호출 종료). 이 경우엔 응용 서비스를 생략하고 컨트롤러 → 조회 전용 기능 접근도 무방하다.

7. 도메인 서비스

  • 한 애그리거트에 넣기 애매한 도메인 기능을 억지로 특정 애그리거트에 구현하면 안됨. → 도메인 기능을 별도의 서비스로 구현. ex) 여러 애그리거트가 필요한 계산 로직이나 한 애그리거트에 넣기 복잡한 계산로직. 외부 시스템 연동이 필요한 도메인 로직.
  • 도메인 서비스 로직을 사용하는 주체는 애그리거트 혹은 응용 서비스. 애그리거트에서 사용할 경우 도메인 서비스 인스턴스를 메소드 파라미터로 전달.
  • 트랜잭션 처리는 도메인 서비스에서 수행하지 않는다.
  • 도메인 서비스의 구현이 특정 구현 기술이나 외부 api에 의존한다면 인터페이스로 추상화 함.
  • 도메인 서비스는 domain 패키지에 위치 시킴.

8. 애그리거트 트랜잭션 관리

  • 동시성 문제에 대한 이야기. 선점 잠금과 비선점 잠금. 기본적인 개념 설명 후 jpa 를 활용한 구현 설명.
  • 선점 잠금: 트랜잭션이 끝나기 전까지 다른 스레드가 해당 애그리거트를 수정할 수 없도록 함 → 다른 스레드는 블로킹. dbms 가 제공하는 행단위 잠금을 통해 구현(select for update). 교착상태가 발생할 수 있는데 최대 대기 시간 지정으로 회피. 단 이 방식은 dbms에 따라 지원할 수도 있고 안할 수도 있음.
  • 비선점 잠금: 커밋 전에 변경 가능 여부를 확인. “조회 - (다른 주체가 변경 수행) → 상태 변경 수행” 과 같은 상황에 사용. version field를 사용해 구현. version을 client로 부터 전달받는다면 충돌 여부 확인 범위를 넓힐 수 있음. 조회 후 트랜잭션 종료 시점에 업데이트 여부와 관계없이 버전 값 증가 시키는 모드도 있음.(OPTIMISTIC_FORCE_INCREMENT)
  • 오프라인 선점 잠금: 여러 트랜잭션에 걸쳐 선점할 때 사용. 첫 트랜잭션에서 잠금을 구하고 마지막 트랜잭션에서 잠금을 해제한다. jpa 에서 따로 제공하는 기능은 없어 보임.
    • 어플리케이션 단에서 LockManager(custom)와 같은 도구를 구현하는 방법.
    • db unique index를 활용한 LockManager 구현하는 방법. (lock type + lock target id 을 uix로)

9. 도메인 모델과 바운디드 컨텍스트

  • 바운디드 컨텍스트. 사용하는 용어를 기준으로 구분. 사용자에게 기능을 제공하는 물리적 시스템. 바운디드 컨텍스트 안에서 도메인을 구현하게 됨.
  • 하나의 애그리거트여도 서로 다른 바운디드 컨텍스트에서 사용된다면 각각의 모델을 갖는다. (이상적인 형태는 바운디드 컨택스트 당 1개)
  • 바운디드 컨텍스는 각각의 표현 영역, 응용 서비스, 도메인, 인프라스트럭쳐를 갖는다.
  • 바운디드 컨텍스트 간 통합은 rest api(직접), 메시지 큐(간접) 등이 있다.
profile
백엔드 개발자입니다.

0개의 댓글