반응형
05-02 12:14
Today
Total
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
관리 메뉴

개발하는 고라니

Mono repo, Multi Module 위에서 DDD(Domain Driven Design) 를 써보자. 본문

잡동사니

Mono repo, Multi Module 위에서 DDD(Domain Driven Design) 를 써보자.

조용한고라니 2023. 2. 11. 19:40
반응형

들어가며

최근 새로운 프로젝트를 시작하였는데, 이 도메인은 기존 팀에서 진행하던 도메인과 별개의 도메인으로 판단되어, 새 코드 레포지토리가 필요했다.

 

마침 최범균님의 '도메인 주도 개발 시작하기' 책으로 스터디를 했고, 기존에 MVC 패턴으로만 개발하면서 느낀점들을 경험하고 싶지 않아 다른 아키텍쳐를 도입하고자 하였다.

 

사실 DDD를 공부하기 전, 'Clean Architecture' 스터디를 먼저 했었다. 정말 어렵다. 초보자가 단숨에 하기에 벅찬 내용이고, 이걸 도입했다가는 아키텍쳐에 매몰되어 개발에 차질이 있을 것 같았다.

그래서 클린 아키텍쳐는 좀 더 성장해서 써보기로 했다. 그래서 고른 것이 DDD 이다.

MVC 에서 아키텍쳐를 변경하게된 이유

1. 유지보수가 힘들다.

가장 큰 이유이다.

MVC는 처음에 개발하기 굉장히 편리하고, 빠르고 쉽다. 하지만, 이 기능 저 기능 추가되고, 변경되고 하다보면 어플리케이션이 거대해진다.

 

크게 API Controller, Application Service, JPA Repository, JPA Entity를 사용하였다.

controller는 간단하다. service를 호출해 원하는 동작을 실행하고, 결과를 반환하니 괜찮았다.

 

가장 큰 문제는 service 였다. 응용 서비스의 역할은 트랜잭션을 관리하고, 도메인 업무의 순서를 보장하는 것이라 생각한다. 하지만 도메인이 따로 있지 않았고 이에 따라 도메인 로직도 애매하게 JPA Entity에 존재하였다.

그 결과 service의 로직이 비대해져서 조그마한 기능 하나 수정하려해도, 사이드 이펙트를 두려워 할 수 밖에 없다.

 

물론, 테스트를 잘 작성해두었다면 맘 놓고 수정할 수 있었겠지만, 우리의 레거시는 그렇지 못했다. 변명을 하자면, 응용 서비스의 메서드 하나를 테스트 하기 위해서는 수많은 하위 로직을 모킹해야했고, 번거로웠다. (사실 변명이 되진 않는다...)

 

어쨋든 위 이유로 유지보수가 굉장히 힘들다. 그래서 도메인을 코어로 명확히 분리하고 역할과 책임을 몰빵하기로 했다.

 

2. 서비스 로직이 비대하다.

위에서 언급한 내용에 해당하긴 한다. 서비스 로직이 repository를 호출하고, 어떨땐 다른 서비스를 호출하고, 역할과 책임이 모호해 중구난방으로 살이 붙다보면 로직 자체가 비대해져 나중엔 분리하기도 힘들어진다.

 

그래서 각 레이어에 역할과 책임을 분명히 하고자 했다.

 

3. 응용 서비스는 기술 구현체를 몰라야한다.

예를 들어, 외부 API 통신을 할 일이 있다. 그래서 서비스에서 이 API 통신의 기능을 구현한 구현체를 직접 사용하게 될 수도 있다.

, 구현/스펙이 달라진다면? 응용 서비스의 로직 또한 수정되어야한다.

 

이는 SRP (Single Responsibility Principle)을 위반한다.

(실제로, DDD나 클린 아키텍쳐 등 내용에서 SRP를 굉장히 중요하게 생각한다.)

 

요약하자면, 다음과 같다.

  • 각 레이어별 역할과 책임을 명확히 분리한다.
  • 이를 위해 레이어간 침범을 강제적으로 규제한다. (멀티 모듈의 의존 관리)
  • 도메인을 코어로 두고, 핵심 도메인 로직을 갖는다.
  • 응용 서비스는 트랜잭션을 관리하고, 도메인 로직의 순서를 보장하는 것이 역할이다.

멀티 모듈 구조

root
├── api                          - API (controller)
├── application                  - 응용 서비스
├── infrastructure               - 구현기술 (DB, HTTP, Queue)
└── domain                       - 도메인, 도메인 로직, repository

* 이해를 돕기위해 api는 하나만두고, 몇몇 모듈은 제외

 

[API / 표현 영역]

HTTP 요청을 응용 역역이 필요로 하는 형식으로 변환해서 응용 영역에 전달하고,

응용 영역의 응답을 HTTP 응답으로 변환하여 전송한다.

 

[Application / 응용 영역]

시스템이 사용자에게 제공해야할 기능을 구현한다.

기능을 구현하기 위해 도메인 영역의 도메인 모델을 사용한다.

로직을 직접 수행하기보다 도메인 모델에 로직 수행을 위임한다.

응용 서비스, Http client를 래핑한 Adapter interface 등이 이 영역에 속한다.

 

[Domain / 도메인 영역]

도메인 모델을 구현한다.

도메인 모델은 도메인의 핵심 로직을 구현한다.

예를 들어, 주문 도메인은 '배송지 변경', '결제 완료', '주문 총액 계산'과 같은 핵심 로직을 모두 도메인 모델에서 구현한다.

도메인 Repository는 이 영역에 속한다.

 

[Infrastructure / 인프라스트럭처 영역]

구현 기술에 대한 것을 다룬다.

RDBMS 연동, 메시징 큐에 메세지 전송하거나, 수신하는 기능을 구현한다.

SMTP를 이용한 메일 발송 기능, HTTP 클라이언트를 이용해 REST API 호출을 처리한다.

논리적인 개념을 표현하기 보다 실제 구현을 다룬다.

JPA Entity, JPA Repository는 이 영역에 속한다.

 

도메인 / 응용 / 표현 영역은 구현 기술을 사용한 코드를 직접 만들지 않는다. 대신 인프라스트럭처 영역에서 제공하는 기능을 사용해 필요한 기능을 개발한다.

모듈의 의존 관리

위는 각 모듈(영역)의 의존성을 도식화한 것이다.

 

표현 영역은 응용 영역을 의존한다. 그리고 런타임 시점에 인프라스트럭쳐를 주입 받는다.

(이는 멀티 모듈 특성상 필요한 작업이다. 응용 영역에서 인프라스트럭처를 직접적으로 사용하지 않지만,

인프라스트럭처에 구현체가 있어서, 이를 Spring의 Bean으로 만들어 런타임 시점에 사용할 수 있게 해야한다.)

 

응용 영역은 도메인 영역을 의존한다.

 

도메인은 POJO 객체로, 어느 것에도 의존해선 안된다.

 

인프라스트럭처 영역은 도메인에 의존하고, 응용 영역에 의존한다. (응용 영역에 정의된 interface를 구현하기 위함)

/**
build.gradle.kts 중 일부
*/
...

project(":api") {
    dependencies {
        implementation(project(":application"))
        runtimeOnly(project(":infrastructure"))
    }
}

project(":application") {
    dependencies {
        implementation(project(":domain"))
    }
}

project(":infrastructure") {
    dependencies {
        implementation(project(":domain"))
        implementation(project(":application"))
    }
}

...

각 모듈의 구성 및 흐름도

 

[Domain Repository]

도메인을 query/command 하는 리퍼지토리이다.

interface UserRepository {
  fun save(user: User)
}

 

[Domain <-> Entity]

나는 Domain과 Entity를 분리했다.

즉, User라는 도메인이 있으면 UserEntity라는 엔티티가 있다.

그래서 Infrastructure에 이 둘을 매핑(mapping) 해주는 Mapper를 두었다.

object UserMapper {
  fun toDomain(entity: UserEntity): User {
  ...
  }
  
  fun toEntity(domain: User): UserEntity {
  ...
  }
}

//Domain
data class User(
  val id: Long,
  val name: String,
  val email: String,
  ...
)


//Entity - Infrastructure
@Entity
data class UserEntity(
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0L,
  val name: String,
  val email: String,
  ...
)

 

[Persistence Adapter]

Domain에 있는 Repository를 구현한 구현체이다.

JPA Repository를 이용해 기능을 구현한다.

@Component
class UserPersistenceAdapter (
  val userJpaRepository: UserJpaRepository,
) : UserRepository {

  override fun save(user: User) {
    val entity: UserEntity = UserMapper.toEntity(user)
    
    userJpaRepository.save(entity)
}

 

TODO..

결론

이렇게 개발해보니, 로직의 복잡도가 낮아지고 로직이 작아졌다. 그러다보니 가독성 또한 높아졌다.

무엇보다 단위 테스트 작성에 거리낌이 없었다.

각 영역의 역할과 책임을 확실히 나누니, mocking 해야할 것이 줄었고 내가 테스팅 해야할 것에 집중할 수 있었다.

 

무조건 좋은 점만 있는 것은 아니었다.

보일러 플레이트 코드가 늘었고, 내가 만들어놓은 규제에 빠져 오랜 시간 고민이 필요하기도 했다.

 

그래도 의미있는 고민이었고, 그 결과 현재의 내가 낼 수 있는 최고의 퍼포먼스를 냈다고 생각한다. 그간 공부해온 것을 쏟아부은 집합체라 할 수 있을 것 같다.

clean code, DDD, clean architecture(살짝), kotlin in action 그리고 effective kotlin

 

1년 전의 내가 짜던 코드와 비교하였을 때 확실히 성장한 것이 보여서 뿌듯했다.

이를 기반으로 다음 공부하고 시도해볼 것은 헥사고날 아키텍처이다.

 

여느 아키텍처 관련 서적에서 말하듯이, 정답인 아키텍쳐는 없다. 각 상황과 팀, 도메인의 복잡도에 따라 유연하게 적절한 아키텍쳐를 가져가면 된다.

 

Github]

 

GitHub - rhacnddl/monorepo: Mono Repo, Multi Module

Mono Repo, Multi Module. Contribute to rhacnddl/monorepo development by creating an account on GitHub.

github.com

 

Reference]

도메인 주도 개발 시작하기.최범균.한빛미디어

반응형
Comments