Spring boot 멀티 모듈 전환기

E4ger·2024년 10월 30일
1

멀티 모듈 재밌겠다.

사이드 프로젝트를 진행하던 중 우연히 멀티 모듈이라는 키워드를 알게 되었는데요.
저는 지금까지 늘 프로젝트를 진행하면서 하나의 프로젝트에 위 그럼처럼
Controller, Service, Repository를 모두 포함하고 있었습니다.
이 구조가 아직까지는 그리 복잡하다고 느끼지 않긴 했습니다만 멀티 모듈을 통해 제가 예전에 느꼈던
단일 모듈의 단점을 해결하는 것을 보게 되었습니다.

제가 이전에 진행했었던 프로젝트는 프리랜서 작업의 스케줄을 관리하는 플랫폼이였는데요.
요구 사항으로 중간에서 모든 기능을 관리하는 관리자(Admin)이라는 개념이 생기게 되었습니다.
예를 들어 위와 같이 일반 유저들은 Schedule을 등록,조회,수정,삭제 하는 기능이 있었다면
관리자도 유저들이 올린 Schedule을 등록,조회,수정,삭제하는 기능이 있어야 했습니다.
그리고 관리자는 Schedule관련 기능을 포함해서 프로젝트에 존재하는 다른 서비스들도 관리해야했기에
유저들이 사용하는 서버와 관리자들이 사용하는 서버를 나누게 되었습니다.


// User Spring Server
// SheduleService
public Schedule getSchedule(Long projectId, Pageable pageable) {
	repository.getSchedule(projectId, pageable);
}
// -------------------------- //


// Admin Spring Server
// SheduleManagementService
public Schedule getSchedule(Long projectId, Pageable pageable) {
	repository.getSchedule(projectId, pageable);
}

예시로 특정 작업의 스케쥴을 조회하는 로직을 구현했다고 가정하겠습니다.
일반 유저들이 사용하는 서버와 관리자가 사용하는 서버에서
이 로직을 구현하는 코드를 보면 전혀 다른 점이 없게 됩니다.
차이라고 함은 유저 서버에서는 해당 작업에 대한 권한이 있는지 검증하는 정도라고 볼 수 있겠네요.
권한을 검증하는 것은 getSchedule이라는 함수 호출 전 진행하면 되는 것이고
단순히 조회를 하는 로직은 100퍼센트 동일하였습니다.

// ScheduleRepository
public interface ScheduleRepository {
	public Schedule getSchedule(Long projectId, Pageable pageable);
}

위와 같이 유저 서버와 관리자 서버의 ScheduleRepository 내부 코드도 대부분 동일하였습니다.

저는 유저 서버와 관리자 서버 코드를 작성할 때 이미 만들어둔 유저 서버의 코드를
(ex) DTO, Service, Entity)들을 전부 그대로 복사 붙여넣기 해서
관리자 서버 프로젝트에 적용한 기억이 아직까지 선명합니다.

멀티 모듈은 이와 같은 문제를 해결해줄 수 있는데요.
위 그림처럼 Schedule 관련한 로직을 모듈화하여 User Server와 Admin Server에서
사용하도록 하면 하나의 코드로 여러 곳에서 재사용 할 수 있게 됩니다.
제 프로젝트에도 관리자 도메인 영역이 추가 될 예정이기에 작성해둔 로직을 모듈화하여
관리자 도메인을 만들 때 이용해보자라고 생각하였습니다.
또한 멀티 모듈은 재사용이란 장점도 있지만 같은 기능을 하는 도메인영역끼리도 모듈화하는 방식이 있기에
나중에 구조가 깔끔해질 것 같다는 생각이 들어 전환을 시작하였습니다.

멀티 모듈 도입 전 프로젝트

제가 제작하고 있는 사이드 프로젝트의 주제는 크리에이터가 직접 문제를 만들어서 올릴 수 있고
유저들은 일정 포인트를 지불하여 구매하는 것이 주요 비즈니스입니다.
(여기서 문제라는 것은 고등학교 과정에 있는 국어, 수학, 영어, 탐구를 의미합니다.)

현재 프로젝트는 어느 정도 진행이 된 상태이며 구현이 완료 된 도메인을 위와 같이 나열해보았습니다.
나열된 각 영역은 더 이상 쪼개어 질 수 없는 수준의 도메인이라고 생각합니다.
따라서 저도 구현을 하다보니 프로젝트 패키지의 구조가 굉장히 복잡해졌는데요.

├── annotation
├── aop
├── common
├── config
├── domain
│   ├── authentication
│   │   ├── controller
│   │   ├── dto
│   │   ├── implement
│   │   ├── repository
│   │   ├── service
│   │   └── vo
│   ├── board
│   │   ├── controller
│   │   ├── dto
│   │   ├── entity
│   │   │   └── converter
│   │   ├── implement
│   │   ├── model
│   │   ├── repository
│   │   ├── service
│   │   └── vo
│   ├── comment
│   │   ├── controller
│   │   ├── dto
│   │   ├── entity
│   │   ├── implement
│   │   ├── model
│   │   ├── repository
│   │   └── service
│   ├── coupon
│   │   ├── controller
│   │   ├── dto
│   │   ├── entity
│   │   ├── implement
│   │   ├── model
│   │   ├── repository
│   │   ├── service
│   │   └── vo
│   ├── creator
│   │   ├── controller
│   │   ├── dto
│   │   ├── entity
│   │   ├── implement
│   │   ├── model
│   │   ├── repository
│   │   ├── service
│   │   └── vo
│   ├── help
│   │   ├── controller
│   │   ├── dto
│   │   └── service
│   ├── library
│   │   ├── controller
│   │   ├── dto
│   │   ├── entity
│   │   ├── implement
│   │   ├── model
│   │   ├── repository
│   │   └── service
│   ├── payment
│   │   ├── controller
│   │   ├── dto
│   │   ├── entity
│   │   ├── implement
│   │   ├── model
│   │   ├── repository
│   │   └── service
│   ├── point
│   │   ├── controller
│   │   ├── dto
│   │   ├── entity
│   │   ├── implement
│   │   ├── model
│   │   ├── repository
│   │   ├── service
│   │   └── vo
│   ├── portone
│   │   ├── dto
│   │   ├── enums
│   │   └── implement
│   ├── question
│   │   ├── common
│   │   ├── controller
│   │   ├── dto
│   │   ├── entity
│   │   ├── implement
│   │   ├── model
│   │   ├── repository
│   │   ├── service
│   │   └── vo
│   ├── review
│   │   ├── controller
│   │   ├── dto
│   │   ├── entity
│   │   ├── implement
│   │   ├── model
│   │   ├── repository
│   │   └── service
│   ├── social
│   │   └── implement
│   ├── subscribe
│   │   ├── controller
│   │   ├── dto
│   │   ├── entity
│   │   ├── implement
│   │   ├── model
│   │   ├── repository
│   │   └── service
│   ├── user
│   │   ├── controller
│   │   ├── dto
│   │   ├── entity
│   │   ├── implement
│   │   ├── model
│   │   ├── repository
│   │   ├── service
│   │   └── vo
│   ├── verification
│   │   ├── dto
│   │   ├── entity
│   │   ├── implement
│   │   ├── model
│   │   ├── repository
│   │   ├── template
│   │   └── vo
│   └── workspace
│       ├── controller
│       ├── implement
│       └── service
├── exception
├── resolver
├── security
└── valid

멀티 모듈 작업을 하기 전 프로젝트 구조는 위와 같습니다. src 디렉토리 내부가 위 구조처럼 되는데요.
나름 프로젝트에 기능들이 많기 때문에 여러 영역들이 있는 것은 당연합니다.
하지만 지금 구조는 여러 하위 도메인을 모아 새로운 도메인 영역을 만들지 않았기 때문에
패키지 디렉토리가 굉장히 많아 보이기는 한 것 같습니다.
일단 멀티 모듈화를 진행하고 도메인 영역 재확립도 진행해봐야겠습니다.

멀티 모듈 설계 방식

저는 진행하기전에 모듈을 어떤 기준으로 쪼개야 할까에 대한 고민을 하였습니다. 그래서 컨퍼런스나 블로그를
많이 찾아보았는데요.

실전! 멀티 모듈 프로젝트 구조와 설계 | 인프콘 2022

제가 처음으로 봤던 방식은 Boot, Infra, Cloud, Data 4개로 구분짓는 것을 볼 수 있었습니다.
Boot는 제 프로젝트 환경에서는 API Endpoint 즉 요청을 받는 Spring Server가 되겠습니다.

Infra는 외부 API로 제 프로젝트에서는 포인트 결제를 위해 사용되는 외부 PG사 API,
소셜 인증을 위한 (Kakao,Google,Naver) API를 이용하여 처리하는 로직이 되겠습니다.

Data는 도메인 영역으로 비즈니스 로직 및 DB 데이터 핸들링인 것 같은데요.
제 프로젝트에서는 domain 비즈니스 로직, Repository, Entity등이 될 것 같습니다.

Cloud는 Cloud(aws,gcp,azure)와 같은 클라우드 설정 관련을 모듈화 한 것 같습니다.

SpringBoot + Kotlin 멀티 모듈 구성 - 도메인 모듈 분리 #4 - 제미니의 개발실무

두번째로 봤던 방식은 Spring Server / Storage / Domain 3가지 모듈로 나누는 방식이였습니다.
Server - API 요청을 받는 End Point로 Spring Server입니다.
Storage - DB 데이터 핸들링을 합니다.
Domain - Domain 영역을 구현하는 비즈니스 로직이 포함되어 있습니다.

그리고 구체적으로 각 모듈이 어떻게 참조하고 있는지 정리가 되어있는데요.

Spring Server는 Domain 모듈을 참조하고 있습니다. 또한 Spring Server는 Storage를
Runtime Only로 사용하고 있는데요. 이렇게 되면 Spring Server는 Storage가 어떻게
구현되어있는지 즉 Mysql인지 MongoDB인지 두 개다 사용하고 있는지 알 필요가 없기 때문에
Spring Server는 DB와 관련 된 의존성이 없어지게 됩니다.

Storage는 Mysql + JPA 라면 JPA entity 및 jpaRepository, repository 구현체들이 있을 것 입니다.
Redis를 사용한다면 Redis 관련 의존성이 추가 되겠네요.

Domain은 API 요청을 처리하기 위한 비즈니스 로직이 있게 됩니다. 추가로 Domain 모듈은 최대한
다른 의존성을 줄이는 게 좋다고 언급이 되는데요. 이 부분은 아래에서 설명하겠습니다.

결론적으로 저는 평소에 제미니의 개발실무라는 유튜브 채널에 올라오는 주제에 대해 많은 고민을 하게 되고
제게 많은 영감을 준 채널이라 이번 프로젝트에서는 두번째 방식으로 진행하게 되었습니다.

결론적으론 제 프로젝트 구조는 위와 같이 qpi, core(domain), storage로 멀티 모듈화 되었습니다.

API 모듈

API 모듈은 Spring Server인데요. API 요청을 받고 domain 모듈을 이용하여 요청을 처리하고 응답을 주게 됩니다.

└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── eager
    │   │           └── questioncloud
    │   │               ├── aop
    │   │               ├── api
    │   │               │   ├── authentication
    │   │               │   ├── creator
    │   │               │   ├── library
    │   │               │   ├── payment
    │   │               │   ├── question
    │   │               │   ├── subscribe
    │   │               │   └── user
    │   │               ├── common
    │   │               ├── config
    │   │               ├── exception
    │   │               ├── resolver
    │   │               ├── security
    │   │               └── validator
    │   └── resources

qc-api 모듈 내부는 위와 같이 설계되었습니다.

aop - 특정 로직 구현을 위해 AOP를 사용하였고 AOP Asepect 클래스가 존재합니다.
api - controller가 존재합니다.
common - API 처리를 위해 사용되는 공통 클래스가 있습니다. ex) DefaultResponse
config - Spring Boot 관련 Config가 있습니다. ex) Filter, Resolver
exception - 인증 관련 예외및 domain 모듈의 CustomException을 처리합니다. ex) ControllerAdvice
security - Spring Security 관련 설정 Config가 있습니다.
validator - API 요청 시 Request를 검증하는 Validator가 있습니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'

    runtimeOnly(project(":qc-storage"))
    implementation(project(":qc-core"))
}

qc-api의 build.gradle에서는 qc-storage를 runtimeOnly로 qc-core를 implementation으로 참조하며
Spring Server 구동을 위한 SpringBoot 의존성을 추가한 상태입니다.

@RestController
@RequestMapping("/api/board")
@RequiredArgsConstructor
public class QuestionBoardController {
    private final QuestionBoardService questionBoardService;
    @GetMapping("/{boardId}")
    public QuestionBoardResponse getQuestionBoard(
        @AuthenticationPrincipal UserPrincipal userPrincipal, @PathVariable Long boardId) {
        QuestionBoardDetail board = questionBoardService.getQuestionBoardDetail(userPrincipal.getUser().getUid(), boardId);
        return new QuestionBoardResponse(board);
    }
}

이처럼 qc-api는 API요청이 들어오면 qc-core를 이용해 비즈니스 로직을 처리하도록 하고응답을 주는데요.
qc-core가 비즈니스 로직을 알아서 처리할 것이기 때문에 qc-api 모듈에서는
API 요청에 대한 인가, 검증, 응답에 대한 책임만 가지게 됩니다.

Domain 모듈

├── main
│   ├── java
│   │   └── com
│   │       └── eager
│   │           └── questioncloud
│   │               └── core
│   │                   ├── common
│   │                   ├── domain
│   │                   │   ├── authentication
│   │                   │   │   ├── dto
│   │                   │   │   ├── implement
│   │                   │   │   ├── repository
│   │                   │   │   ├── service
│   │                   │   │   └── vo
│   │                   │   ├── feed
│   │                   │   │   ├── library
│   │                   │   │   │   ├── dto
│   │                   │   │   │   ├── implement
│   │                   │   │   │   ├── model
│   │                   │   │   │   ├── repository
│   │                   │   │   │   └── service
│   │                   │   │   └── subscribe
│   │                   │   │       ├── dto
│   │                   │   │       ├── implement
│   │                   │   │       ├── model
│   │                   │   │       ├── repository
│   │                   │   │       └── service
│   │                   └── exception
│   └── resources

비즈니스 로직을 처리하는 core(domain) 모듈입니다.

common - domain 모듈 내부에서 공통적으로 사용되는 클래스가 있습니다. ex)PagingInformation
domain - 각 domain이 위치하고 있으며 domain model, 비즈니스 로직을 처리하는 클래스가 위치하고 있습니다.
exception - 비즈니스 로직 수행 도중 발생할 수 있는 예외들이 선언되어 있습니다.

dependencies {
    implementation "org.springframework:spring-context"
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-mail'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation('io.jsonwebtoken:jjwt-api:0.11.1')
    runtimeOnly('io.jsonwebtoken:jjwt-impl:0.11.1', 'io.jsonwebtoken:jjwt-jackson:0.11.1')
    compileOnly("org.springframework:spring-tx")
}
bootJar.enabled = false
jar.enabled = true

qc-core의 build.gradle입니다. 위에서 언급했었던 것처럼 domain 모듈은 최대한 외부 의존성이 없으면 좋다고 하였습니다.

특히 개인적으론 Spring boot의 의존성을 제거하고 싶었는데요.
qc-api는 내부적으로 Spring boot 관련 의존성을 주입받고 있는데
qc-core에서도 Spring boot 관련 의존성을 주입받고 있다면 두 모듈에서 같은 의존성을 가지게 됩니다.
그렇다고 qc-core에서 Spring boot 관련 의존성을 제거 한 후
qc-core가 qc-api를 참조하게 되면 core <-> api 양방향 참조가 때문에 애매한 관계가 되어버립니다.

@Service
@RequiredArgsConstructor
public class CreatorService {
}

하지만 qc-core에서 Spring boot 관련 의존성을 아얘 사용하지 않겠다고 해도 난감해지는데요.
core 모듈 내부에 있는 @Service라는 어노테이션을 사용하지 못하게 되고
qc-api 내부에서 Component Scan을 이용해 직접 로드해야 합니다.

implementation "org.springframework:spring-context"

그래서 초반에 저는 위 의존성을 추가해서 spring-context를 추가하여 어노테이션을 사용하도록 하였는데요.

@Component
@RequiredArgsConstructor
public class AuthenticationProcessor {
 	`
    `
    `
    public void authentication(Long uid) {
        UserWithCreator userWithCreator = userReader.getUserWithCreator(uid);
        UserPrincipal userPrincipal = UserPrincipal.create(userWithCreator.user(), userWithCreator.creator());
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
            userPrincipal,
            userWithCreator.user().getUsername(),
            userWithCreator.user().getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
    }
    `
    `
    `
}

하지만 계속 멀티 모듈 작업을 하다보니 core 모듈 내부에 authentication 도메인에서
Spring Security 인가 처리를 하는 로직이 있게 됩니다.
qc-api에서도 Spring Security를 의존하고 있는데 core 모듈에서도 Spring Security를 의존하고 있는 상황입니다.
사실 이 부분은 곧 수정할 예정인데요.

qc-core 모듈에서는 로그인 시 아이디,비밀번호가 일치하는지 판단하는 책임만 주고
인가 처리를 하는 것은 qc-api 모듈에서 직접 인가처리를 처리하는 로직이 있으면 될 것 같다고 판단하였습니다.

즉 qc-core 모듈에 존재하는 authentication 로직을 qc-api에서 처리하도록하면
qc-core모듈에서는 Spring Security에 관련한 책임이 사라지게 때문에 의존성을 제거할 수 있게 됩니다.

implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
compileOnly("org.springframework:spring-tx")

추가로 메일 인증을 위한 spring boot mail, 외부 api 요청을 위한 spring boot webflux
의존성은 다른 모듈에서 추가되어 있지 않기 때문에 일단 냅두자고 판단하였습니다.

하지만 트랜젝션 관련 의존성은 Storage 모듈에 포함되어 있을텐데요. 이 부분은 고민해봐야겠습니다.
비즈니스 로직을 처리하는 core 모듈에서 해당 의존성을 제거한다면
트랜젝션을 사용하지 않거나 직접 트랜젝션을 구현해야하므로 트레이드 오프를 생각해보며 정해야겠습니다.

core 모듈의 의존성을 제거하려는 욕심은 내려놓기로 하고 로직 구현을 위해 필요한 의존성이라면
모듈 간 심하게 중복되지 않는 선에서 사용하는 것으로 선택하였습니다.

Storage 모듈

├── main
│   ├── java
│   │   └── com
│   │       └── eager
│   │           └── questioncloud
│   │               └── storage
│   │                   ├── authentication
│   │                   ├── config
│   │                   ├── coupon
│   │                   ├── creator
│   │                   ├── library
│   │                   ├── payment
│   │                   ├── point
│   │                   ├── question
│   │                   │   └── converter
│   │                   ├── subscribe
│   │                   ├── user
│   │                   └── verification
│   └── resources

storage 모듈은 위와 같은 구조로 되어있습니다.
config를 제외한 나머지 패키지는 각 도메인별 Entity, Repository 구현체, JpaRepository가 있습니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
    implementation("com.fasterxml.jackson.core:jackson-databind:2.18.0")

    compileOnly(project(":qc-core"))
}

def querydslDir = "src/main/generated"

sourceSets {
    main.java.srcDirs += [querydslDir]
}

tasks.withType(JavaCompile).configureEach {
    options.getGeneratedSourceOutputDirectory().set(file(querydslDir))
}

clean.doLast {
    file(querydslDir).deleteDir()
}

bootJar.enabled = false
jar.enabled = true

storage 모듈의 build.gradle 입니다.
DB 핸들링을 위한 의존성들이 추가되어 있습니다.
추가로 프로젝트에서 querydsl을 사용중이기 때문에 qclass 생성 설정이 포함되어있습니다.

├── config
│   ├── QuerydslConfig.java
│   └── RedisConfig.java

프로젝트에서 사용되는 DB는 MariaDB, Redis 입니다.
또한 JPA, Querydsl로 SQL을 처리하고 있기 때문에 Config에는 Querydsl, Redis Config가 있습니다.

├── creator
│   ├── CreatorEntity.java
│   ├── CreatorJpaRepository.java
│   ├── CreatorProfileEntity.java
│   └── CreatorRepositoryImpl.java

내부에는 위에서 설명했던 것 처럼 Entity, JpaRepository, Repository구현체가 존재합니다.

@Repository
@RequiredArgsConstructor
public class CreatorRepositoryImpl implements CreatorRepository {
    private final CreatorJpaRepository creatorJpaRepository;
    private final JPAQueryFactory jpaQueryFactory;
	`
    `
    `
    @Override
    public Boolean existsById(Long creatorId) {
        return creatorJpaRepository.existsById(creatorId);
    }
    
    `
    `
    `
}

위 코드는 CreatorRepository 구현체인데요. CreatorRepository는 어디에 있는지 궁금해 하실수도 있습니다.
사실 CreatorRepository는 qc-core 모듈 creator 도메인에 존재합니다.

├── dto
│   └── CreatorDto.java
├── implement
│   ├── CreatorAppender.java
│   ├── CreatorReader.java
│   └── CreatorUpdater.java
├── model
│   └── Creator.java
├── repository
│   └── CreatorRepository.java
├── service
│   ├── CreatorService.java
│   └── CreatorWorkSpaceService.java
└── vo
    └── CreatorProfile.java

위 패키지 구조는 qc-core의 creator 도메인 내부인데요.

public interface CreatorRepository {
    Boolean existsById(Long creatorId);

    CreatorInformation getCreatorInformation(Long creatorId);

    Creator save(Creator creator);
}

// -------------------------------------------------------------------------------- //

@Component
@RequiredArgsConstructor
public class CreatorAppender {
    private final CreatorRepository creatorRepository;

    public Creator append(Creator creator) {
        return creatorRepository.save(creator);
    }
}

위 코드처럼 qc-core 모듈 내부에서 Repository를 사용한다면 평소처럼 CreatorRepository를 주입받을 것입니다.

SpringBoot + Kotlin 멀티 모듈 구성 - 도메인 모듈 분리 #4 - 제미니의 개발실무

위에서 qc-api(Spring Server)는 Storage 모듈을 runtimeOnly로 주입한다고 했는데요.
따라서 실제 qc-api가 실행 된다면 storage 모듈이 주입되면서 CreatorRepositoryImpl이 Bean으로 등록되고
Spring boot가 qc-core에서 해당 관련 Bean 주입을 처리할 것입니다.
어찌보면 storage 모듈은 core 모듈에서 만든 Repository를 구현하는 구현체 역할로 볼 수도 있겠습니다.

위와 같이 설계하면 장점은 storage가 변경되어도 qc-api와 qc-core는 영향을 받지 않게 됩니다.

Properties 구성

기존 단일 프로젝트에선 하나의 Properties를 운영 상태에 따라 (local, dev, prod)로 분리하여 처리헀었는데요.
이제는 모듈 별로 properties를 구성하였습니다.

spring.config.activate.on-profile=dev
spring.application.name=questioncloud
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://question-cloud-db:3306/questioncloud?characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=******
spring.datasource.password=******
#---
spring.config.activate.on-profile=local
spring.application.name=questioncloud
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/questioncloud?characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=******
spring.datasource.password=******
#---
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.show-sql=true
logging.level.org.hibernate.type.descriptor.sql=DEBUG
logging.level.org.hibernate.SQL=DEBUG
spring.data.web.pageable.one-indexed-parameters=true
spring.datasource.hikari.connection-timeout=20000
spring.datasource.hikari.validation-timeout=20000
REDIS_HOST=localhost
REDIS_PORT=6379

// application-storage.properties

먼저 storage 모듈 내부에 있는 application-storage.properties입니다.
spring.config.activate.on-profile 키워드와 #--- 구분자를 통해 dev, local 환경에 다른 값을 할당해주었고
공통적인 값은 하단에 추가하였습니다.
storage 모듈은 DB 관련 properties만 있는 것을 알 수 있습니다.

KAKAO_API_KEY==*******************************************************
KAKAO_API_SECRET==*******************************************************
GOOGLE_CLIENT_ID==*******************************************************
GOOGLE_CLIENT_SECRET=*******************************************************
NAVER_CLIENT_ID==*******************************************************
NAVER_CLIENT_SECRET==*******************************************************
PORT_ONE_SECRET_KEY==*******************************************************
CLIENT_URL==*******************************************************
JWT_SECRET_KEY=*******************************************************
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username===*******************************************************
spring.mail.password===*******************************************************
spring.mail.properties.mail.smtp.debug=true
spring.mail.properties.mail.smtp.connectiontimeout=1000
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.auth=true

core 모듈 내부에 있는 application-core.properties입니다.
core 모듈에서 외부 API 요청 시 사용되는 값들이 있습니다.

spring.profiles.include=storage,core

마지막으로 api 모듈 내부에 있는 application.properties인데요
결국 properties 값은 Spring boot 내부에서 로드 되어 사용되는 것이기 때문에
Spring boot Server인 api 모듈에서 spring.profiles.include를 이용해
core, storage 모듈에 있는 properties를 로드합니다.

추가 설정

위에서 작성하지 않은 멀티 모듈 프로젝트 설정에 추가 내용입니다.
IDE에 따라 모듈을 만들 때 알아서 추가되는 부분일 수도 있습니다.

rootProject.name = 'questioncloud'
include 'qc-api'
include 'qc-storage'
include 'qc-core'

프로젝트 최상위 디렉토리에 있는 setting.gradle에 위와 같이 모듈을 추가합니다.

후기

1차적으로 기존 단일 모듈 프로젝트를 멀티 모듈 프로젝트로 전환했는데요.
도메인을 하나씩 각 모듈에 옮기는 과정에서 로직들이 서로 복잡한 관계로 의존하고 있어서
빨간줄 에러를 없애느라 엄청 애를 썼던 것 같습니다.
(하나의 도메인을 옮기면 해당 도메인이 다른 도메인을 의존하고 있었기에 같이 추가해주거나
임시로 주석처리를 해줘야 했습니다.)

처음에 시작은 새로운 경험, 그리고 추후에 추가 될 관리자 도메인 때문이였습니다만
멀티 모듈로 전환하는 과정도 굉장히 의미 있는 시도였지만
얽히고 얽힌 도메인 간의 구조 및 의존을 풀어냈던 과정이 굉장히 많은 걸 깨닫게 해준 것 같습니다.

1차 적으로 멀티 모듈화에 완료하였으니 이제는 도메인 간 관계를 다시 정하면서 각 도메인 간 복잡한 관계를
다시 풀어내고 정리하는 시간을 가져보려 합니다.

0개의 댓글