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을 선택한 구체적인 이유는 다음과 같습니다:
-
유연한 빌드 스크립트 Groovy나 Kotlin DSL을 사용하여 명확하고 유연한 빌드 스크립트를 작성할 수 있습니다. XML 기반의 Maven보다 가독성이 좋고 유지보수가 쉽습니다. 예를 들어, 조건부 의존성 설정이나 동적인 태스크 생성 같은 복잡한 빌드 로직을 쉽게 구현할 수 있습니다.
-
빌드 성능 Gradle은 다음과 같은 최적화를 통해 Maven보다 훨씬 빠른 빌드 성능을 제공합니다:
- 증분 빌드: 변경된 부분만 다시 빌드
- 빌드 캐시: 이전 빌드 결과를 재사용
- 병렬 실행: 독립적인 태스크를 동시에 실행
- 구성 캐싱: 빌드 스크립트 평가 결과를 캐시
-
의존성 관리 Version Catalog를 통한 중앙화된 의존성 관리가 가능하며, 이는 다음과 같은 이점을 제공합니다:
- 버전 충돌 방지
- 일관된 버전 관리
- IDE 지원을 통한 생산성 향상
- 의존성 그룹화를 통한 관리 용이성
-
멀티 프로젝트 지원 복잡한 멀티 모듈 프로젝트를 효과적으로 관리할 수 있는 기능을 제공합니다:
- 모듈 간 의존성 관리
- 공통 설정의 중앙화
- 선택적 구성 적용
- 모듈별 빌드 최적화
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 멀티 모듈 구조
우리의 도서관 시스템은 클린 아키텍처의 원칙을 따르기 위해 다음과 같은 모듈로 분리됩니다:
-
library-core:
- 도메인과 애플리케이션 계층을 포함하는 핵심 모듈
- 모든 비즈니스 로직과 도메인 규칙이 이곳에 위치
- 외부 시스템이나 프레임워크에 대한 의존성이 없음
- 포트(인터페이스)를 통해 외부와 통신
-
library-infrastructure-fake:
- 테스트를 위한 인프라스트럭처 구현체
- 인메모리 저장소 구현
- 테스트 더블(Mock, Stub) 제공
- 통합 테스트 지원을 위한 설정
-
library-infrastructure-jpa:
- JPA 기반의 실제 인프라스트럭처 구현체
- 영속성 어댑터 구현
- 엔티티 매핑 설정
- JPA 관련 설정 및 최적화
-
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이러한 패키지 구조는 다음과 같은 이점을 제공합니다:
- 바운디드 컨텍스트별 명확한 책임 분리
- 도메인 모델과 애플리케이션 로직의 분리
- 애그리거트 단위의 응집도 있는 구조
- 도메인 이벤트의 체계적인 관리
3.1.4 모듈 간 의존성 규칙
클린 아키텍처의 원칙을 지키기 위해 다음과 같은 의존성 규칙을 준수합니다:
-
의존성 방향:
- 모든 의존성은 안쪽(core)을 향해야 함
- 외부 계층은 내부 계층을 알 수 있지만, 내부 계층은 외부 계층을 알 수 없음
- 의존성 주입을 통해 런타임에 구현체 결정
-
library-core 모듈의 독립성:
- 외부 프레임워크나 라이브러리에 대한 직접적인 의존성 배제
- 도메인 모델의 순수성 유지
- 포트를 통한 느슨한 결합 구현
-
인프라스트럭처 모듈의 책임:
- library-core에 정의된 포트의 구현체 제공
- 기술적인 문제 해결에 집중
- 도메인 로직 포함 금지
-
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) {
// ... 구현 내용
}
}이러한 프로젝트 구조는 도메인 주도 설계와 클린 아키텍처의 원칙을 실천하면서도, 실용적인 개발이 가능한 기반을 제공합니다.