brunch

You can make anything
by writing

C.S.Lewis

by 에디의 기술블로그 May 29. 2021

[스터디] 도메인 주도 설계 3주차
(긴글주의...)

도메인 서비스, 애플리케이션 서비스, 테스트 코드 작성하기

해당 글은 작년 스터디 중 작성한 글입니다. 잘못된 내용이 있어서 잠시 취소 처리하였습니다. 조만간 새로운 글을 작성해서 발행하겠습ㄴ다.


도메인 주도 설계에 대해서 전혀 모른다면, 필자의 이전 글을 먼저 읽어보길 바랍니다.

https://brunch.co.kr/@springboot/605

https://brunch.co.kr/@springboot/607



목차 (3주차 이후는 미정)

[1주차] 1. 도메인 주도 설계
[1주차] 2. 전략적 설계

[1주차] 3. 전술적 설계

[2주차] 4. 값객체

[2주차] 5. 엔티티

[2주차] 6. 애그리거트

[3주차] 7. 도메인 서비스

[3주차] 8. 애플리케이션 서비스

[3주차] 9. 테스트 코드 작성하기


미정

[4주차] 10. 리포지토리

[4주차] 11. 리포지토리 구현

[5주차] 12. 아키텍처

[5주차] 13. 의존성

[5주차] 14. 팩토리

[6주차] 15. 도메인 이벤트

[6주차] 16. 데이터 무결성

[7주차] 17. 전략적 설계(바운디드 컨텍스트, 컨텍스트 매핑 등)

[8주차] 18. 실무 사례 검토



3주차 글을 읽기 전에


이 글을 이해하기 위해서는 도메인 주도 설계(이하 DDD)의 기본 개념을 이해하고 있어야 한다.


- 전략적설계, 전술적 설계의 차이

- 도메인 모델, 모델링, 도메인 객체란 무엇인가?

- 엔티티, 값객체, 애그리거트

- 유비쿼터스 언어


전략적 설계 & 전술적 설계

기본적으로 전략적 설계, 전술적 설계에 대한 차이를 이해하고 있어야 한다. 비록 이 글은 전술적 설계 위주의 내용이지만, "전략적 설계"의 중요성을 소홀히하면 안된다. (전략적 설계에 대해서는 소홀하면서) "전술적 설계" 위주의 글을 쓰는 것에 대해서 욕하는 몇몇 분들이 있을 것이다. 필자가 많이 부족하니, 공부 방법을 조언해주면 감사하겠다.


도메인 지식, 모델, 모델링, 도메인 객체

소프트웨어를 복잡하지 않고 심플하게 만들기 위해서 가장 중요한 것은 무엇일까?

- 도메인 지식 및 궁극적으로 해결해야 하는 요구사항에 대해서 정확히 파악하는 것이 중요하다.


도메인 지식은 개발자에 의해서 도메인 객체로 구현된다. 이때, 도메인 객체에는 엔티티, 값객체, 애그리거트 등이 되는데, 개발자는 불필요하게 복잡한 소프트웨어를 만들 필요가 없다. DDD의 목적은, 자랑하기 위해서 소프트웨어를 만드는 것이 아니다.


겉멋든(?) 소프트웨어는 필요 없다.

도메인 지식 및 소프트웨어의 문제를 해결하기 위함이 개발자의 책임이다.


엔티티, 값객체, 애그리거트

이 글에서는 자세한 내용은 생략한다. 잘 모르는 개발자는 필자의 지난 글을 읽어보거나, "에릭 에반스"의 참고자료를 읽어보고 다시 돌아오길 바란다.


유비쿼터스 언어

유비쿼터스 언어를 잘 정의하고, 소프트웨어에 제대로 반영한다면, 주석이 필요 없을 것이다. 소프트웨어에 주석이 없어도 이해하기 쉽도록 소프트웨어를 만들어보자.


DDD의 "엔티티"는 JPA의 "엔티티"가 아니다.

필자의 스터디에서 3주차까지 데이터베이스 구현체에 대해서는 상세하게 얘기하지 않는다. NoSQL이 될지, RDBMS 가 될지 모른다.


도메인 지식에 집중해보자. 데이터베이스는 옵션일 뿐이다.


DDD 에서의 "엔티티"와 JPA 의 @Entity는 같은 의미가 아니다. 구현 방법에 따라서 같은 클래스로 매핑할수도 있고, 별도의 클래스로 분리할수도 있는데, 일부 개발자는 엔티티 라는 단어를 명확하게 구분하지 않고 사용한다. 지금까지 계속 얘기했던 엔티티는 JPA 엔티티와는 전혀 상관이 없다. (JPA는 추후 4주차 스터디에서 생각하기로 하자.)


전략적 설계의 중요성에 대해서 모르는게 아니다.

이 글을 읽는 일부 전문가는 필자의 글이 전략적 설계에 대해서 소홀히하고, 전술적 설계 위주로 공부한다고 필자를 엄청 욕할것이다. 전략적 설계의 중요성을 모르는 것이 아니다. 어떻게 공부해야할지 모르는 것이다. 조언을 많이 해주길 바란다.


전략적 설계는 어떻게 공부하면 되나?


리포지토리 임시 구현체

리포지토리 구현체를 임시로 구현하였는데, 4주차에서는 RDBMS 또는 NoSQL 에 연동할 것이다.

(필자 포함)대부분 개발자는 데이터베이스 중심으로 설계하는 경향이 있는데, "도메인 주도 설계"에서 도메인 모델은 인프라(데이터베이스)에 의해서 오염되면 안된다. 즉, 이론적으로 "도메인 주도 설계"에서 데이터베이스는 중요하지 않다.


(하지만, 솔직히 필자처럼 평범한 개발자는 데이터베이스를 빼놓고 설계하는게 쉽지 않다. 완벽한 DDD 는 정말 가능할까?)


스터디 공고를 스프링부트&JPA 로 배우는 도메인주도 설계라고 모집했는데, 사실 애초에 처음부터 JPA 따위는 생각하지 말았어야 했다.


2주차 이후 일부 변경된 코드

엔티티, 애그리거트 루트, 값객체 에 대한 인터페이스를 정의하였다.

애그리거트 루트 는 엔티티 중에서 하나가 된다. 그래서, 애그리거트루트는 엔티티를 상속한다.


값객체는 아래와 같다.

애그리거트 루트 는 아래와 같다.

엔티티는 아래와 같다.


공통적으로 정의해야하는 기능이 있다면 선언해주면 된다. 필자의 샘플 코드에서는 아직 공통 메서드를 제공하진 않는다. 나중에 추가해보겠다.


패키지 구조

필자는 아래와 같은 패키지 구조를 정의하였다.

단, 추후에 변경될 가능성이 높다. 공부하면서 계속 개선해나가겠다.


글이 엄청 지루하고, 엄청 길다.

어쩌다 보니 글이 매우 길어졌다. 많이 지루하고 설명이 친절하지 않다. 필자의 글을 읽고 싶지 않은 개발자는 아래 도서를 읽어보길 바란다.


[1] 도메인 주도 설계 핵심 (반 버논 지음, 에이콘 출판사)

[2] 도메인 주도 설계 철저 입문 (나루세 마사노부 지음, 위키북스)

[3] 도메인 주도 설계 - 소프트웨어의 복잡성을 다루는 지혜 (에릭 에반스 지음, 위키북스)

[4] DDD START! 도메인 주도 설계 구현과 핵심 개념 익히기 (최범균 지음, 지앤선)



(개인적인 의견) DDD의 궁극적인 목적

도메인 지식이 중요하다.

소프트웨어에서 중요하게 생각하는 것이 무엇인지, 문제를 해결하기 위해서 중요한 것은 무엇인지에 대해서 잘 파악해야 한다. 그 과정은 반드시 도메인전문가(또는 기획자)와 협업해야 한다. 개발자 혼자 판단해서 결정하면 안된다. 전략적 설계가 매우 중요하며, 이번 스터디에서 비록 전략적 설계에 대해서 많은 내용을 다루진 못하고 있지만, 도메인 지식이 우선이라는 사실을 반드시 깨달아야 한다.

복잡한 소프트웨어를 해결하기 위함

"도메인 주도 설계"는 궁극적으로 복잡한 소프트웨어를 해결하기 위함이다. 도메인 주도 설계를 도입하면서 소프트웨어가 오히려 복잡해진다면, "도메인 주도 설계"를 차라리 도입하지 않는게 좋을수도 있다. 


도메인 주도 설계 기반으로 개발되었다는 실무 코드를 보면 아래와 같은 사례를 종종 볼수 있다.


- 도메인 주도 설계를 제대로 구현하지도 않았는데, 도메인 주도 설계라고 우기는 개발자

- 도메인 주도 설계를 하긴 했는데, 오히려 소프트웨어를 더 복잡하게 만들어놓은 개발자


DDD 는 복잡한 도메인을 복잡하지 않은 소프트웨어로 구현하기 위함이다.

DDD 때문에 오히려 복잡해진다면, 차라리 DDD 를 적용하지 말자.



7. 도메인 서비스


7.1 도메인 서비스

"도메인의 개념 가운데 객체로는 모델에 어울리지 않는 것이 있다. 필요한 도메인 기능을 엔티티 또는 값객체에서 억지로 맡게 하면 모델에 기반을 둔 객체의 정의가 왜곡되거나, 또는 무의미하고 인위적으로 만들어진 객체가 추가될 것이다. 서비스는 모델에서 독립적인 인터페이스로 제공되는 연산으로서 엔티티나 값객체와 달리 상태를 캡슐화하지 않는다. 중략... 도메인 계층에서 사라진 도메인 지식이 응용 계층의 코드로 스며들게 되는데, 도메인 서비스를 적절히 도입하면 계층간의 경계를 선명하게 하는 데 도움이 될 수 있다. 단, 서비스는 적절하게 사용해야 하는데, 엔티티 또는 값객체의 모든 행위를 모두 가져와서는 안된다. [3] - 에릭에반스" 단, "서비스로 도메인 개념을 모델링하는 데 너무 의존하지 말자. 반드시 상황이 적절할 때만 사용해야 한다. [1] - 반버논" 서비스를 지나치게 사용하면 애너믹 도메인 모델이 만들어지는 부정적인 결과를 초래하게 된다. 도메인 로직이 엔티티와 값 객체에 응집하지 못하고, 서비스에만 집중될 수 있다. 그래서, 서비스는 매우 중요하고, 적절하게 사용하는 것이 좋다.


7.2 도메인 서비스, 샘플 코드로 이해하기

7.2.1 샘플 코드 (1)

ConfirmedCase(확진자) 도메인 객체는 역학조사가 끝나면 상태를 INVESTIGATED 로 변경한다.

역학조사가 끝난 경우, ConfirmedCase 도메인 객체에 completeInvestigation 라는 행위를 제공한다.

아직까진 크게 문제는 없어 보인다. 하지만, 아래와 같은 도메인 지식이 추가되었다.


[도메인 지식] 확진자 동선이 모두 완료된 경우에만, 확진자 상태를 완료로 변경할 수 있다.


새로 추가된 도메인 지식을 처리하기 위해서 4가지 방법을 생각해보자.

- [X] 도메인 객체에서 직접 참조하는 방법

- [X] 도메인 객체에서 리포지토리 호출

- [X] 애플리케이션 서비스에서 구현

- [O] 도메인 서비스


[X] 첫번째 방법 - 도메인 객체에서 직접 참조

지난 스터디에서 설명했듯이, 확진자 애그리거트와 동선 애그리거트는 분리하였다. 이유는, 확진자 데이터와 동선 데이터는 같이 추가되는 데이터가 아니기 때문이다. 만약, 확진자와 동선 엔티티가 같은 애그리거트에 있다면 어떻게 될까? 아래와 같이 도메인 객체에서 직접 참조할 수 있을 것이다.


같은 애그리거트에 묶여 있었다면, 확진자 엔티티에서 동선 엔티티를 직접 참조할 수 있기 때문에, 확진자 엔티티에서 동선에 대한 도메인 로직을 수행할 수 있다. 하지만, 우리는 애그리거트를 분리하였다. 이유는 동선 데이터는 확진자 데이터와 같이 추가되거나, 같이 변경되는 데이터는 아니기 때문이다. 아래 그림과 같이 애그리거트를 분리하였다.


애그리거트 사이에서는 반드시 애그리거트 루트의 식별자를 통해서만 참조할 수 있다. 첫번째 방법과 같이 객체를 직접 참조할 수 없다.


[X] 두번째 방법 - 도메인 객체에서 리포지토리 호출

확진자 애그리거트에서 동선 데이터를 전부 조회해서 가져오면 되지 않을까? 아래와 같이 리포지토리를 호출하면 되는데, InformationOnRouteRepository 리포지토리를 사용해보자.

하지만, 위와 같은 코딩은 (필자의 개인적인 생각으로는) 잘못된 의존성이다.

의존성 방향은 원의 안쪽으로 향해야 한다. 원 밖에서는 원 안쪽을 알 수 있고 당연히 참조할 수있다. 반대로, 원의 안쪽에서 원 밖에 대해서는 알수 없고, 참조해서는 안된다.


정상적인 의존성은 아래와 같다.



[X] 세번 째 방법 - 애플리케이션 서비스

애플리케이션 서비스 레이어에서 직접 구현해보자. 아래와 같이 InvestigationApplicationService 클래스에서 도메인 지식을 검증하였다.

얼핏보면 크게 문제는 없어보이지만, 도메인 지식이 응용 계층의 코드로 스며들게 된다.


잘못된 방법 - 도메인 지식이 애플리케이션 레이어로 스며들었다.

도메인 지식(로직)이 애플리케이션 레이어로 스며들지 않도록 해야 한다!!


[O] 네번째 방법 - 도메인 서비스

도메인 서비스를 사용해보자. 확진자 동선이 모두 완료된 경우에만, 확진자 상태를 완료로 변경할 수 있다. 라는 도메인 지식을 InvestigationService 라는 도메인 서비스에 정의하였다.

애플리케이션 서비스는 도메인 서비스의 클라이언트가 된다.

도메인 로직을 도메인 서비스에 위임하고, 도메인 로직이 애플리케이션 레이어로 스며들지 않도록 한다.


7.2.2 샘플코드(2)

애플리케이션은, 확진자 정보를 등록하고 변경하는 기능을 제공해야한다. 사용자와 상호작용을 하기 때문에, 애플리케이션 서비스에서 요구사항을 구현한다. 이때, 공통적으로 확진자 데이터가 존재하는지 유효성 체크를 해야한다. 도메인 서비스에 정의하지 않는다면, 아래와 같이 유효성 체크에 대한 도메인 로직이 중복으로 분산되어있다.

이런 경우에, 도메인 서비스를 활용해서 개선해보자. "도메인 서비스"에서 확진자 데이터가 존재하는 도메인 로직을 제공해주면 된다.

애플리케이션 서비스에서는 아래와 같이 도메인 서비스를 주입해서 메서드를 호출하면 된다.


7.3 도메인 객체를 직접 반환하는 도메인 서비스

도메인 서비스는 도메인 객체(엔티티, 값객체)를 직접 반환할 수 있다. 샘플 사례로 알아보자.


[도메인 지식] 병원입원 대상자는, 확진자 중에서 기저질환이 있는 경우이다.  

확진자 도메인 객체에서는 확진자의 기저질환 유무에 대한 속성을 갖는다.

도메인 지식에 의하면, isUnderlyingDisease 값이 true 인 경우(기저질환이 있는 경우)에만 병원 입원 대상자가 된다. 아래 코드와 같이 도메인 지식을 도메인 서비스에 정의하였다.

이때 "도메인 서비스"는 확진자 데이터의 리스트를 반환하는데, 도메인 객체(엔티티, 값객체)를 직접 반환한다. 도메인 서비스에서 DTO 객체로 변환해서 반환하지 않는다. DTO로 변환이 필요하다면 도메인 레이어 아닌, 애플리케이션 레이어에서 DTO로 매핑해서 사용하면 된다. (개발자 취향에 따라서 매핑 안해도 된다.)


혹시, 의문이 생길 수 있다.

애플리케이션 레이어에서 꼭 DTO 로 매핑해야 하는가? 애플리케이션 레이어에서 도메인 객체를 반환하고 컨트롤러에서 DTO 로 매핑하면 되지 않나? 8장에서 다시 얘기하겠다.


7.4 어떤 기준으로 도메인 서비스에 구현해야할지,
솔직히 아직 잘 모르겠다...

도메인 서비스는 어떤 기준으로 애플리케이션 서비스와 구분하면 좋을까?


어떤 도메인 지식을 도메인 서비스로 정의해야할까?

확진자 증감, 증감률에 대한 계산식을 제공하는 기능은, 도메인 서비스에 구현하는게 과연 적합할까? 계산식에 도메인 지식이 포함되어있다면 도메인 서비스라고 할 수 있다. 하지만, 단순 계산식을 도메인 서비스라고 할 수 있을까? 아래와 같은 코드는 도메인 로직을 담고 있지 않다. 단순히 계산하는 로직이다.


솔직히, 잘 모르겠다. 도메인 지식인지, 아닌지 여부를 판단하기 어렵다. 도메인 서비스에 적합한지를 잘 모르겠다. 하지만, 애플리케이션 레이어에서 확진자 증감률에 대한 기능을 여기저기서 사용한다면, 코드를 분산시키는 것보다는, 도메인 서비스에 계산식을 위임할 수 있기 때문에 중복코드를 방지할 수는 있을 것 같다. 그래서, 이 경우는 도메인 서비스에 사용하는 것도 나름 괜찮아 보인다.


하지만, 도메인 서비스를 남용해서는 안된다!!


7.5 도메인 서비스 를 남용하지 말자


(주의)필자의 개인적인 생각이다. 개발자마다 다르게 생각할 것 같다.


7.5.1 도메인 객체의 행위를 도메인 서비스에 모두 정의하지 말자!!!

지난 스터디에서 설명했듯이, 도메인 객체에는 속성과 함께 행위를 정의한다. 확진자 엔티티에는 이름을 변경하는 메서드가 정의되어있다.

확진자의 이름을 변경해야 한다면, 도메인 객체의 해당 메서드를 사용하면 된다. 굳이, 도메인 서비스에 동일한 로직을 복사해서 넣을 필요가 없다. 아래와 같이 "도메인 서비스"에서 이름을 변경하는 메서드를 제공할 필요가 없다.


"도메인 서비스"에서는 반드시 도메인 객체에서 정의하기 애매한 경우에만, 도메인 지식에 한해서만 사용해야 한다. 도메인 객체의 모든 행위를 도메인 서비스로 옮기지 말자!!!


7.5.2 리포지토리를 그대로 호출하는 메서드는 굳이 필요 없을 것 같다.

데이터를 단순히 CRUD 하는 경우에도 굳이 도메인 서비스에 정의할 필요가 없을 것 같다. 아래와 같이 제공해줄 필요가 있을까?


애플리케이션 서비스에서 리포지토리를 바로 호출해서 사용하면 된다.


"도메인 주도 설계 철저입문 68페이지" 를 보면, 도메인 서비스를 남용하면 데이터와 행위가 단절돼 로직이 흩어지기 쉽기 때문에, 가능하면 도메인 서비스를 피하라고 한다. 하지만, 도메인 서비스를 무조건 사용하지 말라는 의견은 아닐 것이다. 도메인 서비스에 구현할지 망설여진다면, 일단 도메인 객체(엔티티, 값객체) 에 먼저 정의해서 사용하는게 좋겠다. 그리고, 도메인 객체에 정의하기 너무 애매한 상황이 생기면 도메인 서비스를 사용하는 것을 고려해보자.


필자가, 몇주 고민해봤는데

정확하게 도메인 서비스를 어떤 기준으로 사용하면 좋을지에 대해서 명확하게 정답을 찾지 못하였다. 조언 좀 부탁한다.


7.6 의존성

의존성에 대해서 알아보자. 5주차에서 더 상세히 공부할 것이다.


7.6.1 리포지토리 참조 의존성

아래와 같이 리포지토리 인터페이스를 정의하였다.

해당 인터페이스만 봤을 때는, 리포지토리 구현체로 어떤 데이터베이스를 사용할지 전혀 알수가 없다. 필자는 임시로 아래와 같이 구현체 클래스를 작성하였다.

필자가 임시로 구현한 구현체는 아래와 같다. 심플하게 Map 자료구조를 사용하였다.

"도메인 주도 설계"의 기본 가치에 의하면, 도메인 모델을 정의하고 도메인 객체를 정의하는 과정에서는 데이터베이스가 어떤 종류인지 크게 중요하지 않다. 도메인 지식에 집중하며, 데이터베이스 상세 구현체는 나중에 결정해도 된다. 도메인 서비스는 리포지토리 추상화 인터페이스를 호출한다.

도메인 서비스에서는 어떤 데이터베이스를 사용하는지 전혀 알 수 없다. 그리고, 리포지토리 구현체는 원의 바깥쪽인 인프라스트럭처 레이어에 위치하게 된다.

애플리케이션 서비스에서도 마찬가지이다. 애플리케이션 서비스에서 리포지토리의 인터페이스를 참조한다.

ConfirmedCaseRepository 의 구현체인 SimpleConfirmedCaseRepository 구현체가 주입될 것이다. 하지만, 애플리케이션 서비스에서도 마찬가지로, 어떤 데이터베이스를 사용하는지 전혀 알수 없다. 아래 코드와 같이, 애플리케이션 서비스에서 인프라스트럭처 레이어에 있는 리포지토리 구현체를 직접 참조해서는 안된다.

아래 그림과 같이, 원 밖으로 참조하면 안된다.


7.6.2 도메인 서비스 참조 의존성

필자의 샘플 코드는 도메인 서비스를 추상화하지 않았다. 하지만, 특이한 경우에는 도메인 서비스를 추상화할 수도 있는데, 도메인 서비스를 인터페이스로 정의하고 구현체는 인프라스트럭처 레이어에 구현하게 된다. 그림으로는 아래와 같다.

이 경우에도 반드시 원의 안쪽으로 의존해야 한다. 애플리케이션 서비스는 추상화된 도메인 서비스 인터페이스를 호출해야 한다. 애플리케이션 서비스가 원의 밖으로 의존해서는 안된다.



글쓰는게 지친다.

더이상 글을 쓰기 어려우니... 자세한 내용은 이만 생략하겠다.



8. 애플리케이션 서비스


8.1 애플리케이션 서비스

도메인 서비스는 도메인 로직을 담당한다. 반면에, 애플리케이션 서비스는 소프트웨어의 요구사항, 유스케이스를 구현한다. 즉, 애플리케이션 서비스는 사용자의 필요를 만족시키고 목적을 달성하게 하는 것으로, 궁극적으로 소프트웨어의 요구사항을 구현하는 것이다.


도메인 지식만으로는 소프트웨어가 동작할 수 없다. 


8.2 상호작용 & 유스케이스

애플리케이션은 사용자와 상호작용을 한다.


- 애플리케이션 사용자는 CSV 파일로 다수의 확진자 데이터를 한번에 등록할 수 있다.

- 애플리케이션 사용자는 이름으로 확진자를 검색할 수 있다.


[유스케이스] 애플리케이션 사용자는 CSV 파일로 다수의 확진자 데이터를 한번에 등록할 수 있다.

상세한 설명은 생략한다. (실제로 동작하는 코드는 아니다.)


[유스케이스] 애플리케이션 사용자는 이름으로 확진자를 검색할 수 있다. 

데이터 검색 기능을 구현해보자.

이름으로 검색하는 기능은 문자열을 파라미터로 전달받고, DTO 리스트를 반환한다.

"도메인 서비스"에서는 도메인 객체(엔티티 또는 값객체)를 반환했었다. 하지만, 도메인 객체를 실제 사용자에게 모두 공개하는 것은 바람직하지 않을 수도 있다. 필자는 관례적으로 도메인 객체를 사용자에게 그대로 제공하지 않고, 사용자가 필요한 데이터만 정리해서 제공해준다. 애플리케이션 레이어에서 데이터를 변환하며, 이때 사용하는 것이 바로 DTO 이다. ConfirmedCaseDTO 라는 객체를 정의하였다. 애플리케이션 레이어에서 사용하는 DTO 는 세터가 정의되어도 크게 문제는 없다고 생각한다. 그래서 @Data 어노테이션을 선언하였다.

도메인 객체를 DTO 로 매핑해주는 of 메서드, DTO 를 도메인 객체로 매핑해주는 toEntity 메서드를 정의하였다. 필자의 코드보다는, DTO 어셈블러를 사용하는게 더 깔끔할 것이다.

암튼, 이름으로 확진자를 검색하는 기능을 제공하기 위해서, 아래와 같이 컨트롤러를 구현하였다.

사용자는, 아래와 같이 Rest API 로 호출할 수 있다.

도메인 객체를 직접 제공하지 않고, 필요한 데이터만 DTO 데이터로 전달해주면 된다.


8.3 DTO 에 대해서 (개발자마다 다른 의견)

개발자마다 의견이 다르며, 소프트웨어에 따라서 다를 것이다.


1) 모든 도메인 객체를 DTO 로 변환해야 할까?  

"도메인 객체를 반환 타입으로 노출시키지 않기 위해서, DTO 를 제공한다면 추가적인 타입 때문에 발생하는 부담으로 인해 돌발적인 복잡성이 수반될 수 있다. 또한 불필요한 DTO 가 계속해서 생성되고, 트래픽 양이 많아진 애플리케이션 내에서 가비지 컬렉션으로 인한 메모리 부담이 가중될 수 있다. [1] - 반버논"

"반버논"은 반드시 DTO 로 매핑할 필요는 없다고 주장한다. 하지만, 필자의 생각으로는 가능하면 DTO 를 반드시 사용하는게 좋겠다는 의견이다.


2) 도메인 객체에서 DTO 를 생성해도 되지 않나?

필자의 샘플 코드에서는 DTO 객체에서 도메인 객체인 엔티티를 생성했었다. 만약 반대로, 도메인 객체(엔티티)에서 DTO 를 생성하는건 괜찮을까? 바람직하지 않다. 아래 그림과 같이 의존성은 반드시 원의 안쪽으로 향해야 한다.

원 안쪽에 존재하는 도메인 객체는 원 밖의 DTO 를 알면 안된다. 즉, 도메인 객체에서는 DTO 의 존재 가치에 대해서 전혀 몰라야 한다. 즉, 도메인 객체(엔티티, 값객체) 에서는 DTO 를 생성해서는 안된다.


3) DTO Assembler

마틴 파울러는 DTO Assembler 이라는 패턴을 가이드로 제시한다.

https://buildplease.com/pages/repositories-dto/?fbclid=IwAR3ImFfJBjhiiwk7cwzp3RSS9HdAt0xjewMCTLPB-6pr04T_81fSi-2Rlco

https://martinfowler.com/eaaCatalog/serviceLayer.html?fbclid=IwAR3ImFfJBjhiiwk7cwzp3RSS9HdAt0xjewMCTLPB-6pr04T_81fSi-2Rlco


4) DTO 에 대한 변환을 컨트롤러에서 해도 되지 않나? 애플리케이션 레이어에서는 도메인 객체를 반환해도 되지 않나?

마틴 파울러는 서비스 레이어에서 DTO로 변환하는 것을 제시하였지만, 일부 개발자는 애플리케이션 레이어에서 DTO로 매핑하지 않고, 도메인 객체를 반환하는걸 선호하는 개발자가 있다. 즉, 컨트롤러(인터페이스 레이어 or 프리젠테이션 레이어)에서 도메인 객체를 DTO로 변환할 것이다. 


선택은 각자에게 맡기겠다. 이때 고민해야 하는 포인트는 아래와 같다.


도메인 객체를 어디까지 보호할 것인가?

캡슐화 수준을 어떻게 할것인가?


원의 안쪽으로 의존성이 향하기 때문에, 도메인 객체는 원 밖에서 어디서나 참조할 수는 있다. 단, 너무 멀리 가면 위험할 수 있다. 그래서, 도메인 객체를 보호해야할 필요성이 있다면, 도메인 객체를 보호하기 위해서 적당한 레이어에서 DTO 로 변환하는 것이다. 개발자의 선택에 따라서 다르게 구현될 것이다.


8.4 애플리케이션 동작에 필요한 다양한 기능
(인증, 트랜잭션, 캐싱 등)

애플리케이션 서비스에서는 소프트웨어의 요구사항 및 유스케이스를 구현하고 사용자와 상호작용을 한다. 그 외에, 다양한 기능을 수행해야 하는데, 대표적으로 아래와 같은 역할을 한다.


- 트랜잭션

- 인증

- 캐싱


1) 트랜잭션

역학조사관이 확진자의 역학조사를 시작하였다. 역학조사가 시작되면, 확진자의 상태를 "INVESTING" 로 변경해야 하고, 확진자 동선을 추가하게 한다. 필자는 확진자와 동선 데이터를 별도의 애그리거트로 분리하였다. 그래서, 이 경우 서로 다른 두개의 애그리거트를 동시에 변경해야 한다. 둘중 하나라도 변경에 실패하게 되면, 나머지 하나에 대한 변경을 롤백해야 한다. 그래서, 해당 기능은 트랜잭션으로 묶여야 한다.

스프링 프레임워크를 사용한다면 @Transactional 어노테이션으로 트랜잭션을 선언할 수 있다. 필자의 샘플 코드는 실제로 동작하지는 않는다. 4주차 리포지토리에서 알아보겠다. 만약, JPA 를 사용한다면, 아래 코드와 같이 더티체킹이 동작하기 때문에, save 메서드를 실행할 필요가 없다.

JPA 의 더티체킹으로 인해서, 트랜잭션이 종료되는 순간 JPA 플러시가 실행되고, 업데이트 쿼리가 자동으로 실행될 것이다.


자세한 내용은 4주차에서...


2) 인증

확진자의 동선을 추가하는 기능을 역학조사관인 경우에만 제공하고 싶다면 어떻게 할까? 애플리케이션의 특정 권한에 한해서 허락해야할 때, 인증 기능을 애플리케이션 레이어에 적용할 수 있다. 스프링 시큐리티를 사용한다면 아래와 같이 사용한다.

참고로, 인증 로직을 애플리케이션 레이어에 선언할 수도 있지만, Rest API 를 제공한다면 컨트롤러 레이어에 선언해줄 수도 있다. 상세한 내용은 생략한다.


3) 캐싱

애플리케이션의 모든 요청에 대해서 매번 데이터베이스를 조회할 필요가 없는 경우에는, 캐싱을 적용할 수 있다.

인증, 캐싱 등은 매우 중요한 주제이지만 이번 스터디의 주제와는 거리가 있으니 상세한 설명은 생략한다.


8.5 도메인 서비스 vs 애플리케이션 서비스

도메인 서비스 vs 애플리케이션 서비스 어떤 기준으로 구분해서 사용하면 좋을까?


도메인 서비스

- 도메인 로직에 집중한다.

- 도메인 객체를 반환한다.

- 도메인 객체에 구현해야하는 행동까지 모두 제공하지는 않도록 주의한다.

- 남용하지 않도록 주의하자.

 

애플리케이션 서비스

- 사용자의 요구사항을 구현하며, 사용자와 상호작용을 한다.

- 트랜잭션, 캐싱, 인증 등 기타 기능을 함께 수행한다.

- 도메인 객체를 DTO 객체로 매핑해서 반환한다.
(일부 개발자는 DTO 로 변환하지 않고 도메인 객체를 그대로 반환하기도 한 후, 컨트롤러에서 매핑하기도 한다. 하고싶은대로 해라...)

- 도메인 로직이 스며들지 않도록 주의하자.


명확하게 어떤 기준으로 정의하면 좋을지 아직은 와닿지가 않는다.

스터디를 계속 진행하면서 계속 생각해보자.



9. 테스트코드 작성하기


테스트 코드를 작성해보자. 필자의 글에서는, 실패하는 테스트코드를 먼저 작성하지 않았기 때문에 TDD 는 아니다.


글이 너무 길어졌다. 테스트 코드 관련해서는 간단하게 소개만 하고 넘어가겠다. 글을 쓰기 지친다. 나중에 기회가 되면 테스트 코드 관련해서는 각잡고 다시 공부하겠다.


9.1 도메인 서비스 테스트 코드

도메인 서비스는 가능하면 반드시 단위테스트로 작성하도록 하자. 일단, 패키지는 아래와 같다.



9.1.1 ConfirmedCaseService 도메인 서비스 단위 테스트 작성하기

단위테스트를 작성하기 위해서 리포지토리를 Mocking 하겠다. Mockito 를 편하게 사용하기 위해서, @ExtendWith(MockitoExtension.class) 는 사용하였다.


1)  "확진자 도메인 서비스" 의 데이터 중복(exist) 기능 테스트

두가지 경우(확진자 존재 유무에 따라서) 테스트를 하겠다.
ConfirmedCaseRepository 를 Mocking 해서 사용한다.

 

- given : 확진자 존재 유무에 해당하는 조건을 선언해준다.

- when : 테스트 하고자하는 메서드를 실행한다.

- then : 테스트를 검증한다.

 

테스트를 실행하면 아래와 같다.


시간 관계상 상세한 설명은 생략한다....



2) "확진자 도메인 서비스" 의 병원 입원 대상자(hospitalizedConfirmedCaseList) 메서드 테스트


given : 테스트를 준비한다. 두 개의 데이터 중 하나만 기저질환이 있는 데이터로 설정한다.

when : 테스트 하고자하는 메서드를 실행한다.

일반적으로 when 의 코드는 한줄로 해결될 것이다.

then : 테스트를 검증한다. 두 개의 데이터 중 한개만 검색될 것이며, 기저질환이 있는 데이터만 반환하는지 검증하면 된다.

 


시간 관계상 상세한 설명은 생략한다....


3) "확진자 도메인 서비스"의 확진자 증감률(DailyChangeAmount, DailyChangeRate) 메서드를 테스트

시간 관계상 상세한 설명은 생략한다....

확진자 도메인 서비스의 전체 테스트 결과는 아래와 같다.


단위 테스트로 작성했기 때문에 테스트는 매우 빠르게 실행될 것이다. @SpringBootTest 어노테이션은 사용하지 않는다. @SpringBootTest 어노테이션은 통합테스트에서 사용한다. 일부 개발자는 @SpringBootTest(classes=클래스)에 의한 방법을 단위테스트라고 주장한다. 하지만, 필자의 기준으로 @SpringBootTest 는 스프링을 실행시키기 때문에 통합테스트에 가깝다.



9.1.2 InvestigationService 도메인 서비스 단위 테스트 작성하기

InvestigationService 도메인 서비스는, 확진자의 동선이 모두 완료된 경우에만 확진자의 상태를 변경할 수 있다는 도메인 지식을 포함하고 있다. 해당 도메인 지식을 테스트하겠다.

1) "역학조사 도메인 서비스" 의 complete 기본 동작 테스트


2) "역학조사 도메인 서비스" 의 complete 실행 시 예외 처리를 검증

시간 관계상 상세한 설명은 생략한다....

 

9.2 애플리케이션 서비스 테스트 코드

시간 관계상 상세한 설명은 생략한다....

시간 관계상 상세한 설명은 생략한다....

시간 관계상 상세한 설명은 생략한다....

시간 관계상 상세한 설명은 생략한다....

시간 관계상 상세한 설명은 생략한다....

시간 관계상 상세한 설명은 생략한다....


테스트 코드 관련해서는 나중에 각잡고 다시 공부하기로 하자.


같이 얘기해보고 싶은 주제


스터디를 마무리하기 전에, 같이 애기해보고 싶은 주제가 있다.


1)도메인 서비스, 애플리케이션 서비스 어떻게 정의해서 구분하면 좋을까? 

필자의 글에서는, 도메인 지식 유무에 따라서 구분하는 것으로 정의하였다. 하지만, 솔직히 아직은 잘 모르겠다. ㅠㅠ


2)도메인 객체는 어디까지 보호해야 하는가?

필자의 글에서는 애플리케이션 레이어에서 DTO 객체로 반환하였다. 하지만, 일부 개발자는 애플리케이션 서비스에서 도메인 객체를 반환하고, 컨트롤러에서 DTO 로 변환하는 경우도 있다. 심지어는 도메인 객체를 DTO 로 변환하지 않고, 사용자에게 그대로 노출해줄 수도 있을것이다.


필자는 애플리케이션 레이어까지만 보호해야한다고 생각하는데, 여러분은 어디까지 보호해야 한다고 생각하는가?


3)일급 컬렉션 은?

필자가 많이 사용하는 일급 컬렉션은, DDD 에 적용할 수 있을까? 도메인 서비스 라고 정의할 수 있나? 아니면 엔티티 리스트 라는 개념이 DDD 에 존재하는가?


4)네이밍은 어떻게?

여러가지 방법으로 정의할 수 있을 것이다.


#1.필자의 글에서는 아래와 같이 정의하였다.

- 도메인 서비스 : xxxService

- 애플리케이션 서비스 : xxxApplicationService


하지만, 개발자들마다 정의하는 방법이 각양각색이다.


#2.도메인 서비스를 좀 더 명확하게 정의할 수도 있겠다.

- 도메인 서비스 : xxxDomainService

- 애플리케이션 서비스 : xxxApplicationService


#3.인터페이스를 정의하는 경우도 있다.

- 도메인 서비스 : xxxService implements DomainService

- 애플리케이션 서비스 : xxxSeervice implements ApplicationService


#4.도메인 서비스는, Service 라는 네이밍을 사용하지 않는 경우

- 도메인 서비스 : xxxValidator 등등

- 애플리케이션 서비스 : xxxService

조영호님의 코드를 보면, 도메인 서비스에 Service 라는 네이밍을 사용하지 않으셨다. OrderValidator 이라는 네이밍이 사용되었다.

OrderValidator 이라는 네이밍이 사용되었다.

https://github.com/eternity-oop/Woowahan-OO-03-domain-event/blob/master/src/main/java/org/eternity/food/order/domain/OrderValidator.java

이 경우, 애플리케이션 서비스는 xxxService 라 정의하였다.



어떤 방법이 좋을지 아직 잘 모르겠다.


5) 그래서, 과연 도메인 서비스가 정말 필요한가?

도메인 서비스는 당연히 필요하다. 하지만, 남용하지 않도록 하자. 도메인 객체인 엔티티 또는 값객체에 정의할 수 있다면, 도메인 객체에 먼저 정의하자. 도메인객체(엔티티,값객체)에 정의하기 애매한 지식에 한해서만 도메인 서비스를 사용하도록 하자. 그리고, 애플리케이션 레이어로 도메인 로직이 스며들지 않도록 주의하자.



4주차에서는 리포지토리에 대해서!!


도메인 객체 vs 퍼시스턴스 객체


The Domain Model is NOT the same as the Persistence Model. Each serve a different layer with different responsibilities.



DDD의 가치에 완벽하게 따르면, 사실 도메인 객체와 퍼시스턴스 객체는 같은 클래스로 정의하면 안된다. 즉, JPA 의 엔티티는, DDD 의 도메인 객체와 동일한 클래스로 사용하면 안된다. 필자가 해당 이슈에 대해서 커뮤니티에 질문을 올렸는데, 개발자마다 생각하는 방향이 모두 다르다는 것을 확인하였다.


1) 같은 클래스로 해도 된다.

2) 다른 클래스로 해야 한다.

3) 차라리, JPA 를 안쓰겠다. Spring Data JDBC 또는 다른 대안을 찾겠다.


신기하게도 위 3가지 선택지가 모두 나왔다. 1)번 방법을 선택하겠다는 개발자가 제일 많았지만, DDD의 가치에 의하면 1)번 방법은 옳지 않다고 생각한다. 그럼에도 불구하고 1)번이 현실적으로 좋은 방법인것 같기도 하다. (물론, JPA 를 선호하지 않기 때문에 3)번 방법으로 하고 싶지만...)


사실, 아직 어떤 데이터베이스인지 알 필요가 없다.

위 고민은 RDBMS 를 사용한다는 가정이다. 하지만, NoSQL 이 될수도 있다. Document DB 인 MongoDB 를 사용할수도 있다. 도메인 지식에 기반하여 도메인 객체를 정의해야하며,


데이터베이스는 최후의 선택으로 최대한 미루자.



그럼.. 이만 글을 마무리하고, 4주차에서는 리포지토리 구현체에 대해서 상세하게 알아보겠다.

(어려운 주제라서, 다음 스터디가 언제가 될런지..ㅠㅠ)


레퍼런스

[1] 도메인 주도 설계 핵심 (반 버논 지음, 에이콘 출판사)

[2] 도메인 주도 설계 철저 입문 (나루세 마사노부 지음, 위키북스)

[3] 도메인 주도 설계 - 소프트웨어의 복잡성을 다루는 지혜 (에릭 에반스 지음, 위키북스)

[4] DDD START! 도메인 주도 설계 구현과 핵심 개념 익히기 (최범균 지음, 지앤선)

매거진의 이전글 [스터디] 도메인 주도 설계 2주차
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari