DDD 실무 회고! (with 서비스 런칭 한달 시점)

Picbel·2023년 1월 8일
2

Architecture

목록 보기
1/3
post-thumbnail

서비스 런칭!

11, 12월에는 블로그 포스팅을 거의 못했습니다.
여러가지 이유가 있는데 크게 2가지입니다.
1. 서비스런칭일정이 가까워지니 일이 많아 바빠진 점
2. 공부한 범위가 기초CS영역이라 블로그 포스팅을 하기에 적합하지 않았습니다.
(제 블로그는 실무, 공부하면서 느낀 경험을 기술하는 것을 목표로 하고있습니다. 단순 기술적 지식은 저보다 잘 정리하실분이 많기때문에!)
서비스 런칭이 12월 초에 마무리 되었습니다.
오픈 3주가 끝낫을 때 앱서비스의 매출이 4500만원 가량 발생하였는데 오픈3주라는 기한을 생각하면 만족스럽습니다.

이번 포스팅은 DDD에 가깝게 아키텍쳐를 잡아가며 서비스를 만든 회고와 현재 느끼는 점들에 대한 간략한 후기성 포스팅입니다.

완벽한 DDD를 하였나?

먼저 이 질문에 대한 답은 '아니오'입니다.
DDD에서 가장 중요한것은 바운더리맵과 컨텍스트를 정하는 행위라 생각합니다.
그 이유는 그에 따라서 도메인이 설계되고 루트 애그리게이트가 생기고 영속화계층의 인터페이스가 작성되기 때문입니다.
허나 서비스 일정의 이유로 바운더리맵과 컨텍스트가 명확하지 못하게 개발을 하게되었습니다.
서비스를 탄생시켜야하는 단계이기때문에 요구사항또한 명확하지 못했구요.
그래서 어떤 일이 벌어졋냐? 하면
명확한 루트 애그리게이트가 없습니다.
위 상황의 연장선이라 할수 있는데 도메인모델에 대한 진입점이 여러곳인 경우가 생겻습니다.
결국 각 도메인모델별로 영속화계층(repository)가 생기고 저장 및 관리합니다.
이 문제는 각 도메인이 범위가 겹치는 경우 즉 직접참조에 대한 명확한 구분이 없다는 것이고 이부분은 영속화계층을 설계 할때 많은 애로사항을 만들었습니다.
23년 1분기 말에는 작업하여 2분기 중반에는 명확한 루트 애그리게이트 구분과 아키텍쳐 개선이 이루어질 예정입니다.

명확하게 지킨것들

아무래도 회고다 보니 아쉬운점이 먼저 떠오르나 봅니다.
지금부턴 서비스를 런칭하며 아키텍쳐적으로 절대적으로 지킨것들과 오픈 한달이 넘어가는 지금 규칙을 지켜 얻은것들을 기술하겠습니다.

  1. rich 도메인이라는 개념에 맞게 최대한 도메인모델이 많은 것을 수행하도록 한다.
  2. 계층의 역할은 명확히하고 다음과 같이 정한다.

    domain : 우리가 구현해야할 개념!
    usecase : domain의 흐름을 제어한다.
    repository : domain을 영속성을 담당한다.

  3. usecase가 다른 usecase를 호출하지않는다.(횡단 참조 방지)
  4. 각 계층은 인터페이스로 먼저 설계하고 해당 인터페이스에 대하여 테스트 코드를 작성한다.

1번 rich 도메인이라는 개념에 맞게 최대한 도메인모델이 많은 것을 수행하도록 한다.
기능의 응집성을 최대한 높힐려는 의도였습니다.
도메인 자체만으로 표현할수 있는 행위가 많아졋으며 service 혹은 usecase에서 값에 대한 직접적인 변경을 막고 값변경에 대한 책임을 domain내부로 캡슐화 할려는 의도였습니다.
해당 규칙으로 usecase에 대한 테스트 부담이 많이 줄어 들었습니다.
usecase는 결국 도메인들의 흐름제어를 위해 도메인에서 제공하는 함수들만 호출하면 되기에 복잡한 테스트가 불필요해졌습니다.
테스트 코드를 작성할때 가장 비용이 저렴한 도메인 domain 테스트만 잘 작성해도 되니까요

2번 계층의 역할은 명확히하고 다음과 같이 정한다.
domain은 위에서 설명하였으니 usecase와 repository를 좀 더 상세히 설명하여 보겠습니다.
usecase는 도메인들의 함수의 호출하며 값변경 등 도메인의 흐름을 제어합니다.
이 흐름을 제어하여 도메인이 가져야할 행위를 표현하도록 노력하였습니다.

ex) 상품의 재고 n개를 선점합니다.
상품을 선점할려면 이 상품의 재고를 확인하고 n개 이상의 재고가 있을시 재고를 선점합니다.
재고가 없다면 실패합니다.

이런 경우가 있다면 각 도메인 상품이라는 도메인에서 다음과 같은 함수를 제공합니다.
'재고 확인', '재고 n개 선점하기'를 제공하고 usecase는 위 두 함수를 이용하여 흐름을 제어합니다.

간단하게 코드로 보면 이렇습니다.

class 선점usecase {
	fun 선점하기(n : 차감갯수){
    	if(재고 확인 >= n) {
        	재고 n개 선점하기
            성공 결과 반환
        }else {
        	실패 결과 반환
        }
    }
}

(재고 확인 >= n 이부분을 한번더 도메인에 몰아넣는것이 더 좋으나 예제를 위해 다음과 같이 하였습니다.)
위와 같이 도메인의 함수를 호출하여 도메인의 값변경을 지시 등 이런 도메인의 흐름을 제어하는것을 목표로 하였습니다.
얻게되는 장점은 위 1번의 사항과 동일합니다. 저 두개는 한쪽이 완성되면 자연스럽게 공존하게 되는것 같습니다.

그럼 다음으로 repository의 경우를 보겠습니다.
repository는 domain의 영속화를 전적으로 담당합니다.
무슨뜻이면 repository에 domain을 save(저장)하면 domain의 모든 값(프로퍼티)을 save하겠다는 뜻입니다.
usecase는 이것이 RDB나 Redis이든 파일시스템이든 궁금하지 않습니다.
repository.save()에 넘긴 domain은 항상 저장된다.라는 전제 원칙으로 하였습니다.
그래서 코드를 보면 다음과 같아집니다 위의 선점코드를 그대로 가져오겠습니다.

class 선점usecase {
	fun 선점하기(n : 차감갯수){
    	if(상품 재고 확인 >= n) {
        	상품 재고 n개 선점하기
            성공 결과 반환
            상품repository.save(상품) // <- 상품domain을 영속화한다.
        }else {
        	실패 결과 반환
        }
    }
}

다음과 같이 흐름제어를 한 domain을 그냥 저장만 하면됩니다.
생성, 업데이트를 분기하여 저장하는것은 계층내의 내부의 문제입니다.
(spring data의 CrudRepository를 생각하시면 됩니다.)
영속계층인 만큼 트랜잭션또한 해당 계층에서 관리하여 트랜잭션이 usecase에 영향을 주는것 또한 막을수 있습니다.
해당 방식의 장점은 추후의 기능이 확장될때 저장이나 조회 등 영속계층에 대하여 코드의 재사용성이 굉장히 올라갑니다.
jpa나 redis같은 내부 구현을 몰라도 되기 때문에 jpa의 osiv문제나 트랜잭션의 문제로 usecase가 고통받는일 또한 사라집니다.

일례로 현재 오픈1달된 시점에서 api와 admin에서 상품의 domain과 usecase를 굉장히 확장하지만
domain과 usecase에서 흐름제어만 하고 save하면 하나의 기능이 확장되는 훌륭한 생산성을 보여주고있습니다.

3번 usecase가 다른 usecase를 호출하지않는다.(횡단 참조 방지)
API의 흐름이 controller -> usecase -> repository로만 흐르게끔 최대한 유도하였습니다.
usecase가 다른 usecase를 호출하게되면 아키텍쳐적으로 방향을 잡기 어려워지고 추후 유지보수하는데 문제가 발생할것으로 예상되었습니다.
만약 코드의 공유가 필요하거나 공통의 기능이라면 해당 기능을 mixin으로 추출하여 공유하였습니다.

4번 각 계층은 인터페이스로 먼저 설계하고 해당 인터페이스에 대하여 테스트 코드를 작성한다.
테스트를 신경쓴 이유는 리펙토링에 좀 더 과감하고 테스트가 각 계층의 문서역할을 해주길 바래서였습니다.
총 테스트는 850개정도입니다. 해당 테스트는 ci/cd 배포과정, PR 등에서 코드퀄리티 유지와 기능검수에 대하여 상당한 많은 자동화를 해주었습니다.
테스트를 굉장히 촘촘하게 만든덕분에 위에서 말한 아키텍쳐 개선에 대하여 과감하게 결정 할 수 있게되었습니다.
개선후 검증 비용에 대하여 절약해주었기 때문이죠

후기

처음부터 완벽하진 못했지만 그래도 많은 부분 지켜내고자 했던 부분이 오픈한달차부터도 유지보수에 대하여 굉장한 편리성을 가져다 주는것을 경험하니 굉장히 뿌듯합니다 :)

profile
Software Developer

0개의 댓글