본문 바로가기

spring

단일 모듈 프로젝트 멀티 모듈 구성하기 -7

오늘 포스팅은 어제 실행이 가능하게끔 만들었으니 우선 api 부분도 main을 생성해 따로 구동이 가능하게끔 구성하겠다.

 

아래와 같이 API도 Main을 추가하고 패키지를 바꿔줘서 실행이 가능하게끔 했다.

package com.project;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = "com.project")
public class MainApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(MainApiApplication.class, args);
    }
}

 

실행 후 위와 같이 스웨거도 작동이 잘 되고 실행도 된다.

그렇다면 모듈로써 구성은 다 한 것이고 api와 web을 구분해서 모듈로써 각 시스템이 실행이 가능해야 한다. 

 

각 web과 api 모두 잘 작동하는 것을 확인할 수 있게 되었다.

 

이제 원래 단일 프로젝트 단일 모듈에서 실행되는 아키텍처와 멀티 모듈 아키텍처에서 실행되는 것을 그림으로 확인해보자.

위는 단일 모듈 아키텍처 구성했을 때 큰 범위로 나눠 본 것이다.

단일 프로젝트 진행할 때에도 최대한 다른 기능을 가진 것을 의존하지 않게 설계했기 때문에 나눌 때에는 별로 어려움이 없었다 크게 들어가는 기능도 없었기에 프로젝트를 멀티 모듈로 나눈다는 것 자체에는 어려움이 없었지만 의존관계에 대해서 공부하고 멀티 모듈로 나눌 때 여파에 대해서 신경이 쓰였다. api 측 mvc 패턴 클래스를 수정할 때엔 큰 사이드 이펙트도 없고 의존 관계를 가진 다른 클래스를 수정하는 일도 없었지만 web과 관련된 서비스를 수정할 때에는 api측 로직에 영향이 갔었다. api 측에 swagger가 필요하거나 web에 만 특정 기능이 필요하다 해도 기능을 추가하고 의존성을 추가하게 되면 같은 책임이 생기기에 멀티 모듈로 나누고 추상화 계층을 맞추어 주었다.

아래는 멀티 모듈로 나눈 후의 아키텍처 구성이다.

 

 

이전과 달리 명확한 기능을 가진 모듈로 구성을 하고 구조를 확실하게 해 유지보수와 확장성을 챙겼다.

이전 포스팅에서도 말했듯이 사실은 서비스도 분리하는 것이 맞다고 생각한다. 물론 어떤 것이 맞고 틀린 것은 없어서 자신의 프로젝트에 맞게 구성하면 된다. 필자 프로젝트는 서비스와 컨트롤러 가 크지 않기에 분리하지 않은 것이다. 만약 서비스를 분리한다면이라고 생각해 보았을 때 핵심 비즈니스 로직은 도메인 쪽에 위치할 것이고 유스케이스 로직은 서비스 모듈에 위치하게끔 구성할 것 같다.

이전에 서비스 로직에서 핵심 비즈니스 로직과 유스케이스 로직을 분리한다고 했는데 이때 고민이 또 됐다. 서비스 모듈을 따로 두지 않다 보니 핵심 비즈니스 로직은 도메인과 연관이 클 텐데 도메인과 연관이 크지 않고 서비스와 관련이 더 깊은 로직도 있었다. 이때 기준점을 두었다.

package com.project;

import com.project.subdomain.Address;
import com.project.subdomain.Tier;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Table(name = "users")
@NoArgsConstructor
public class User {
    @Id
    @GeneratedValue
    @Column(name = "users_id")
    private Long id;


    private String loginId;

    private String name;


    private Integer age;


    @Column(nullable = false, unique = true)
    private String email;

    private String password;

    @Embedded
    private Address address;

    @Enumerated(EnumType.STRING)
    private Tier tier;
    //누적 구매 금액
    private int accumulatedAmount;

    @OneToMany(mappedBy = "user")
    private List<Market> markets = new ArrayList<>();

    @OneToMany(mappedBy = "user")
    private List<Delivery> deliveries = new ArrayList<>();

    @OneToMany(mappedBy = "user")
    private List<Purchase> purchases = new ArrayList<>();



    public User(String loginId, String name, Integer age, String email, String password, Address address, Tier tier) {
        this.loginId = loginId;
        this.name = name;
        this.age = age;
        this.email = email;
        this.password = password;
        this.address = address;
        this.tier = tier;
    }

    public User(String loginId, String name, Integer age, String email, String password, Address address, Tier tier, int accumulatedAmount) {
        this.loginId = loginId;
        this.name = name;
        this.age = age;
        this.email = email;
        this.password = password;
        this.address = address;
        this.tier = tier;
        this.accumulatedAmount = accumulatedAmount;
    }

    /**
     * 구매 금액 누적
     */
    public int addAmount(int totalPrice) {
        return this.accumulatedAmount = accumulatedAmount + totalPrice;
    }

    /**
     * 사용자가 구매한 구매 가격 누적에 따라 tier가 변경하는 로직
     * @param accumulatedAmount 누적금액 파라미터
     */
    public void upgradeTier(int accumulatedAmount) {

        if (accumulatedAmount >= 2000000) {
            this.tier = Tier.GOLD;
        } else if (accumulatedAmount >= 1500000) {
            this.tier = Tier.SILVER;
        } else if (accumulatedAmount >= 1000000){
            this.tier = Tier.BRONZE;
        }
    }
}

 

위는 필자 도메인 중 하나이다. 이처럼 도메인과 강하게 결합되는 로직만 도메인 부분으로 구현하게 하고 

/**
 * 헤더 값
 */
private HttpHeaders getHeaders() {
    HttpHeaders httpHeaders = new HttpHeaders();
    String auth = "KakaoAK " + adminKey;

    httpHeaders.set("Authorization", auth);
    httpHeaders.set("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

    return httpHeaders;
}

 

이와 같이 서비스 로직에 필요하고 어느 도메인에 얽히지 않은 로직은 두기로 했다.

이로써 구성은 끝이 났고 각 모듈에 README 파일을 두고 각 모듈은 어떤 기준을 두고 있고 어떤 의존성을 가지는지 설명을 하는 README를 각각 설정하고 멀티 모듈 구성은 끝을 낼 것이다. 하지만 이로써 끝낼 것은 아니고 이전에 카프카에 대해서도 배웠었다. 그래서 카프카를 사용해보려고 한다. 예상 시나리오는 결제 시에 결제 내용을 프로듀서로 메시지를 발행하고 토픽에 저장하는 식으로 구성할 것이고 컨슈머는 간단히 하려고 한다. 어플이라면 푸시 알림을 보내겠지만 웹이다 보니 alert를 통해서 발행할 수 있지만 이미 구현해 놓은 기능 중 하나가 결제 후 결제 정보를 웹에서 확인할 수 있게 했기 때문이다. 카프카를 더 일찍 알았다면 카프카를 통해서 제공했을 것이다. 이외에도 카프카로 현재 프로젝트에 진행할 부분이 있다면 토픽을 2~3개 정도 두고 여러 기능을 해보도록 할 것이다.

 

마지막으로 README를 작성하기 앞서 필자 모듈 결과를 확인하고 각 모듈별로 어떤 기준으로 나누었고 어떤 의존을 가지는지 알아보고 끝을 맺도록 하겠다.

 

 

큰 상위 모듈을 먼저 설명하고 하위 모듈을 설명하는 식으로 구성하겠다.

전체적으로 적용할 모듈과 의존 구성을 확인하자.

//plugin은 미리 구성해 놓은 task 들의 모음이며, 특정 빌드 과정에 필요한 기본 정보를 포함 한다.
plugins {
    id 'java'// 테스트, 번들링 기능과 함께 Java 컴파일을 추가해주며, 다른 JVM 언어 플러그인의 기반이 됨
    id 'org.springframework.boot' version '3.2.3'// 실행가능한 jar 또는 war로 패키징하여 애플리케이션 실행이 가능하도록 하며, spring-boot-dependencies 기반의 의존성 관리를 사용함
    id 'io.spring.dependency-management' version '1.1.4'// 자동으로 spring-boot-dependencies bom을 끌어와서 버전 관리를 해줌
}


// 현재의 root 프로젝트와 앞으로 추가될 서브 모듈에 대한 설정
allprojects {
    sourceCompatibility = '21'
    targetCompatibility = '21'

    // 라이브러리들을 받아올 원격 저장소들을 설정함
    repositories {
        mavenCentral()
    }
}

// 루트 제외한 전체 서브 모듈에 해당되는 설정
// settings.gradle에 include된 전체 프로젝트에 대한 공통 사항을 명시함
// root 프로젝트까지 적용하고 싶다면 allprojects에서 사용해야 함
subprojects {

    // subprojects 블록 안에서는 plugins 블록을 사용할 수 없으므로 apply plugin을 사용해야 함
    apply plugin: 'java'
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'

    dependencies {

        implementation 'org.springframework.boot:spring-boot-starter-web'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'



        testCompileOnly 'org.projectlombok:lombok'
        testAnnotationProcessor 'org.projectlombok:lombok'
        annotationProcessor 'org.projectlombok:lombok'
        compileOnly 'org.projectlombok:lombok'


    }

    test {
        useJUnitPlatform()
    }
}

전체적으로 적용할 의존성으로 web을 받았지만 이 부분은 전역적으로 사용돼서 사용했지만 추후 더 정확한 모듈을 나누기 위해서는 이 web도 받지 않는 것이 좋을 수 있겠다고 생각했다. 설명이 필요할 것 같은 부분은 주석을 달아 놓았으니 확인하면 된다.

 

 

global-utils

이 모듈이 가지고 있는 의존성은 아래와 같다.

bootJar { enabled = false }
jar { enabled = true }


dependencies {

}

 

독립 모듈 계층으로 어느 기능에도 종속되어선 안된다. 순수한 자바 코드로 이루어진 변경이 가장 적은 모듈에 해당한다.

상수 정의나 Type, Util 등을 정의하고 모든 모듈에서 사용할 수 있는 것들을 정의했다.

├── global-utils
│   ├── build.gradle
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── com
│       │   │       └── project //공통으로 사용될 로직
│       │   └── resources
│       └── test
│           ├── java
│           └── resources

 

application

이 모듈은 사용자 요청과 응답에 가장 가까운 계층으로 독립적으로 실행이 가능한 애플리케이션 모듈과 요청과 응답에 맞는 규격의 객체를 제공하는 모듈도 가진다.

하위 설계한 모듈들로 독립적인 서비스를 제공한다. 

모듈 구성은 api, dto, web이 있다.

 

api

REST API 제공을 위한 별도의 서비스 모듈이다.

WEB과 같은 데이터베이스를 공유하고 Swagger를 통해서 명세해 http 메서드에 맞는 자원을 통한 서비스를 제공하는 모듈

│   ├── api
│   │   ├── build.gradle
│   │   └── src
│   │       ├── main
│   │       │   ├── java
│   │       │   │   └── com
│   │       │   │       └── project
│   │       │   │           ├── MainApiApplication.java
│   │       │   │           ├── aop
│   │       │   │           │   ├── LogTraceAspect.java
│   │       │   │           │   └── pointcut
│   │       │   │           │       └── Pointcuts.java
│   │       │   │           ├── apiresponse
│   │       │   │           │   └── CustomErrorResponse.java
│   │       │   │           ├── config
│   │       │   │           │   └── SwaggerConfig.java
│   │       │   │           ├── controller
│   │       │   │           │   ├── ItemApiController.java
│   │       │   │           │   ├── MarketApiController.java
│   │       │   │           │   ├── PurchaseApiController.java
│   │       │   │           │   └── UserApiController.java
│   │       │   │           ├── service
│   │       │   │           │   ├── ItemApiService.java
│   │       │   │           │   ├── MarketApiService.java
│   │       │   │           │   ├── PurchaseApiService.java
│   │       │   │           │   ├── UploadApiService.java
│   │       │   │           │   └── UserApiService.java
│   │       │   │           └── trace
│   │       │   │               ├── LogTrace.java
│   │       │   │               ├── ThreadLocalLogTrace.java
│   │       │   │               ├── TraceId.java
│   │       │   │               └── TraceStatus.java
│   │       │   └── resources
│   │       │       ├── application-dev.yml
│   │       │       ├── application-prod.yml
│   │       │       └── application.yml
│   │       └── test
│   │           ├── java
│   │           └── resources
// 실행가능한 jar로 생성하는 옵션, main이 없는 라이브러리에서는 false로 비활성화함
// 스프링 부트 2.0 이상이라면 bootRepackage.enabled를 사용해야 함
bootJar { enabled = true }

// 외부에서 의존하기 위한 jar로 생성하는 옵션, main이 없는 라이브러리에서는 true로 비활성화함
jar { enabled = false }

dependencies {
    implementation project(':global-utils')
    implementation project(':new-core:domain')
    implementation project(':new-core:infra')
    implementation project(':new-core:database:database-api')
    implementation project(':application:dto')


    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    runtimeOnly 'com.mysql:mysql-connector-j'
    implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    // actuator 추가
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    // prometheus 추가
    implementation 'io.micrometer:micrometer-registry-prometheus'
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
}

test {
    useJUnitPlatform()
}

 

위와 같은 의존성을 가지고 있다.

 

web

웹 애플리케이션 서비스를 제공하기 위한 모듈로 필자의 쇼핑몰을 제공하는 모듈이다. 요청과 응답에 따른 엔드포인트와 서비스 로직이 포함된다.

│   └── web
│       ├── build.gradle
│       └── src
│           ├── main
│           │   ├── java
│           │   │   └── com
│           │   │       └── project
│           │   │           ├── MainWebApplication.java
│           │   │           ├── aop
│           │   │           │   ├── LogTraceAspect.java
│           │   │           │   └── pointcut
│           │   │           │       └── Pointcuts.java
│           │   │           ├── controller
│           │   │           │   ├── HomeController.java
│           │   │           │   ├── ItemController.java
│           │   │           │   ├── KakaoPayController.java
│           │   │           │   ├── LogController.java
│           │   │           │   ├── MarketController.java
│           │   │           │   └── loginController.java
│           │   │           ├── exception
│           │   │           │   ├── FileStorageException.java
│           │   │           │   └── MyDbException.java
│           │   │           ├── pay
│           │   │           │   ├── KaKaoPayReadyV0.java
│           │   │           │   ├── KakaoPayApprovalV0.java
│           │   │           │   └── response
│           │   │           │       ├── AmountV0.java
│           │   │           │       └── CardV0.java
│           │   │           ├── service
│           │   │           │   ├── DeliveryService.java
│           │   │           │   ├── FileService.java
│           │   │           │   ├── ItemService.java
│           │   │           │   ├── KakaoService.java
│           │   │           │   ├── MarketService.java
│           │   │           │   ├── PurchaseService.java
│           │   │           │   └── UserService.java
│           │   │           └── trace
│           │   │               ├── LogTrace.java
│           │   │               ├── ThreadLocalLogTrace.java
│           │   │               ├── TraceId.java
│           │   │               └── TraceStatus.java
│           │   └── resources
│           │       ├── application-dev.yml
│           │       ├── application-prod.yml
│           │       ├── application.yml
│           │       ├── static
│           │       │   ├── css
│           │       │   │   ├── blog.css
│           │       │   │   ├── blog.rtl.css
│           │       │   │   ├── bootstrap-grid.css
│           │       │   │   ├── bootstrap-grid.css.map
│           │       │   │   ├── bootstrap-grid.min.css
│           │       │   │   ├── bootstrap-grid.min.css.map
│           │       │   │   ├── bootstrap-grid.rtl.css
│           │       │   │   ├── bootstrap-grid.rtl.css.map
│           │       │   │   ├── bootstrap-grid.rtl.min.css
│           │       │   │   ├── bootstrap-grid.rtl.min.css.map
│           │       │   │   ├── bootstrap-reboot.css
│           │       │   │   ├── bootstrap-reboot.css.map
│           │       │   │   ├── bootstrap-reboot.min.css
│           │       │   │   ├── bootstrap-reboot.min.css.map
│           │       │   │   ├── bootstrap-reboot.rtl.css
│           │       │   │   ├── bootstrap-reboot.rtl.css.map
│           │       │   │   ├── bootstrap-reboot.rtl.min.css
│           │       │   │   ├── bootstrap-reboot.rtl.min.css.map
│           │       │   │   ├── bootstrap-utilities.css
│           │       │   │   ├── bootstrap-utilities.css.map
│           │       │   │   ├── bootstrap-utilities.min.css
│           │       │   │   ├── bootstrap-utilities.min.css.map
│           │       │   │   ├── bootstrap-utilities.rtl.css
│           │       │   │   ├── bootstrap-utilities.rtl.css.map
│           │       │   │   ├── bootstrap-utilities.rtl.min.css
│           │       │   │   ├── bootstrap-utilities.rtl.min.css.map
│           │       │   │   ├── bootstrap.css
│           │       │   │   ├── bootstrap.css.map
│           │       │   │   ├── bootstrap.min.css
│           │       │   │   ├── bootstrap.min.css.map
│           │       │   │   ├── bootstrap.rtl.css
│           │       │   │   ├── bootstrap.rtl.css.map
│           │       │   │   ├── bootstrap.rtl.min.css
│           │       │   │   ├── bootstrap.rtl.min.css.map
│           │       │   │   ├── checkout.css
│           │       │   │   ├── cover.css
│           │       │   │   ├── kakao.png
│           │       │   │   └── product.css
│           │       │   ├── error
│           │       │   │   ├── 4xx.html
│           │       │   │   └── 5xx.html
│           │       │   └── js
│           │       │       ├── bootstrap.bundle.js
│           │       │       ├── bootstrap.bundle.js.map
│           │       │       ├── bootstrap.bundle.min.js
│           │       │       ├── bootstrap.bundle.min.js.map
│           │       │       ├── bootstrap.esm.js
│           │       │       ├── bootstrap.esm.js.map
│           │       │       ├── bootstrap.esm.min.js
│           │       │       ├── bootstrap.esm.min.js.map
│           │       │       ├── bootstrap.js
│           │       │       ├── bootstrap.js.map
│           │       │       ├── bootstrap.min.js
│           │       │       ├── bootstrap.min.js.map
│           │       │       ├── checkout.js
│           │       │       └── color-modes.js
│           │       └── templates
│           │           ├── admin
│           │           │   ├── adminPage.html
│           │           │   ├── modifyBook.html
│           │           │   ├── modifyClothes.html
│           │           │   ├── modifyElectronics.html
│           │           │   ├── modifyFood.html
│           │           │   ├── updateItemBook.html
│           │           │   ├── updateItemClothes.html
│           │           │   ├── updateItemElectronics.html
│           │           │   └── updateItemFood.html
│           │           ├── error
│           │           │   ├── cartError.html
│           │           │   ├── ioError.html
│           │           │   └── uploadFileError.html
│           │           ├── fragments
│           │           │   ├── bodyHeader.html
│           │           │   ├── footer.html
│           │           │   └── header.html
│           │           ├── home.html
│           │           ├── items
│           │           │   ├── createItemBook.html
│           │           │   ├── createItemClothes.html
│           │           │   ├── createItemElectronics.html
│           │           │   └── createItemFood.html
│           │           ├── list
│           │           │   ├── bookList.html
│           │           │   ├── clothesList.html
│           │           │   ├── electronicsList.html
│           │           │   └── foodList.html
│           │           ├── login
│           │           │   ├── login.html
│           │           │   └── sign-up.html
│           │           ├── loginHome.html
│           │           └── order
│           │               ├── buyItem.html
│           │               ├── cancel.html
│           │               ├── fail.html
│           │               ├── kakaopay.html
│           │               ├── purchase.html
│           │               └── purchaseV2.html
│           └── test
│               ├── java
│               │   └── com
│               │       └── test
│               │           └── TestApplication.java
│               └── resources

 

// 실행가능한 jar로 생성하는 옵션, main이 없는 라이브러리에서는 false로 비활성화함
// 스프링 부트 2.0 이상이라면 bootRepackage.enabled를 사용해야 함
bootJar { enabled = true }

// 외부에서 의존하기 위한 jar로 생성하는 옵션, main이 없는 라이브러리에서는 true로 비활성화함
jar { enabled = false }

dependencies {
    implementation project(':global-utils')
    implementation project(':new-core:domain')
    implementation project(':new-core:infra')
    implementation project(':new-core:interceptor')
    implementation project(':new-core:database:database-mysql')
    implementation project(':application:dto')


    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    runtimeOnly 'com.mysql:mysql-connector-j'
    implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    // actuator 추가
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    // prometheus 추가
    implementation 'io.micrometer:micrometer-registry-prometheus'
}

test {
    useJUnitPlatform()
}

 

dto

web과 api에서 공동으로 사용하는 도메인에 대해서 응답과 요청을 주고받는 계층에서 사용자가 필요한 정보에 따라 실제 엔티티는 숨기고 변환한 데이터를 담을 dto와 form 데이터를 담을 객체를 가진 모듈

│   ├── dto
│   │   ├── build.gradle
│   │   └── src
│   │       ├── main
│   │       │   ├── java
│   │       │   │   └── com
│   │       │   │       └── project
│   │       │   │           ├── apidto
│   │       │   │           │   ├── BookApiDto.java
│   │       │   │           │   ├── ClothesApiDto.java
│   │       │   │           │   ├── ElectronicsApiDto.java
│   │       │   │           │   ├── FoodApiDto.java
│   │       │   │           │   ├── ItemApiDto.java
│   │       │   │           │   ├── ItemCond.java
│   │       │   │           │   ├── ItemSearchDto.java
│   │       │   │           │   ├── UserDto.java
│   │       │   │           │   ├── UserLoginIdPwDto.java
│   │       │   │           │   ├── UserPurchaseDto.java
│   │       │   │           │   ├── save
│   │       │   │           │   │   ├── BookSaveApiDto.java
│   │       │   │           │   │   ├── ClothesSaveApiDto.java
│   │       │   │           │   │   ├── ElectronicsSaveApiDto.java
│   │       │   │           │   │   ├── FoodSaveApiDto.java
│   │       │   │           │   │   └── UserSaveDto.java
│   │       │   │           │   └── update
│   │       │   │           │       ├── UpdateBookDto.java
│   │       │   │           │       ├── UpdateClothesDto.java
│   │       │   │           │       ├── UpdateElectronicsDto.java
│   │       │   │           │       ├── UpdateFoodDto.java
│   │       │   │           │       └── UpdateItemDto.java
│   │       │   │           ├── dto
│   │       │   │           │   ├── BookAndFileDto.java
│   │       │   │           │   ├── ClothesAndFileDto.java
│   │       │   │           │   ├── ElectronicsAndFileDto.java
│   │       │   │           │   ├── FoodAndFileDto.java
│   │       │   │           │   ├── ItemDto.java
│   │       │   │           │   ├── MarketPayDto.java
│   │       │   │           │   ├── MarketPayDtoV2.java
│   │       │   │           │   └── PurchasePayDto.java
│   │       │   │           └── form
│   │       │   │               ├── LoginForm.java
│   │       │   │               ├── UserForm.java
│   │       │   │               └── itemform
│   │       │   │                   ├── BookForm.java
│   │       │   │                   ├── ClothesForm.java
│   │       │   │                   ├── ElectronicsForm.java
│   │       │   │                   └── FoodForm.java
│   │       │   └── resources
│   │       └── test
│   │           ├── java
│   │           └── resources
bootJar { enabled = false }
jar { enabled = true }


dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation project(':new-core:domain')
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
}

도메인과 연관이 있기 때문에 도메인 의존성을 가지고 있으며 form 데이터에 대해 검증이 필요하므로 validation과 api에서 swagger를 사용할 때 명세에 dto가 필요하기 때문에 swagger 의존성을 가졌다.

 

new-core

가장 많은 모듈을 가지고 있는 모듈로 하나의 모듈이 하나의 기능을 할 수 있게끔 구성했다.

이 모듈이 가지고 있는 모듈은 각각

database

domain

infra

interceptor

이렇게 구성되어 있다. 각 모듈에 대해서도 알아보자.

 

database

기존 database를 따로 가지는 멀티 모듈을 구성하는 사람들은 많지 않다.

필자는 프로젝트에서 여러 데이터베이스 기술을 사용하게 될 때 리포지토리 계층을 각 데이터베이스마다 다르게 가져가면 좋을 것 같다고 생각했기 때문에 별도의 리포지토리를 정의하고 각 batabase 하위 모듈은 서비스 모듈에 맞는 별도의 리포지토리 모듈을 가지게 했다.

서비스 비즈니스를 모른다.

 

database-mysql

mysql을 사용하는 데이터베이스 모듈로 리포지토리 계층을 담당한다. CRUD를 제외한 별도의 쿼리 로직도 담겨 있다.

데이터베이스 관련 기술을 제외한 기술을 의존해선 안된다.

│   │   ├── database-mysql
│   │   │   ├── build.gradle
│   │   │   └── src
│   │   │       ├── main
│   │   │       │   ├── java
│   │   │       │   │   └── com
│   │   │       │   │       └── project
│   │   │       │   │           ├── custom
│   │   │       │   │           │   ├── DeliveryRepositoryCustom.java
│   │   │       │   │           │   ├── FileRepositoryCustom.java
│   │   │       │   │           │   ├── ItemRepositoryCustom.java
│   │   │       │   │           │   └── MarketRepositoryCustom.java
│   │   │       │   │           ├── impl
│   │   │       │   │           │   ├── DeliveryRepositoryImpl.java
│   │   │       │   │           │   ├── FileRepositoryImpl.java
│   │   │       │   │           │   ├── ItemRepositoryImpl.java
│   │   │       │   │           │   └── MarketRepositoryImpl.java
│   │   │       │   │           └── repository
│   │   │       │   │               ├── DeliveryRepository.java
│   │   │       │   │               ├── FileRepository.java
│   │   │       │   │               ├── ItemRepository.java
│   │   │       │   │               ├── MarketRepository.java
│   │   │       │   │               ├── PurchaseRepository.java
│   │   │       │   │               └── UserRepository.java
│   │   │       │   └── resources
│   │   │       └── test
│   │   │           ├── java
│   │   │           └── resources
│   │   └── src
│   │       ├── main
│   │       │   ├── java
│   │       │   │   └── com
│   │       │   └── resources
│   │       └── test
│   │           ├── java
│   │           └── resources

 

dependencies {
    implementation project(':new-core:domain')
    implementation project(':application:dto')
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'com.mysql:mysql-connector-j'
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api' // Ensure this is included
}

 

ORM 데이터베이스인 Mysql을 사용하기 때문에 JPA를 사용하며 쿼리는 QueryDsl을 사용했으므로 의존을 갖는다.

 

database-api

별도의 데이터베이스 기술을 사용하지는 않고 web과 같은 mysql을 사용하지만 서비스를 나눈 상황에서 별도의 아키텍처를 갖기 위해 별도의 데이터베이스 모듈을 가진다.

규칙은 mysql 모듈과 같은 규칙을 가진다.

│   │   ├── database-api
│   │   │   ├── build.gradle
│   │   │   └── src
│   │   │       ├── main
│   │   │       │   ├── java
│   │   │       │   │   └── com
│   │   │       │   │       └── project
│   │   │       │   │           ├── custom
│   │   │       │   │           │   ├── ItemApiRepositoryCustom.java
│   │   │       │   │           │   ├── MarketApiRepositoryCustom.java
│   │   │       │   │           │   ├── PurchaseApiRepositoryCustom.java
│   │   │       │   │           │   ├── UploadApiRepositoryCustom.java
│   │   │       │   │           │   └── UserApiRepositoryCustom.java
│   │   │       │   │           ├── impl
│   │   │       │   │           │   ├── ItemApiRepositoryImpl.java
│   │   │       │   │           │   ├── MarketApiRepositoryImpl.java
│   │   │       │   │           │   ├── PurchaseApiRepositoryImpl.java
│   │   │       │   │           │   ├── UploadApiRepositoryImpl.java
│   │   │       │   │           │   └── UserApiRepositoryImpl.java
│   │   │       │   │           └── repository
│   │   │       │   │               ├── ItemApiRepository.java
│   │   │       │   │               ├── MarketApiRepository.java
│   │   │       │   │               ├── PurchaseApiRepository.java
│   │   │       │   │               ├── UploadApiRepository.java
│   │   │       │   │               └── UserApiRepository.java
│   │   │       │   └── resources
│   │   │       │       
│   │   │       └── test
│   │   │           ├── java
│   │   │           └── resources

 

bootJar { enabled = false }
jar { enabled = true }

dependencies {
    implementation project(':new-core:domain')
    implementation project(':application:dto')
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'com.mysql:mysql-connector-j'
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api' // Ensure this is included
}

test {
    useJUnitPlatform()
}

 

domain

Java Class로 표현된 도메인 Class이다. 특정 모듈을 의존하지 않으며 Entity를 구성하기 위한 jpa와 QueryDsl을 사용하기 위해 Qfile이 필요하기 때문에 jpa와 querydsl의 의존성을 가진다.

이외 별도의 의존을 가지지 않는 것이 규칙이다.

│   ├── domain
│   │   ├── build.gradle
│   │   └── src
│   │       ├── main
│   │       │   ├── java
│   │       │   │   └── com
│   │       │   │       └── project
│   │       │   │           ├── Delivery.java
│   │       │   │           ├── Market.java
│   │       │   │           ├── Purchase.java
│   │       │   │           ├── UploadFile.java
│   │       │   │           ├── User.java
│   │       │   │           ├── item
│   │       │   │           │   ├── Book.java
│   │       │   │           │   ├── Clothes.java
│   │       │   │           │   ├── ClothesType.java
│   │       │   │           │   ├── Electronics.java
│   │       │   │           │   ├── Food.java
│   │       │   │           │   └── Item.java
│   │       │   │           └── subdomain
│   │       │   │               ├── Address.java
│   │       │   │               ├── DeliveryStatus.java
│   │       │   │               └── Tier.java
│   │       │   └── resources
│   │       └── test
│   │           ├── java
│   │           └── resources
// 실행가능한 jar로 생성하는 옵션, main이 없는 라이브러리에서는 false로 비활성화함
// 스프링 부트 2.0 이상이라면 bootRepackage.enabled를 사용해야 함
bootJar { enabled = false }

// 외부에서 의존하기 위한 jar로 생성하는 옵션, main이 없는 라이브러리에서는 true로 비활성화함
jar { enabled = true }

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    compileOnly 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
}

 

infra

설정과 외부 설정 파일을 담당하는 모듈이다. 이 모듈에서는 원하는 기능의 의존성을 추가하고 사용할 수 있게끔 한다.

만약 필요한 설정이 각기 다르다면 infra의 하위 모듈을 구성해서 지원하는 방식으로 지원하는 것을 규칙으로 정했다.

해당 기술에 대한 config와 해당 기술에 대한 의존만 가져야 한다.

│   ├── infra
│   │   ├── build.gradle
│   │   └── src
│   │       ├── main
│   │       │   ├── java
│   │       │   │   └── com
│   │       │   │       └── project
│   │       │   │           └── config
│   │       │   │               └── MetricConfig.java
│   │       │   └── resources
│   │       │       ├── application-actuator.yml
│   │       │       ├── application-file.yml
│   │       │       ├── application-logging.yml
│   │       │       ├── application-mysql.yml
│   │       │       ├── application-pay.yml
│   │       │       └── application-swagger.yml
│   │       └── test
│   │           ├── java
│   │           └── resources

 

bootJar { enabled = false }
jar { enabled = true }


dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
}

필자는 많은 기술을 사용하지 않기에 해당 모듈에서는 actuator의 필자가 설정한 특정 매트릭에 대한 설정만 가지고 있다.

 

interceptor

보안에 해당하는 부분이다. 별도의 시큐리티를 사용하지 않고 접근 제한을 인터셉터로 적용했다. 물론 보안적으로 부족한 것을 알지만 프로젝트 진행당시 직접 보안 요소를 넣어보고 싶어서 구성했다. 차후 필자 깃허브 리포지토리 브랜치에는 스프링 시큐리티를 적용한 코드도 있다. 물론 바꿀 수 있지만 허점이 커진다. 이미 도메인으로 만든 USER 도메인이나 mvc 패턴 사이사이 바꿔야 할 것이 너무 많기에 사이드 이펙트도 많이 생기고 인터셉터로 진행하겠지만 차후 프로젝트 진행 시에는 필수로 시큐리티와 JWT를 사용한 보안을 적용하도록 하겠다.

이 모듈의 규칙은 접근 제한을 담당하는 모듈로 웹과 관련된 설정과 domain, global-utils만 가지고 있다.

접근 제한을 늘리는 것은 허용하나 다른 기술의 의존하는 것은 허용하지 않는다.

│   ├── interceptor
│   │   ├── build.gradle
│   │   └── src
│   │       ├── main
│   │       │   ├── java
│   │       │   │   └── com
│   │       │   │       └── project
│   │       │   │           ├── annotation
│   │       │   │           │   └── Login.java
│   │       │   │           ├── argumentResolver
│   │       │   │           │   └── LoginUserArgumentResolver.java
│   │       │   │           ├── config
│   │       │   │           │   └── WebConfig.java
│   │       │   │           └── interceptor
│   │       │   │               ├── AdminInterceptor.java
│   │       │   │               ├── LogInterceptor.java
│   │       │   │               └── LoginCheckInterceptor.java
│   │       │   └── resources
│   │       └── test
│   │           ├── java
│   │           └── resources
│   └── src
│       ├── main
│       │   ├── java
│       │   └── resources
│       └── test
│           ├── java
│           └── resources

 


bootJar { enabled = false }
jar { enabled = true }


dependencies {
    implementation project(':new-core:domain')
    implementation project(':global-utils')
}

 

여기까지 모든 모듈에 대해서도 알아보았다. 위 설명에 대한 내용은 각 모듈의 README에도 들어갈 예정이다.

 

다 구성하고 보니 아쉬운 부분이 많은 프로젝트이다. 개인 프로젝트에 기한을 설정하고 한 부분이라 원하는 부분을 다 넣지 못하고 기능적으로 만족하지 못하는 부분도 있지만 WEB 애플리케이션과 REST API를 제공하는 데 있어 스프링 MVC의 기술적으로 부족함은 없을 것이다.

이 프로젝트 이후로 스프링 배치, 레디스, 카프카, 캐시에 대해서 알아보았어서 추가로 더하고 싶은 부분은 현재 프로젝트에 생각을 해보았을 때 카프카가 가장 적절해 보였다. 캐시 부분도 생각을 해보았지만 캐시를 사용하기에 적절한 부분이 딱히 없었다. 쇼핑몰 프로젝트이고 카테고리 별로 아이템을 띄울 때 수량을 같이 띄우다 보니 캐시를 사용하는 부분에 있어 큰 이점이 없었다. 상세 페이지로 아이템에 대한 정보를 상세하게 보여주는 부분과 구매 시에 수량을 나타내는 부분으로 나눈다면 의미가 있을 것이라 생각한다. 이처럼 아쉬운 부분이 많지만 처음 프로젝트를 시작할 때 유스케이스를 작성하고 명세할 때 기간 기준으로 맞춰 제작한 프로젝트이므로 추후 어떤 프로젝트를 해도 사용할 수 있는 범위가 넓어졌으므로 다양한 시선에서, 다양한 기술로 기능을 구현할 수 있을 것 같다.

 

다음 포스팅은 카프카를 사용할 것이다. infra 모듈에 설정을 하고 프로듀서와 컨슈머를 생성해서 web 모듈에 의존성을 주고 로직에서 구독하고 메시지를 발행 및 소비를 하도록 구성하는 포스팅으로 다시 오도록 하겠다.