본문 바로가기
개인공부

헥사고날 + 멀티모듈 학습 (모듈간 의존성 약화)

by 리승우 2024. 2. 4.

시작하기에 앞서..

멀티모듈은 무엇인지, 왜 써야 하는지에 대한 설명 및 환경을 구성하는 방법에 대해서는 아래 링크에 기재되었습니다 https://www.notion.so/5a7fbe5ab3554de7a40bd711604f1af5?pvs=4

 

헥사고날에 개념에 대한 설명은 아래 링크를 봐주시면 되겠습니다 https://www.notion.so/Hexagonal-Architecture-9958bc791c034e7d845010cb489975b2

 

 

하단에는 멀티모듈 환경에서 서브 프로젝트간 의존성을 헥사고날의 특징인 의존성 역전법칙을 차용하여 낮춘 방식을 구현한 내용을 정리해두었습니다.


멀티모듈 + 헥사고날 방식 (의존성 약화) 프로젝트 생성

  1. 아래와 같이 멀티모듈 환경 세팅
  • Member
    • ㄴ Modules
      • ㄴ MemberDomain
  • Post
    • ㄴ Modules
      • ㄴ PostDomain
      • ㄴ PostService
  • Common

 

2. settings.gradle, build.gradle 아래와 같이 세팅

 

- settings.gradle

rootProject.name = 'Hexa-Multimodule-tutorial'
include 'Member'
include 'Common'
include 'Post'
include 'Post:Modules:PostService'
include 'Post:Modules:PostDomain'
include 'Member:Modules:MemberDomain'

// findProject(':ex:a')?.name = 'a'
// 1. ex 프로젝트 내부의 a모듈을 찾은 뒤 이름을 가져온다. 만약 없다면 null값 반환
// 2. 이름을 가져오게되면, 해당 모듈의 이름을 'a'로 변경한다

include 'Member:Modules'
findProject(':Member:Modules')?.name = 'Modules'
include 'Post:Modules'
findProject(':Post:Modules')?.name = 'Modules'

 

- build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.0.6'
    id 'io.spring.dependency-management' version '1.1.0'
}

// 이 코드 블록 내에서 정의된 설정은 모든 프로젝트에서 적용됩니다.
allprojects {
    // Gradle 빌드 시스템이 생성하는 라이브러리나 실행 가능한 파일의 그룹을 정의합니다.
    group = 'com.example'
    // Gradle 빌드 시스템이 생성하는 라이브러리나 실행 가능한 파일의 버전을 정의합니다.
    version = '0.0.1-SNAPSHOT'
    // 컴파일러가 사용할 소스 코드의 호환성을 정의합니다.
    sourceCompatibility = '17'

    // Gradle 빌드 시스템에서 사용할 리포지토리를 정의합니다.
    // 이 코드에서는 mavenCentral() 메서드를 통해 Maven Central 리포지토리를 사용합니다.
    repositories {
        mavenCentral()
    }
}

// 이 코드 블록 내에서 정의된 설정은 하위 프로젝트에서 적용됩니다.
// 이 코드 블록은 allprojects와 유사하지만 하위 프로젝트에 대해서만 적용됩니다.
// 이 때 plugins 블록은 사용이 불가능하여 apply plugin을 사용해야함
// core plugin 외의 community plugin의 버전은 앞서 plugins 블록에 선언한 버전을 따라감
subprojects {
    apply plugin: 'java'
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'

    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }

    // Gradle 빌드 시스템에서 사용할 의존성을 정의합니다.
    // 의존성은 라이브러리나 외부 모듈을 사용하기 위한 설정입니다.
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-web'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        annotationProcessor 'org.projectlombok:lombok'
        annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
        compileOnly 'org.projectlombok:lombok'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testCompileOnly 'org.projectlombok:lombok'
        testAnnotationProcessor 'org.projectlombok:lombok'
    }

    // Gradle 빌드 시스템에서 사용할 작업(task)을 정의합니다.
    // 이 코드에서는 test 작업에 useJUnitPlatform() 메서드를 적용하여 JUnit 테스트를 수행합니다.
    tasks.named('test') {
        useJUnitPlatform()
    }
}

// 하위 프로젝트별 개별설정 가능함 (모듈별 설정가능)
project(":Member") {
    bootJar {
        enabled = true
    }
    jar {
        enabled = true
    }

    // 해당 모듈에만 필요한 라이브러리가 발생하면 이곳에 추가
    dependencies {
        // 해당 문구를 적음으로써 원하는 모듈 project 로딩
        implementation project(':Common')
        implementation project(':Member:Modules:MemberDomain')
        implementation project(':Post:Modules:PostService')
    }
}

project(":Member:Modules:MemberDomain") {
    bootJar {
        enabled = false
    }

    jar {
        enabled = true
    }

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation 'org.springframework.boot:spring-boot-starter-jdbc'
        runtimeOnly 'mysql:mysql-connector-java:8.0.31'
    }
}

project(":Post") {
    bootJar {
        enabled = true
    }

    jar {
        enabled = true
    }

    dependencies {
        implementation project(':Post:Modules:PostDomain')
				implementation project(':Common')
    }
}

project(":Post:Modules:PostDomain") {
    bootJar {
        enabled = false
    }

    jar {
        enabled = true
    }

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation 'org.springframework.boot:spring-boot-starter-jdbc'
        runtimeOnly 'mysql:mysql-connector-java:8.0.31'
    }
}

project(":Post:Modules:PostService") {
    bootJar {
        enabled = false
    }

    jar {
        enabled = true
    }

    dependencies {
        implementation project(':Post:Modules:PostDomain')
    }
}

project(":Common") {
    /*
    스프링 부트 기반의 멀티 모듈 프로젝트를 구축할 때, Build를 실행하면 자동으로
    bootJar 태스크를 실행함(스프링 부트 플러그인에 이런 설정이 포함되어 있음)

    bootJar는 실행가능한 jar를 만들려 하기 때문에 main()이 필요함
    그렇기 때문에 main()이 없는 Common은 enabled를 false로 해줘야
    결론적으로 저걸 넣지 않으면 추후 Common에 있는 Bean Class를 다른 모듈에서 사용할 때 에러가 발생할 수 있음.
    */
    bootJar {
        enabled = false
    }

    jar {
        enabled = true
    }

    dependencies {

    }
}

 

위 세팅대로 할 시, 아래와 같은 의존성이 형성됨

 

3. 상황가정

Member 모듈에서 Post 모듈에 있는 값 사용할 목적을 가지고 있음

허나 추후 Post모듈을 변경하게 되면 변경할 코드양이 증가하게 됨. 이를 방지하기 위해 DIP를 하여 의존성을 약화시킬 예정

  • 잠깐! 의존성이란?
    의존성은 소프트웨어 컴포넌트가 다른 컴포넌트에 대해 의존하는 것을 의미
    일반적으로, 의존성은 소프트웨어 개발에 필수적임
    그러나 지나치게 많은 의존성을 가지는 것은 소프트웨어의 유지보수, 변경, 확장을 어렵게 만들 수 있음.

    의존성이 지나치게 많으면 다음과 같은 문제점이 발생할 수 있음.
    1. 복잡도 증가 의존성이 많을수록 코드의 복잡도가 증가. 이는 코드를 이해하고 수정하는 데 필요한 시간과 노력을 증가시키고 버그 발생 확률을 높임.
    2. 유지보수 어려움 의존성이 많을수록 소프트웨어를 유지보수하기 어려워짐. 의존하는 다른 컴포넌트가 변경될 경우, 이를 사용하는 모든 컴포넌트가 영향을 받아 수정이 필요할 수 있음.
    3. 테스트 복잡도 증가 의존성이 많을수록 테스트를 작성하고 실행하는 데 필요한 노력과 시간이 증가 의존하는 다른 컴포넌트를 테스트해야 하므로, 테스트 환경 구축이 복잡해짐.
    4. 기능 확장 어려움 의존성이 많을수록 새로운 기능 추가가 어려워짐. 의존하는 다른 컴포넌트를 고려해야 하므로, 새로운 기능을 추가하기 위해 전체 소프트웨어를 다시 설계하거나 수정해야 할 수도 있음
    따라서, 적절한 수준의 의존성을 유지하고 의존하는 컴포넌트의 범위를 제한하여 소프트웨어의 유지보수성과 확장성을 개선해야 함.

  • 잠깐! DI, DIP란?
    • DI
      • 주로 생성자주입, 필드주입, 메서드 주입등의 방법으로 구현
    • DIP
      • 객체지향 설계 원칙 중 하나로, 상위 수준 모듈은 하위 수준 모듈에 의존해서든 안되며,
        둘 다 추상화된 인터페이스에 의존해야 한다는 것.
        ⇒ 이를 통해 코드 결합도 저하, 코드 유연성 및 확장성을 높일 수 있음
        ⇒ 하위 수준 모듈의 변경이나 교체가 있어도 상위 수준 모듈에 영향을 주지 않음

4. 3번 상황 실현을 위해,

:Post:Modules:PostService 모듈에 Port 인터페이스 생성

:Post:Modules:PostService 모듈에 postService 클래스 생성

 

Post 모듈측에 인터페이스 및 구현체 생성

package com.example.postservice;

public interface Port {
    String postPort();

    String testMethod();
}
package com.example.postservice;

import com.example.postdomain.Post;
import org.springframework.stereotype.Service;

@Service
public class PostService implements Port {

    @Override
    public String postPort() {
        Post post = new Post();
        post.setPostId(1L);
        post.setTitle("제목");
        post.setBoard("내용");
        return post.getPostId() + post.getTitle() + post.getBoard();
        }

    @Override
    public String testMethod() {
        return "testMethod";
    }
    }

 

4. Member 측 모듈에서 Post측에 있는 구현체를 끌어다 씀.

package com.example.member;

import com.example.common.Order;
import com.example.memberdomain.Member;
import com.example.postservice.PostService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class MemberController {

		// Post측 모듈에서 작성된 구현클래스 생성자 주입
    private final PostService postService;

    @GetMapping("/port")
    public String postPort() {
        String post = postService.postPort();
        Member member = new Member();
        member.setId(1L);
        member.setUserName("이승우");
        return member.getId() + member.getUserName() + post + " + Interface 연결확인";
    }
}

 

5. 위와 같이 세팅 후 Post모듈 삭제 시 Member 모듈의 아래 영역만 수정하면 됨. (의존성 약화완료)

 

- 해당 오류를 해결하기 위해선 기존 연동데이터를 대체할 클래스 or 인터페이스 등록하면 됨.

 

 

위 방식의 단점

  1. Post 모듈 삭제 시, 위와 같이 Member 모듈 내 컴파일 에러 발생
  2. Post 모듈 삭제 시, 연관되어 있는 모듈들 각각 컴파일 에러가 발생하여, 일일이 확인하여 유지보수하는 번거로운 상황발생
  3. 객체간 직접적 의존성 및 높은 결합도 (더 낮출 수 있음)

문제해결법

중재자 패턴(Mediator) 을 사용하자!

 

 

중재자 패턴

  • 객체들 간의 복잡한 상호작용을 캡슐화하고 조정하기 위해 중개자 객체를 도입하여 객체 간 결합도를 낮추는 디자인 패턴. 이를 통해 코드의 유연성과 확장성을 높일 수 있음

중재자 패턴 특징

  1. 객체간 결합도 낮춤 중개자 객체를 통해 객체간 직접적 통신을 막고, 중개가 객체를 거쳐서만 통신이 이루어짐 이를 통해 객체 간 결합도를 낮추고, 유지보수와 확장성을 높일 수 있음
  2. 복잡한 객체간 상호작용을 캡슐화 중개자 객체는 객체 간 상호작용을 조정하는 역할을 하여, 객체 간의 복잡한 상호작용을 캡슐화함. 이를 통해 코드의 가독성과 유지보수성을 높일 수 있음
  3. 객체 간의 직접적인 의존성을 제거 중개자 객체를 통해 객체 간의 직접적인 의존성을 제거함 이를 통해 객체 간 결합도를 낮춤

⇒ 객체간의 통신을 위해 서로간에 직접 참조할 필요가 없으며, 객체들 간 수정을 하지않고 관계를 수정할 수 있다

 

 

 

상황 가정

  • Post 모듈에 있는 데이터를 Member 모듈에서 활용하자! 이때, 중재자 모듈을 활용하여 서로에 대한 의존성을 약화시키자!

해결 과정

  1. Mediator(중재자) 모듈 생성 (모듈 이름은 임의대로 변경해도 상관없음)

 

2. Mediator 모듈 내 인터페이스 생성

> 해당 인터페이스를 굳이 만든 이유
1. 해당 인터페이스를 구현하는 클래스가 여러 개일 경우, 각각의 클래스를 구분하고 사용하기 쉬워서
2. 인터페이스를 통해 클래스 간 의존성을 낮추고, 유연성과 확장성을 챙기기 위해

 

package com.example.mediator;

public interface PostInterface {
    String postToMember();

    String testMethod();
}

 

3. Mediator 모듈 내 인터페이스 구현부 생성

 

Post 모듈 데이터 활용하여 Member 모듈과 상호작용하는 구현부 작성

package com.example.mediator;

import com.example.memberdomain.Member;
import com.example.postdomain.Post;
import org.springframework.stereotype.Service;

@Service
public class PostToMember implements PostInterface {

    @Override
    public String postToMember() {

        Post post = new Post();
        post.setPostId(1L);
        post.setTitle("게시물 제목");
        post.setBoard("게시물 내용");

        Member member = new Member();
        member.setId(1L);
        member.setUserName("멤버이름");

        return " * 게시물,멤버 상호작용 로직구현 => " + post.getPostId() + post.getTitle() + post.getBoard() + " + " + member.getId() + member.getUserName();
        }

    @Override
    public String testMethod() {
        return "testMethod";
    }
    }

 

4. MemberService 모듈 내, Mediator 구현부 활용

 

Member 모듈만 다룬 데이터 로직 추가

package com.example.memberservice;

import com.example.common.Order;
import com.example.mediator.PostToMember;
import com.example.memberdomain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final PostToMember postToMember;

    @GetMapping("/port")
    public String postPort() {
        String post = postToMember.postToMember();
        Member member = new Member();
        member.setId(1L);
        member.setUserName("이승우");
        return " * Member 별도로직 작성 => " + member.getId() + member.getUserName() + " | " + post;
    }
}

 

5. MemberApplication 작동확인

 

잘 작동함!

 

 

 

Post 모듈 삭제 시
- Member 모듈 내 컴파일 에러 없음

- Mediator 모듈 내 컴파일 에러만 수정하면 됨

⇒ Member는 Post를 직접적으로 의존하지 않는 상태가 됨
⇒ Mediator 모듈 내, Post를 대체할 모듈을 작성해야 함

모듈 통신에 Mediator를 전부 사용하면, 추후 특정 모듈에서 변경사항이 생겼을 경우
Mediator 모듈만 확인하여 유지보수를 진행하면 됨 (용이성 증가)

Member 모듈 컴파일 에러 없음

 

Mediator 모듈 컴파일 에러 생김 (Post 모듈 삭제로 인한 컴파일 에러)

 

 

모듈별 의존 그래프 (빨간색은 기존대비 새로 추가된 의존성)

 

 

build.gradle 최종형태

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.0.6'
    id 'io.spring.dependency-management' version '1.1.0'
}

// 이 코드 블록 내에서 정의된 설정은 모든 프로젝트에서 적용됩니다.
allprojects {
    // Gradle 빌드 시스템이 생성하는 라이브러리나 실행 가능한 파일의 그룹을 정의합니다.
    group = 'com.example'
    // Gradle 빌드 시스템이 생성하는 라이브러리나 실행 가능한 파일의 버전을 정의합니다.
    version = '0.0.1-SNAPSHOT'
    // 컴파일러가 사용할 소스 코드의 호환성을 정의합니다.
    sourceCompatibility = '17'

    // Gradle 빌드 시스템에서 사용할 리포지토리를 정의합니다.
    // 이 코드에서는 mavenCentral() 메서드를 통해 Maven Central 리포지토리를 사용합니다.
    repositories {
        mavenCentral()
    }
}


// 이 코드 블록 내에서 정의된 설정은 하위 프로젝트에서 적용됩니다.
// 이 코드 블록은 allprojects와 유사하지만 하위 프로젝트에 대해서만 적용됩니다.
// 이 때 plugins 블록은 사용이 불가능하여 apply plugin을 사용해야함
// core plugin 외의 community plugin의 버전은 앞서 plugins 블록에 선언한 버전을 따라감
subprojects {
    apply plugin: 'java'
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'

    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }

    // Gradle 빌드 시스템에서 사용할 의존성을 정의합니다.
    // 의존성은 라이브러리나 외부 모듈을 사용하기 위한 설정입니다.
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-web'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        annotationProcessor 'org.projectlombok:lombok'
        annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
        compileOnly 'org.projectlombok:lombok'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testCompileOnly 'org.projectlombok:lombok'
        testAnnotationProcessor 'org.projectlombok:lombok'
    }

    // Gradle 빌드 시스템에서 사용할 작업(task)을 정의합니다.
    // 이 코드에서는 test 작업에 useJUnitPlatform() 메서드를 적용하여 JUnit 테스트를 수행합니다.
    tasks.named('test') {
        useJUnitPlatform()
    }
}

// 하위 프로젝트별 개별설정 가능함 (모듈별 설정가능)
project(":Member") {
    bootJar {
        enabled = true
    }
    jar {
        enabled = true
    }

    // 해당 모듈에만 필요한 라이브러리가 발생하면 이곳에 추가
    dependencies {
        // 해당 문구를 적음으로써 Common project 로딩
        implementation project(':Common')
        implementation project(':Member:Modules:MemberDomain')
        implementation project(':Member:Modules:MemberService')
        }
}

project(":Member:Modules:MemberDomain") {
    bootJar {
        enabled = false
    }

    jar {
        enabled = true
    }

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation 'org.springframework.boot:spring-boot-starter-jdbc'
        runtimeOnly 'mysql:mysql-connector-java:8.0.31'
    }
}

project(":Member:Modules:MemberService") {
    bootJar {
        enabled = false
    }
    jar {
        enabled = true
    }

    // 해당 모듈에만 필요한 라이브러리가 발생하면 이곳에 추가
    dependencies {
        // 해당 문구를 적음으로써 Common project 로딩
        implementation project(':Common')
        implementation project(':Member:Modules:MemberDomain')
        implementation project(':Mediator')
    }
}

project(":Post") {
    bootJar {
        enabled = true
    }

    jar {
        enabled = true
    }

    dependencies {
        implementation project(':Post:Modules:PostDomain')
    }
}

project(":Post:Modules:PostDomain") {
    bootJar {
        enabled = false
    }

    jar {
        enabled = true
    }

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation 'org.springframework.boot:spring-boot-starter-jdbc'
        runtimeOnly 'mysql:mysql-connector-java:8.0.31'
    }
}

project(":Post:Modules:PostService") {
    bootJar {
        enabled = false
    }

    jar {
        enabled = true
    }

    dependencies {
    }
}

project(':Mediator') {
    bootJar {
        enabled = false
    }

    jar {
        enabled = true
    }

    dependencies {
        implementation project(':Post:Modules:PostDomain')
        implementation project(':Member:Modules:MemberDomain')
    }
}

project(":Common") {
    /*
    스프링 부트 기반의 멀티 모듈 프로젝트를 구축할 때, Build를 실행하면 자동으로
    bootJar 태스크를 실행함(스프링 부트 플러그인에 이런 설정이 포함되어 있음)

    bootJar는 실행가능한 jar를 만들려 하기 때문에 main()이 필요함
    그렇기 때문에 main()이 없는 Common은 enabled를 false로 해줘야
    결론적으로 저걸 넣지 않으면 추후 Common에 있는 Bean Class를 다른 모듈에서 사용할 때 에러가 발생할 수 있음.
    */
    bootJar {
        enabled = false
    }

    jar {
        enabled = true
    }

    dependencies {

    }
}

 

 

settings.gradle 최종형태

rootProject.name = 'Hexa-Multimodule-tutorial'
include 'Member'
include 'Common'
include 'Post'
include 'Post:Modules:PostService'
include 'Post:Modules:PostDomain'
include 'Member:Modules:MemberDomain'
include 'Member:Modules:MemberService'
include 'Mediator'


include 'Member:Modules'
findProject(':Member:Modules')?.name = 'Modules'
include 'Post:Modules'
findProject(':Post:Modules')?.name = 'Modules'
include 'Common:Modules'
findProject(':Member:Modules')?.name = 'Modules'

'개인공부' 카테고리의 다른 글

MongoDB란?  (1) 2023.12.07
VPN VPC 개념  (1) 2023.11.29
Oauth2  (0) 2023.10.16
Redis 개념 및 사용법  (0) 2023.06.03
자동 build Tool별 특징 및 장단점  (0) 2023.05.20

댓글