3.1 프로젝트 설정과 구조화

3.1 프로젝트 설정과 구조화

프로젝트를 시작할 때 가장 중요한 것은 확장 가능하고 유지보수하기 좋은 구조를 만드는 것입니다. 이 장에서는 도메인 주도 설계와 클린 아키텍처 원칙을 따르는 도서관 시스템의 프로젝트 구조를 설정하는 방법을 살펴보겠습니다.

3.1.1 자바 빌드 도구의 이해

자바 생태계에서 빌드 도구는 시간에 따라 크게 발전해왔습니다. 각 도구들은 이전 도구의 한계를 극복하면서 새로운 기능과 편의성을 제공해왔습니다.

Ant (2000년)

Ant는 자바 생태계의 첫 번째 주요 빌드 도구입니다. Make를 모델로 하여 만들어졌으며, XML을 사용하여 빌드 프로세스를 정의합니다. Ant의 주요 특징은 다음과 같습니다:

  • 유연한 빌드 프로세스 정의 가능
  • XML 기반의 명령적 빌드 스크립트
  • 의존성 관리 기능 부재
  • 빌드 작업의 명시적 정의 필요

예를 들어, Ant의 빌드 파일은 다음과 같은 형태를 가집니다:

<project name="MyProject" default="dist" basedir=".">
    <target name="compile">
        <javac srcdir="${src.dir}" destdir="${build.dir}"/>
    </target>
    <target name="dist" depends="compile">
        <jar destfile="${dist.dir}/MyProject.jar" basedir="${build.dir}"/>
    </target>
</project>

Maven (2004년)

Maven은 “규약이 설정보다 낫다”(Convention over Configuration)는 철학을 도입했습니다. 표준화된 프로젝트 구조와 빌드 라이프사이클을 제공하여, 개발자들이 복잡한 빌드 스크립트를 작성할 필요성을 줄였습니다. Maven의 주요 특징은 다음과 같습니다:

  • 선언적 의존성 관리
  • 표준화된 프로젝트 구조
  • 중앙 저장소를 통한 라이브러리 관리
  • XML 기반의 프로젝트 정의

Maven의 pom.xml은 다음과 같은 구조를 가집니다:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>my-project</artifactId>
    <version>1.0.0</version>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>3.2.0</version>
        </dependency>
    </dependencies>
</project>

Gradle (2012년)

Gradle은 Ant의 유연성과 Maven의 규약 기반 접근방식의 장점을 모두 취하면서, Groovy나 Kotlin DSL을 사용하여 더 표현력 있는 빌드 스크립트를 작성할 수 있게 해줍니다. Gradle의 주요 특징은 다음과 같습니다:

  • 선언적이면서도 프로그래밍 가능한 빌드 스크립트
  • 증분 빌드를 통한 뛰어난 성능
  • 정교한 의존성 관리
  • 멀티 프로젝트 빌드에 최적화
  • 빌드 캐시를 통한 빌드 시간 단축

이러한 특징들 때문에 현대적인 자바 프로젝트, 특히 대규모 멀티 모듈 프로젝트에서는 Gradle이 선호되고 있습니다.

3.1.2 빌드 도구로서의 Gradle

우리는 이 프로젝트의 빌드 도구로 Gradle을 선택했습니다. Maven 대신 Gradle을 선택한 구체적인 이유는 다음과 같습니다:

  1. 유연한 빌드 스크립트 Groovy나 Kotlin DSL을 사용하여 명확하고 유연한 빌드 스크립트를 작성할 수 있습니다. XML 기반의 Maven보다 가독성이 좋고 유지보수가 쉽습니다. 예를 들어, 조건부 의존성 설정이나 동적인 태스크 생성 같은 복잡한 빌드 로직을 쉽게 구현할 수 있습니다.

  2. 빌드 성능 Gradle은 다음과 같은 최적화를 통해 Maven보다 훨씬 빠른 빌드 성능을 제공합니다:

    • 증분 빌드: 변경된 부분만 다시 빌드
    • 빌드 캐시: 이전 빌드 결과를 재사용
    • 병렬 실행: 독립적인 태스크를 동시에 실행
    • 구성 캐싱: 빌드 스크립트 평가 결과를 캐시
  3. 의존성 관리 Version Catalog를 통한 중앙화된 의존성 관리가 가능하며, 이는 다음과 같은 이점을 제공합니다:

    • 버전 충돌 방지
    • 일관된 버전 관리
    • IDE 지원을 통한 생산성 향상
    • 의존성 그룹화를 통한 관리 용이성
  4. 멀티 프로젝트 지원 복잡한 멀티 모듈 프로젝트를 효과적으로 관리할 수 있는 기능을 제공합니다:

    • 모듈 간 의존성 관리
    • 공통 설정의 중앙화
    • 선택적 구성 적용
    • 모듈별 빌드 최적화

3.1.2 Version Catalog를 이용한 의존성 관리

Version Catalog는 Gradle 7.4부터 도입된 기능으로, 프로젝트의 의존성 버전을 중앙에서 관리할 수 있게 해줍니다. gradle/libs.versions.toml 파일을 생성하여 다음과 같이 설정합니다:

[versions]
spring-boot = "3.2.0"
spring-cloud = "2023.0.0"
jackson = "2.15.3"
junit-jupiter = "5.10.1"
assertj = "3.24.2"
mockito = "5.7.0"
hibernate = "6.3.1.Final"
jakarta-validation = "3.0.2"
lombok = "1.18.30"
mapstruct = "1.5.5.Final"

[libraries]
spring-boot-dependencies = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot" }
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" }
spring-boot-starter-data-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa" }
spring-boot-starter-validation = { module = "org.springframework.boot:spring-boot-starter-validation" }
spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test" }

jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
jakarta-validation = { module = "jakarta.validation:jakarta.validation-api", version.ref = "jakarta-validation" }

lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" }
mapstruct = { module = "org.mapstruct:mapstruct", version.ref = "mapstruct" }
mapstruct-processor = { module = "org.mapstruct:mapstruct-processor", version.ref = "mapstruct" }

junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }

[bundles]
testing = ["junit-jupiter", "assertj-core", "mockito-core"]
mapping = ["mapstruct", "mapstruct-processor"]

[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "spring-boot" }

3.1.3 멀티 모듈 구조

우리의 도서관 시스템은 클린 아키텍처의 원칙을 따르기 위해 다음과 같은 모듈로 분리됩니다:

  1. library-core:

    • 도메인과 애플리케이션 계층을 포함하는 핵심 모듈
    • 모든 비즈니스 로직과 도메인 규칙이 이곳에 위치
    • 외부 시스템이나 프레임워크에 대한 의존성이 없음
    • 포트(인터페이스)를 통해 외부와 통신
  2. library-infrastructure-fake:

    • 테스트를 위한 인프라스트럭처 구현체
    • 인메모리 저장소 구현
    • 테스트 더블(Mock, Stub) 제공
    • 통합 테스트 지원을 위한 설정
  3. library-infrastructure-jpa:

    • JPA 기반의 실제 인프라스트럭처 구현체
    • 영속성 어댑터 구현
    • 엔티티 매핑 설정
    • JPA 관련 설정 및 최적화
  4. library-web-api:

    • REST API 엔드포인트 제공
    • 웹 계층 구성 (컨트롤러, DTO, 예외 처리 등)
    • API 문서화
    • 보안 설정

먼저 루트 프로젝트의 settings.gradle 파일을 설정합니다:

rootProject.name = 'library'

dependencyResolutionManagement {
    versionCatalogs {
        libs {
            from(files("gradle/libs.versions.toml"))
        }
    }
}

include 'library-core'
include 'library-infrastructure-fake'
include 'library-infrastructure-jpa'
include 'library-web-api'

루트 프로젝트의 build.gradle:

plugins {
    alias(libs.plugins.spring.boot) apply false
    alias(libs.plugins.spring.dependency.management) apply false
}

subprojects {
    group = 'net.badnom.library'
    version = '0.0.1-SNAPSHOT'

    apply plugin: 'java'
    apply plugin: 'io.spring.dependency-management'

    java {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        annotationProcessor libs.lombok
        compileOnly libs.lombok
        
        testImplementation libs.bundles.testing
    }

    test {
        useJUnitPlatform()
    }

    tasks.withType(JavaCompile) {
        options.encoding = 'UTF-8'
        options.compilerArgs += [
            '-Xlint:unchecked',
            '-Xlint:deprecation',
            '-parameters'
        ]
    }
}

각 모듈별 build.gradle 설정:

library-core/build.gradle:

dependencies {
    api libs.spring.boot.starter.validation
    api libs.jakarta.validation
    implementation libs.jackson.databind
    implementation libs.bundles.mapping
}

library-infrastructure-jpa/build.gradle:

dependencies {
    implementation project(':library-core')
    implementation libs.spring.boot.starter.data.jpa
    
    testImplementation project(':library-infrastructure-fake')
}

library-infrastructure-fake/build.gradle:

dependencies {
    implementation project(':library-core')
    implementation libs.spring.boot.starter.test
}

library-application/build.gradle:

apply plugin: libs.plugins.spring.boot

dependencies {
    implementation project(':library-core')
    implementation project(':library-infrastructure-jpa')
    implementation libs.spring.boot.starter.web
}

3.1.3 패키지 구조

우리의 도서관 시스템은 바운디드 컨텍스트를 기준으로 다음과 같이 패키지를 구성합니다:

net.badnom.library
├── shared               # 공통 컴포넌트와 shared kernel
│   ├── domain          # 공통 도메인 컴포넌트
│   │   ├── model       # Money, Email 등 공통 값 객체
│   │   └── event       # 기본 도메인 이벤트 클래스
│   └── infrastructure  # 공통 인프라스트럭처 컴포넌트
├── user                # 사용자 관리 컨텍스트
│   ├── domain
│   │   ├── member     # Member 애그리거트
│   │   │   └── event
│   │   └── staff      # Staff 애그리거트
│   │       └── event
│   └── application
│       ├── command    # 커맨드와 핸들러
│       └── policy     # 정책 처리
├── book                # 도서 메타데이터 컨텍스트
│   ├── domain
│   │   ├── book       # Book 애그리거트
│   │   │   └── event
│   │   └── category   # Category 애그리거트
│   │       └── event
│   └── application
│       ├── command
│       └── policy
└── circulation         # 도서 대출 컨텍스트
    ├── domain
    │   ├── checkout   # Checkout 애그리거트
    │   │   └── event
    │   └── reservation# Reservation 애그리거트
    │       └── event
    └── application
        ├── command
        └── policy

이러한 패키지 구조는 다음과 같은 이점을 제공합니다:

  1. 바운디드 컨텍스트별 명확한 책임 분리
  2. 도메인 모델과 애플리케이션 로직의 분리
  3. 애그리거트 단위의 응집도 있는 구조
  4. 도메인 이벤트의 체계적인 관리

3.1.4 모듈 간 의존성 규칙

클린 아키텍처의 원칙을 지키기 위해 다음과 같은 의존성 규칙을 준수합니다:

  1. 의존성 방향:

    • 모든 의존성은 안쪽(core)을 향해야 함
    • 외부 계층은 내부 계층을 알 수 있지만, 내부 계층은 외부 계층을 알 수 없음
    • 의존성 주입을 통해 런타임에 구현체 결정
  2. library-core 모듈의 독립성:

    • 외부 프레임워크나 라이브러리에 대한 직접적인 의존성 배제
    • 도메인 모델의 순수성 유지
    • 포트를 통한 느슨한 결합 구현
  3. 인프라스트럭처 모듈의 책임:

    • library-core에 정의된 포트의 구현체 제공
    • 기술적인 문제 해결에 집중
    • 도메인 로직 포함 금지
  4. web-api 모듈의 통합점:

    • 필요한 구현체들의 조합과 설정
    • 최종 실행 가능한 애플리케이션 구성
    • API 버전 관리 및 문서화

3.1.5 도메인 이벤트 구조

각 애그리거트의 도메인 이벤트는 다음과 같은 구조를 따릅니다:

// shared/domain/event/DomainEvent.java
public interface DomainEvent {
    Instant getOccurredOn();
    String getEventType();
}

// circulation/domain/checkout/event/BookCheckedOutEvent.java
public class BookCheckedOutEvent implements DomainEvent {
    private final BookId bookId;
    private final MemberId memberId;
    private final Instant occurredOn;
    
    // ... 구현 내용
}

3.1.6 애플리케이션 계층 구조

커맨드와 핸들러는 다음과 같은 구조를 따릅니다:

// circulation/application/command/CheckoutBookCommand.java
public record CheckoutBookCommand(
    BookId bookId,
    MemberId memberId
) implements Command {}

// circulation/application/command/CheckoutBookCommandHandler.java
public class CheckoutBookCommandHandler implements CommandHandler<CheckoutBookCommand> {
    private final BookRepository bookRepository;
    private final MemberRepository memberRepository;
    
    @Override
    public CommandResult handle(CheckoutBookCommand command) {
        // ... 구현 내용
    }
}

이러한 프로젝트 구조는 도메인 주도 설계와 클린 아키텍처의 원칙을 실천하면서도, 실용적인 개발이 가능한 기반을 제공합니다.

Last updated on