본문 바로가기
Design pattern/DDD

멀티모듈로 헥사고날 아키텍쳐 구현하기[1] - 모듈 구성

by 코딩공장공장장 2024. 3. 10.

이전 포스팅에서 헥사고날 아키텍쳐에 대하여 설명하였다.

 

헥사고날 아키텍쳐 패턴을 멀티모듈 방식으로 구성한 방식에 대해 공유해보겠다.

 

코틀린 스프링 부트 환경에서 구현하였으니 코틀린 스프링 부트 기준으로 설명을 하겠다.

 

헥사고날 아키텍쳐의 핵심인 application layer는 순수 pojo 방식을 지향한다.

 

그리고 어떠한 라이브러리나 adapter(컨트롤러, repository, 3rd-party-api)의 의존도 갖지 않는다.

 

이번 포스팅에서는 모듈구성에 관한 내용을 다루고,

 

다음 포스팅에서 순수 pojo 방식으로 application layer를 구성하는 방법에 대해 다루도록 하겠다.

 

목차

  • 모듈 구성
  • 패키지 구성
  • gradle 설정

 

모듈 구성

 

 

헥사고날 아키텍쳐를 간단하게 설명하면

 

비즈니스 로직을 갖는 application core와 클라이언트와 소통하는 user interface, 그리고 application이 필요로하는 툴(DB, 3rd-party-api)을 사용할 수 있게 하는 infrastructure 영역으로 구분된다.

 

비즈니스 로직은 순수한 프로그래밍 언어로 표현되도 우리 비즈니스 규칙을 담고 있다.

 

따라서 어떠한 기술에도 종속되지 않고 순수한 우리 비즈니스를 표현할 수 있어야한다.

 

그래야 기술에 변경이 일어나더라도 코드의 수정없이 재사용이 가능하다.

 

반면 user interface와 infrastructure는 기술에 종속되어있다.

 

user interface쪽을 보면 web server와 소통하기 위한 adapter들이 존재하고 이 adapter들은 각각 특정 기술에 종속되어있다.

 

이는 infrastructrue도 마찬가지이다.

 

이렇게 기술과 비즈니스 로직을 구분하고 또한 툴을 사용을 위해 기술에 종속된 adapter들간의 경계를 명확하게 하여 기술의 변경이 비즈니스 로직에 영향을 미치지 않고 다른 adpater들에게도 영향을 미치지 않게 하는 것이 헥사고날 아키텍쳐의 장점이다.

 

나는 이 경계를 모듈을 통해 확실히 구분짓고자 멀티모듈 방식을 채택하였다.

 

모듈의 구성은 아래와 같이 하였다.

  • app-domain : 비즈니스 로직을 표현하기 위한 객체와 이 객체를 다루는 util 클래스 등으로 구성
  • app-service : 비즈니스 로직인 service 클래스 및 user-interface와 infrastructure와 소통할 인터페이스로 구성
  • bootstrap : 스프링 프로젝트를 구동하기 위한 목적으로 부트스트랩 클래스만 존재
  • infrastructure  : DB 영속화, 3rd-party-api 등 application layer에서 호출하는 driven-adapter 모듈이 위치
    - orm-adapter : DB 접근 목적의 adapter(repository, entity 등)
  • user-inerface  : 클라이언트와 소통하며 application layer를 호출하는 driving-adapter 모듈들이 위치
    - rest-api : 웹서버와 소통하는 controller로 구성
  • modules : 재사용과 추상화 목적으로 추가한 모듈로 user-interface나 infrastructure에서 사용할 모듈들이 위치한다.

 

하나하나 자세히 설명해보도록 하겠다.

 

app-domain 모듈

 

app-domain 모듈은 application core의 domain layer에 해당하는 부분으로 비즈니스 규칙을 담은 dto들이 위치한다.

 

또한 헥사고날 아키텍쳐 설명에 의한 이 레이어에 domain service라는 domain 객체를 다루는 상태가 존재하지 않는 service가 존재할 수 있다고 하였는데 용어의 application layer의 service와 용어 혼돈을 피하고자 그냥 util이라는 용어를 사용하였다.

dependencies {
}

 

build.gradle.kts의 의존설정에는 위와 같이 아무것도 존재하지 않는다.

 

app-service 모듈

 

application layer에 해당하는 모듈이다.

 

비즈니스 로직을 담은 Service 클래스들이 존재하고

 

user-interface 영역의 adapter가 application layer를 호출하기 위한 인터페이스가 port.in 패키지에 존재하고

 

application layer가 호출하는 infrastructure의 adapter의 인터페이스가 port.out에 존재한다.

 

application layer와 소통하는 모든 인터페이스는 application layer 안에 존재하므로

 

application layer는 외부 툴이 변경되더라도 영향을 받지 않는다.

 

구현체가 존재하는 user-interface와 infrastructure에서 책임져야한다.

 

port와 usecase라는 용어는 ddd를 기반으로 정하였다.

 

dependencies {
    implementation(project(":app-domain"))
}

 

의존설정은 오직 app-domain에만 의존한다.

 

user-interface : rest-api

나의 프로젝트에는 user-interface에 존재하는 driven-adpater는 rest-api 밖에 존재하지 않으므로 한번에 설명하겠다.

 

user-interface에는 여러 adapter들이 존재할 수 있다. 

 

웹서버와 소통하는 adapter가 존재할 수 있고 cli와 소통하는 adapter가 존재할 수 있다.

 

나의 경우 웹서버와 소통하는 adapter 하나만 존재하기에 rest-api모듈 하나만 생성하였고

 

구조는 아래와 같다.

 

controller와 dto, mapper가 존재한다.

 

헥사고날 아키텍쳐는 application layer 중심이고 인터페이스도 application layer에서 정의한다.

 

app-service의 port.in 패키지에 정의된 인터페이스를 통해 application layer를 호출한다.

 

또한 인터페이스에서 정의한 dto를 메서드의 인자값으로 넣어줘야하니 mapper를 통한 변환 처리도 여기서 담당한다.

 

이 input dto는 당연하게 app-domain에 정의된 dto 객체이다.

 

    implementation(project(":app-domain"))
    implementation(project(":app-service"))

    implementation("org.springframework.boot:spring-boot-starter-web")

 

따라서 위와 같이 app-domain과 app-service 의존성이 필요하다.

 

뿐만아니라 spring-boot-starter-web 의존성도 필요하다. 웹서버라는 툴과 소통하기 위한 스프링 부트 라이브러리이다.

 

바로 이 라이브러리가 기술에 종속된 라이브러리이고 추후 웹서버와 소통하는 라이브러리나 프레임워크를 바꾼다면

 

이 모듈만 변경하면 된다.

 

참고로 스프링 의존성을 설정했다고 해서 부트스트랩 클래스는 만들지 말자.

 

이유는 bootstrap 모듈을 설명하며 이야기 하겠다.

 

import org.springframework.boot.gradle.tasks.bundling.BootJar
import org.springframework.boot.gradle.tasks.run.BootRun

tasks.named<BootJar>("bootJar") {
    enabled = false
}

tasks.named<BootRun>("bootRun") {
    enabled = false
}

 

또한 gradle 설정에 위와 같이 bootJar, bootRun이 실행 되지 않도록 enabled 설정을 false로 선언하자.

 

이유는 마찬가지로 bootstrap 모듈을 설명하며 이야기하겠다.

 

infrastructure : orm-adapter

application layer에서 호출하는 adapter이다.

 

user-interface와 마찬가지로 하위에 여러 모듈이 존재할 수 있다. 

 

db, sms, email, s3와 같은 외부 툴을 사용하기 위한 adapter가 여기에 존재한다.

 

현재 프로젝트에서는 db만 연동하므로 orm-adapter만 존재한다.

 

위와 같이 entity, repository, mapper 패키지가 존재한다.

 

repository는 app-service의 port.out 패키지에서 정의한 인터페이스의 구현체이다.

 

인터페이스에서 정의한 output 값으로 변환해서 값을 전달해야하므로 이 모듈에도 mapper가 존재한다.

 

따라서 entity라는 객체는 이 모듈 밖으로 절대 나가지 않을 것이다.

 

implementation(project(":app-domain"))
implementation(project(":app-service"))

implementation("org.springframework.boot:spring-boot-starter-data-jpa")

 

app-domain과 app-service를 의존하고 spring-starter-data-jpa 모듈을 의존한다.

 

spring-starter-data-jpa가 바로 db 영속화를 위한 기술에 종속된 모듈이다.

 

우리가 영속화를 위한 기술을 바꾼다면 이 모듈만 영향을 받고 다른 모듈은 영향을 받지 않는다.

 

rest-api 모듈과 마찬가지로 bootJar, bootRun 실행을 막고 부트스트랩 클래스는 만들지 말자.

import org.springframework.boot.gradle.tasks.bundling.BootJar
import org.springframework.boot.gradle.tasks.run.BootRun

tasks.named<BootJar>("bootJar") {
    enabled = false
}

tasks.named<BootRun>("bootRun") {
    enabled = false
}

 

bootstrap 모듈

헥사고날 아키텍쳐를 멀티모듈에서 구현한 많은 글들에서 bootstrap 모듈을 별도로 두고 있다.

 

사실 나는 왜 bootstrap 모듈을 별도로 두는지에 대해 이해하지 못하였었다.

 

그리하여 나의 과거 프로젝트는 rest-api와 같은 클라이언트의 요청을 최초로 받는 모듈, 프로젝트가 시작되는 모듈에 bootStrap 클래스를 두고 모든 모듈을 해당 모듈에서 import하고 있었다.

 

물론 user-interface에 해당하는 adapter가 단 하나만 존재하고 앞으로도 하나만 존재할 것이라면 해당 모듈에 부트스트랩 클래스를 위치시켜도 된다.

 

허나, user-interface 안에 다양한 클라이언트 환경과 소통할 adapter를 여러개 둔다면 별도로 bootstrap클래스가 위치할 모듈을 만들자.

 

bootstrap은 스프링 프로젝트를 시작하기 위해 ioc 컨텍스트를 띄우고 빈 생성 및 초기화를 담당한다.

 

즉 스프링 프로젝트를 실행하는 클래스가 bootstrap 클래스이다.

 

만약 user-interface에 해당하는 adapter가 클라이언트의 최초의 요청을 받기에 프로젝트의 시작점이라고 생각하고 부트스트랩 클래스를 둔다면 어떻게 되겠나??

 

하나의 프로젝트를 여러개로 나누어서 구동하는 것이다.

 

이상한 발상이다. 누가 이렇게 하는 사람도 없겠지만 과거 나의 프로젝트 구조를 억지로 짜맞춰서 구동한다면 user-interface의 모듈마다 프로필을 별도로 두고 port를 각각 할당하여 하나의 프로젝트를 adapter마다 하나씩 실행하는 것이다.

 

만약 app-service와 app-domain을 공유하고 별도의 서비스라면 그렇게 해도 된다.

 

하지만 그게 아니라 bootstrap 모듈을 따로 만들어 프로젝트가 하나만 실행되도록 하자.

 

(나의 가장 큰 착각은 클라이언트의 최초 요청을 받는 지점이 프로젝트를 실행시키고 시작시켜야하는 지점이라고 생각한 것이다. 프로젝트를 실행 및 설정의 초기화와 서비스의 시작점과 다르다.)

 

implementation(project(":app-domain"))
implementation(project(":app-service"))

implementation(project(":user-interface:rest-api"))
implementation(project(":infrastructure:orm-adapter"))

implementation("org.springframework.boot:spring-boot-starter-web")

 

따라서 의존성은 위와 같이 모든 모듈에 대한 의존성이 필요하다.

 

또한 당연히 부트스트랩 클래스를 포함해야하므로 스프링 의존또한 하나 필요하다.

 

그리고 이제 우리는 아래와 같이 bootJar과 bootRun의 enabled 설정을 true로 설정할 수 있고

 

이 부트스트랩 모듈을 실행함으로써 프로젝트를 구동할 수 있고 bootJar 파일도 만들 수 있다.

import org.springframework.boot.gradle.tasks.bundling.BootJar
import org.springframework.boot.gradle.tasks.run.BootRun

tasks.named<BootJar>("bootJar") {
    enabled = true
    archiveVersion = "1.0.0" // 버젼 정보
    archiveBaseName = "hmc-backoffice-management-mgt"
}

tasks.named<BootRun>("bootRun") {
    enabled = true
}

 

 

modules

이 모듈은 말그대로 모듈이다. 

 

헥사고날 아키텍쳐에 포함되는 adapter는 아니고 말그대로 필요한 모듈을 구현하기 위해 위치 시켜놓았다.

 

각 adapter에 필요한 모듈을 직접 구현해야하는 경우 adapter에 직접 구현해도 되겠지만

 

재사용성과 변경을 고려한다면 추상화를 통해 모듈을 만들어 연동하는 것이 좋기에 별도로 분리해 두었다.

 

마무리

이렇게 해서 헥사고날 아키텍쳐 패턴을 멀티모듈로 구성해 보았다.

 

허나 이렇게만 하면 절대 실행되지 않는다.

 

그렇다. app-service의 service는 스프링 의존성이 없어 빈으로 등록되지 않는다.

 

다음 포스팅을 통해 app-service의 service 클래스를 스프링 의존성 없이 빈으로 띄우는 방법을 알아볼 것이다.

 

그리고 이 모듈이 어떻게 연결되어 실행되는지 구동 원리에 대해서도 설명할 것이다.

 

 

반응형