DDD 를 적용하다 겪은 실수들
서론
최근에 도메인 주도 설계 철저 입문 이라는 책을 읽고난 후, 회사에서 마침 가맹점 신청이라는 개발 건이 들어와서 경량 DDD 를 적용하다가, 그 과정에서 저지른 실수들과 느낀 점을 공유해드리려고 합니다.
도메인 로직을 응집력 있게 설계하기
도메인 주도 개발에서 도메인 객체란 도메인 모델을 구현한 도메인 객체를 의미하는데 대표적인 예로 ‘값 객체, 엔티티, 명세 등’ 을 의미한다고 배웠습니다.
비록 경량 DDD 였지만, 개발 당시 가장 중점적으로 생각했던 부분은 ‘어떻게 도메인 로직을 응집력있게 설계할까?’ 였습니다.
그간 직,간접 경험과 책을 내용을 비추어볼 때, 가벼운 도메인 로직은 엔티티에 모아두는 것이 좋고, 조금 복잡한 도메인 로직의 경우에는 명세(Specification) 로 관리하는 것이 좋다고 알고 있었습니다.
일단 domain 패키지 아래에 ENTITY 클래스와 DTO 클래스를 만들었습니다. ENTITY 에는 builder 를 구현하였고, DTO 에서 toEntity 메서드를 통해서 ENTITY로 변환할 수 있도록 해주었습니다.
서비스는 나름 어디서 들어본건 있어가지고… CQRS 라는걸 적용해본답시고 service(명령 처리) 와 findService(조회 처리) 로 나누었습니다.
DTO 에 toEntity 라는 메서드가 있었고, 프레젠테이션 계층인 컨트롤러에서 명령을 처리하는 메서드의 파라미터로 DTO 를 사용하고 있었습니다.
@PostMapping("/create")
public String create(
@ModelAttribute FranchiseDto franchiseDto,
BindingResult bindingResult .. 생략
) { 생략 .. }
즉, 위와 같은 형태로 사용하고 있었는데 CQRS 에 대해서 조금 더 공부해보니 위와 같은 방식에 몇 가지 문제가 존재 했습니다.
CQRS 는 읽기 쓰기 표현이 일치하지 않고 복잡한 경우나, 읽기 쿼리가 복잡하여 성능 문제가 우려되는 경우 (Ex. 페이징) 읽기 쿼리후 엔티티의 불필요한 데이터를 노출 시킬 위험이 있는 경우에 사용하면 좋다. 읽기 모델에는 비즈니스 논리 또는 유효성 검사 스택이 없으며 뷰 모델에서 사용할 DTO 를 반환 한다. 결과적으로 읽기 모델과 쓰기 모델의 일관성이 유지된다.
첫 번째 문제는 DTO 에 유효성 검사 로직이 들어있었으며(이때 JSR-303 기반 어노테이션을 적용하진 않았지만, Validator 클래스를 만들어 유효성 검사를 하던 중이었습니다.)
두 번째 문제는 DTO 에 도메인 로직들이 들어가게 되었습니다.
세 번째 문제는 Validator 클래스에서 조회 서비스를 호출하여 도메인 로직까지 검증하고 있었습니다.
그러다 보니, 도메인 로직이 엔티티, DTO, Validator 등 여러 군데에 퍼져있어서 응집력이 낮아졌고 다른 사람이 유지보수를 한다고 생각했을때 도메인 로직을 한눈에 파악하기 어려울 것이라고 생각했습니다.
이때 몸담고 있던 회사는 SI 였으며 DDD 를 적용하거나 DTO 를 사용한 좋은 선례가 딱히 없었습니다.
문제 해결을 위해 CQRS 부터 다시 찾아보고 공부하였습니다. 그 결과내가 DTO 를 잘못 사용하고 있었구나라는 것을 느꼈습니다.
DTO 를 잘 못사용하고 있었다라는 것을 깨우침을 준 문장이 있었는데 다음과 같습니다.
즉, 위 문장을 참고하였을 때 DTO 는 쿼리 후 반환되는 데이터의 모델이며 비지니스 로직이랑 유효성 검사 스택이 존재해서는 안 된다(유효성 검사 대상은 ENTITY)라는 것을 깨닫고 다음과 같이 리팩토링 하였습니다.
- DTO 에 도메인 로직과 유효성 검사 스택 제외
- CUD 처리하는 핸들러 메서드의 파라미터에는 ENTITY 를 사용하여 유효성 검사 실시, 읽기 작업을 요구하는 핸들러 메서드의 파라미터에는 DTO 사용 (쿼리 후 model 에 담기는 데이터의 형식도 DTO)
- Validator 에 들어있던 도메인 로직은 명세로 빼서 명령을 처리하는 서비스에서 처리 하도록 변경
- DTO 를 WEB 패키지 아래로 이동
위와 같은 리팩토링을 하고나니, DTO 에 도메인 로직과 유효성 검사 로직이 제거되었으며, 모든 도메인 로직들이 도메인 객체에 모여 응집력이 높아지게 되었습니다.
하지만 이 과정에서 새로운 문제를 찾았으며, 깨달음을 얻었습니다.
유지보수하는 입장에서는 도메인 로직을 파악하기 쉬울지 몰라도 개발 당시에는 생산성이 너무 안나왔습니다.
그 이유는 개발 환경이 MyBatis 를 사용하고있다보니 SQL 에 비지니스 로직이 존재할 수 밖에 없고, JPA 를 사용할 때 처럼 객체지향적인 설계를 하기 힘든 환경이었습니다.
JPA 를 사용하여 객체지향적인 설계를 하게되면 객체 그래프를 참조하도록 설계가 되는데, MyBatis 를 사용하다보니 DTO 를 God Class 처럼 사용하는 편이 개발 속도 측면에서는 훨씬 빨랐습니다.
웹 애플리케이션이 간단하면 A 등록할 때 A 만 등록하고 끝난다면 상관 없지만, A 등록할 때 B 도 등록하고 이력도 쌓아야하고 하는 경우가 대부분이기 때문입니다.
유효성 검사를 ENTITY 에서 담당하게 하려면 일단 객체 그래프 참조가 가능하도록 설계를 해야 하는데, MyBatis 를 쓰다 보면 DTO 를 God Class 처럼 사용하는 편이 성능적인 측면에서도 더 좋기 때문입니다.
객체 그래프 참조가 가능하도록 설계하면 SQL 에서 Collection 이나 Association 을 사용하게 되고, 이 작업은 객체 1개에 속성을 바인딩하는 것보다 성능이 떨어집니다.
정리
DDD 를 공부하고 개발에 적용하면서 배우고 느낀 점을 정리하였습니다.
OOP 를 하기 위해 DDD 를 선택하였습니다. 하지만 개발 환경에 따라서 정석적인 DDD 방식을 따르기보다 자신의 환경에 맞게 알 맞은 개발 방식을 선택하여 유지보수성과 확장성을 고민하여 개발하는게 더 낫겠다라는 것을 느꼈습니다.