TL;DR

  • Kotlin 프로젝트 디렉토리 구조 원칙의 핵심 개념과 용어를 한눈에 정리한다.
  • Kotlin 프로젝트 디렉토리 구조 원칙이(가) 등장한 배경과 필요성을 요약한다.
  • Kotlin 프로젝트 디렉토리 구조 원칙의 특징과 적용 포인트를 빠르게 확인한다.

1. 개념

Kotlin 프로젝트 디렉토리 구조 원칙은(는) 핵심 용어와 정의를 정리한 주제로, 개발/운영 맥락에서 무엇을 의미하는지 설명한다.

2. 배경

기존 방식의 한계나 현업의 요구사항을 해결하기 위해 이 개념이 등장했다는 흐름을 이해하는 데 목적이 있다.

3. 이유

도입 이유는 보통 유지보수성, 성능, 안정성, 보안, 협업 효율 같은 실무 문제를 해결하기 위함이다.

4. 특징

  • 핵심 정의와 범위를 명확히 한다.
  • 실무 적용 시 선택 기준과 비교 포인트를 제공한다.
  • 예시 중심으로 빠른 이해를 돕는다.

5. 상세 내용

작성일: 2026-02-24 카테고리: Backend / Kotlin / Architecture 포함 내용: Kotlin, 프로젝트 구조, Package by Layer, Package by Feature, DDD 계층, 멀티 모듈, internal, sealed class, Screaming Architecture, Convention over Configuration


1. 기본 구조: Gradle/Maven 표준

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  Kotlin/JVM 표준 프로젝트 구조                              │
│                                                             │
│  my-app/                                                    │
│  ├── src/                                                   │
│  │   ├── main/                                              │
│  │   │   ├── kotlin/          ← Kotlin 소스 코드           │
│  │   │   └── resources/       ← 설정 파일, 정적 자원        │
│  │   └── test/                                              │
│  │       ├── kotlin/          ← 테스트 코드                 │
│  │       └── resources/       ← 테스트용 설정/픽스처        │
│  ├── build.gradle.kts         ← 빌드 스크립트               │
│  └── settings.gradle.kts      ← 프로젝트 이름/모듈 설정     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Convention over Configuration

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  Gradle은 표준 경로를 자동 인식                             │
│                                                             │
│  별도 설정 없이도:                                          │
│  ├── src/main/kotlin/ → 소스 루트로 자동 인식               │
│  ├── src/test/kotlin/ → 테스트 루트로 자동 인식             │
│  └── src/main/resources/ → 리소스로 자동 인식              │
│                                                             │
│  → 설정 파일에 경로를 일일이 명시할 필요 없음               │
│                                                             │
│  Java vs Kotlin:                                            │
│  ├── Java:   src/main/java/                                 │
│  ├── Kotlin: src/main/kotlin/                               │
│  └── 공존 가능 → 점진적 마이그레이션 가능                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2. 패키지 구조의 두 가지 철학

2-1. Package by Layer (계층별)

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  전통적 방식: 기술 계층 기준으로 묶기                       │
│                                                             │
│  com.example.myapp/                                         │
│  ├── controller/                                            │
│  │   ├── OrderController.kt                                 │
│  │   ├── ProductController.kt                               │
│  │   └── UserController.kt                                  │
│  ├── service/                                               │
│  │   ├── OrderService.kt                                    │
│  │   ├── ProductService.kt                                  │
│  │   └── UserService.kt                                     │
│  ├── repository/                                            │
│  │   ├── OrderRepository.kt                                 │
│  │   └── ProductRepository.kt                               │
│  ├── entity/                                                │
│  │   ├── Order.kt                                           │
│  │   └── Product.kt                                         │
│  └── dto/                                                   │
│      ├── OrderRequest.kt                                    │
│      └── OrderResponse.kt                                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

장점:

  • 단순하고 익숙함 (Spring Boot 튜토리얼 기본 구조)
  • 계층이 명확하게 보임
  • 소규모 프로젝트에서 빠르게 시작 가능

단점:

  • “주문” 기능 하나 수정 시 4~5개 패키지를 오가야 함
  • 관련 없는 파일들이 같은 폴더에 섞임
  • 코드 구조만 보고 이 앱이 뭘 하는지 파악 불가

2-2. Package by Feature (기능별) ← 권장

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  현대적 방식: 비즈니스 기능 기준으로 묶기                   │
│                                                             │
│  com.example.myapp/                                         │
│  ├── order/                                                 │
│  │   ├── OrderController.kt                                 │
│  │   ├── OrderService.kt                                    │
│  │   ├── OrderRepository.kt                                 │
│  │   ├── Order.kt                                           │
│  │   └── OrderDto.kt                                        │
│  ├── product/                                               │
│  │   ├── ProductController.kt                               │
│  │   └── ...                                                │
│  └── user/                                                  │
│      ├── UserController.kt                                  │
│      └── ...                                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

장점:

  • 주문 관련 코드가 한 곳에 모임
  • 기능 추가/삭제 = 패키지 추가/삭제
  • 코드 구조가 앱이 하는 일을 보여줌
  • Kotlin의 internal 가시성으로 패키지 내 캡슐화 가능
  • 마이크로서비스 추출 용이
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  Uncle Bob - Screaming Architecture:                        │
│                                                             │
│  "프로젝트 구조는 프레임워크가 아니라                       │
│   비즈니스를 소리쳐야 한다"                                 │
│                                                             │
│  ❌ controller/, service/, repository/ (Spring 구조)        │
│  ✅ order/, product/, user/ (비즈니스 구조)                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

비교: “주문 기능을 수정하라”

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  Package by Layer:                                          │
│  ├── controller/ → OrderController 찾기                     │
│  ├── service/ → OrderService 찾기 (관련 없는 파일과 섞임)   │
│  ├── repository/ → OrderRepository 찾기                     │
│  └── entity/ → Order 찾기 (4개 폴더 점프)                   │
│                                                             │
│  Package by Feature:                                        │
│  └── order/ → 모든 관련 파일이 여기에 (1개 폴더)           │
│                                                             │
│  결정 기준:                                                 │
│  ├── 10개 미만 클래스 → Layer도 무방                        │
│  ├── 중간 규모 이상 → Feature 권장                          │
│  └── DDD 적용 시 → Feature 필수                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

3. Spring Boot + DDD 기반 구조 (실무 권장)

Feature + Layer를 결합한 구조:

com.example.myapp/
├── order/
│   ├── domain/
│   │   ├── Order.kt               ← Entity / Aggregate Root
│   │   ├── OrderItem.kt
│   │   ├── OrderStatus.kt         ← sealed class
│   │   ├── Money.kt               ← Value Object
│   │   ├── OrderRepository.kt     ← interface만 (구현 없음)
│   │   └── OrderEvent.kt
│   ├── application/
│   │   ├── PlaceOrderUseCase.kt
│   │   ├── CancelOrderUseCase.kt
│   │   └── dto/
│   │       ├── PlaceOrderCommand.kt
│   │       └── OrderResponse.kt
│   ├── infrastructure/
│   │   ├── JpaOrderRepository.kt  ← OrderRepository 구현체
│   │   ├── OrderJpaEntity.kt      ← DB 매핑 전용 클래스
│   │   └── OrderMapper.kt
│   └── presentation/
│       └── OrderController.kt
├── product/
│   └── ...
└── common/
    ├── config/
    ├── exception/
    └── util/

각 계층의 역할

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  domain/ (도메인 계층)                                      │
│  ├── 비즈니스 규칙의 핵심                                   │
│  ├── 프레임워크 의존성 없음                                  │
│  ├── Repository는 인터페이스만 선언                          │
│  └── 아무것도 의존하지 않음                                  │
│                                                             │
│  application/ (애플리케이션 계층)                           │
│  ├── Use Case 조율                                          │
│  ├── @Transactional 관리                                    │
│  ├── domain만 의존                                          │
│  └── Command/Query DTO 보유                                 │
│                                                             │
│  infrastructure/ (인프라 계층)                              │
│  ├── domain 인터페이스 구현                                  │
│  ├── JPA Entity (DB 매핑)                                   │
│  ├── DB 접근, 외부 API 호출                                  │
│  ├── 프레임워크 의존성 집중                                  │
│  └── domain ↔ JPA Entity 변환                               │
│                                                             │
│  presentation/ (표현 계층)                                  │
│  ├── HTTP 요청/응답 처리                                     │
│  ├── @RestController                                        │
│  ├── application만 호출                                     │
│  └── 입력값 검증                                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

의존성 방향

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  presentation → application → domain ← infrastructure      │
│                                                             │
│  핵심: infrastructure가 domain을 구현하지만                 │
│        domain은 infrastructure를 모름 (DIP!)               │
│                                                             │
│  ┌──────────────┐     ┌──────────────┐                      │
│  │ presentation │ → → │ application  │ → → domain           │
│  └──────────────┘     └──────────────┘       ▲             │
│                                              │             │
│                              infrastructure ─┘             │
│                              (domain 인터페이스 구현)        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

4. domain 모델 vs JPA Entity 분리

분리하지 않은 경우 (문제)

// 분리 전: JPA 어노테이션과 비즈니스 로직이 혼재
@Entity
@Table(name = "orders")
class Order(
    @Id
    val id: Long = 0,          // JPA 요구: 기본값 필요

    @Column
    var status: String = "",   // JPA 요구: var 강제

    @OneToMany
    val items: MutableList<OrderItem> = mutableListOf()
) {
    fun cancel() { ... }  // 비즈니스 로직
}
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  분리 전 문제점:                                            │
│                                                             │
│  ├── JPA 제약(기본 생성자, var 강제)이 도메인 오염          │
│  ├── DB 스키마 변경 시 도메인 모델 영향                     │
│  ├── 도메인 테스트에 JPA/DB 필요                            │
│  └── 관심사 혼재                                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

분리한 경우 (권장)

// domain/Order.kt → 순수 도메인 모델
class Order(
    val id: OrderId,
    val customerId: CustomerId,
    private var status: OrderStatus,
    private val items: List<OrderItem>
) {
    fun cancel(reason: String) {
        check(status != OrderStatus.Cancelled) { "이미 취소됨" }
        status = OrderStatus.Cancelled
    }
}

// infrastructure/OrderJpaEntity.kt → DB 매핑 전용
@Entity
@Table(name = "orders")
class OrderJpaEntity(
    @Id val id: Long,
    @Column var status: String,
    @Column val customerId: Long
)

// infrastructure/OrderMapper.kt → 변환 담당
fun OrderJpaEntity.toDomain(): Order = Order(...)
fun Order.toJpaEntity(): OrderJpaEntity = OrderJpaEntity(...)
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  주의:                                                      │
│  소규모 프로젝트는 이 분리가 불필요할 수 있음               │
│  프로젝트 규모와 복잡도에 따라 결정                         │
│                                                             │
│  분리 기준:                                                 │
│  ├── 도메인 로직이 복잡하고 지속적으로 발전하는가?          │
│  ├── DB 스키마와 도메인 모델이 자주 따로 변경되는가?        │
│  └── 팀 규모가 있고 장기 유지보수가 필요한가?               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

5. 멀티 모듈 프로젝트

구조

my-app/                          ← 루트 프로젝트
├── api/                         ← Spring Boot 진입점
│   └── src/main/kotlin/
├── domain/                      ← 순수 비즈니스 로직
│   └── src/main/kotlin/
├── infra/                       ← DB, 외부 API
│   └── src/main/kotlin/
├── common/                      ← 공통 유틸
│   └── src/main/kotlin/
└── settings.gradle.kts
// settings.gradle.kts
rootProject.name = "my-app"
include("api", "domain", "infra", "common")

// api/build.gradle.kts
dependencies {
    implementation(project(":domain"))
    implementation(project(":common"))
}

// infra/build.gradle.kts
dependencies {
    implementation(project(":domain"))
    implementation(project(":common"))
}

모듈 간 의존성

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  api ──→ domain, common                                     │
│  infra ─→ domain, common                                    │
│  domain → common (또는 의존 없음)                           │
│  common → 없음                                              │
│                                                             │
│  장점:                                                      │
│  ├── 컴파일 타임에 의존성 방향 강제                          │
│  │   (domain이 infra를 import하면 빌드 에러!)               │
│  ├── 모듈별 독립 빌드/테스트 가능                           │
│  └── 팀별 모듈 소유권 명확                                  │
│                                                             │
│  도입 시점:                                                 │
│  ├── 개발자 5인 이상                                        │
│  ├── 빌드 수준 의존성 강제가 필요할 때                      │
│  └── 모노레포에서 도메인을 여러 앱이 공유할 때              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

6. Kotlin 고유의 장점 활용

6-1. internal 가시성

// Java에는 없는 모듈 단위 캡슐화

// infrastructure/ 내부에서만 사용 (외부 모듈 접근 불가)
internal class JpaOrderRepository : OrderRepository {
    // domain 인터페이스 구현이지만 외부에 노출 안 함
}

// application/ 에서 외부로 공개
class PlaceOrderUseCase(
    private val orderRepository: OrderRepository  // 인터페이스만 노출
)
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  internal = 모듈 단위 가시성                                │
│                                                             │
│  ├── 같은 모듈 내에서는 접근 가능                           │
│  ├── 다른 모듈에서는 접근 불가                              │
│  └── Java의 package-private보다 강력한 캡슐화               │
│                                                             │
│  멀티 모듈 + internal 조합:                                 │
│  → 구현 세부사항을 모듈 경계에서 완전히 숨김               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

6-2. sealed class로 도메인 상태 표현

// OrderStatus.kt
sealed class OrderStatus {
    object Pending   : OrderStatus()
    object Paid      : OrderStatus()
    object Shipped   : OrderStatus()
    object Delivered : OrderStatus()
    data class Cancelled(val reason: String) : OrderStatus()
}

// when 완전성 보장 → 새 상태 추가 시 컴파일 에러
fun describe(status: OrderStatus) = when (status) {
    is OrderStatus.Pending   -> "결제 대기"
    is OrderStatus.Paid      -> "결제 완료"
    is OrderStatus.Shipped   -> "배송 중"
    is OrderStatus.Delivered -> "배송 완료"
    is OrderStatus.Cancelled -> "취소: ${status.reason}"
}

6-3. data class와 파일 수준 함수

// Value Object: data class가 완벽히 적합
data class Money(val amount: Long, val currency: String) {
    operator fun plus(other: Money): Money {
        check(currency == other.currency)
        return copy(amount = amount + other.amount)
    }
}

// 매퍼: 유틸 클래스 없이 확장 함수로
// OrderMapper.kt
fun Order.toResponse() = OrderResponse(
    id = id.value,
    status = status.toString()
)

fun OrderJpaEntity.toDomain() = Order(...)

// 한 파일에 여러 클래스/함수 → Kotlin에서 자연스러운 패턴

7. resources/ 구조

src/main/resources/
├── application.yml              ← 공통 설정
├── application-dev.yml          ← 개발 환경 (profile)
├── application-prod.yml         ← 운영 환경 (profile)
├── db/
│   └── migration/               ← Flyway / Liquibase
│       ├── V1__init.sql
│       ├── V2__add_order.sql
│       └── V3__add_index.sql
├── static/                      ← 정적 파일 (선택)
└── templates/                   ← Thymeleaf 등 (선택)

src/test/resources/
├── application-test.yml         ← 테스트 DB 설정
└── fixtures/                    ← 테스트용 샘플 데이터
    ├── order.json
    └── product.json
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  Profile 기반 설정 분리:                                    │
│                                                             │
│  application.yml → 공통 (항상 적용)                         │
│  application-dev.yml → 개발 환경 설정 override              │
│  application-prod.yml → 운영 환경 설정 override             │
│                                                             │
│  활성화:                                                    │
│  spring.profiles.active=dev                                 │
│                                                             │
│  Flyway 명명 규칙:                                          │
│  V{버전}__{설명}.sql                                        │
│  V1__init.sql, V2__add_order_table.sql                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

8. 테스트 디렉토리 구조

원칙: main 구조를 그대로 미러링

src/test/kotlin/com/example/myapp/
├── order/
│   ├── domain/
│   │   └── OrderTest.kt             ← 단위 테스트 (순수)
│   ├── application/
│   │   └── PlaceOrderUseCaseTest.kt ← Mock 기반 단위 테스트
│   ├── infrastructure/
│   │   └── JpaOrderRepositoryTest.kt ← 통합 테스트 (DB 필요)
│   └── presentation/
│       └── OrderControllerTest.kt   ← API 테스트 (MockMvc)
└── support/
    ├── IntegrationTestBase.kt       ← 테스트 공통 설정
    └── TestFixtures.kt              ← 테스트 데이터 팩토리

테스트 분류

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  domain/    → 순수 단위 테스트 (Spring 없음)                │
│  ├── 빠름 (ms 단위)                                         │
│  ├── 외부 의존성 없음                                       │
│  └── 비즈니스 규칙 검증 집중                                │
│                                                             │
│  application/ → Mock 기반 단위 테스트                       │
│  ├── 빠름                                                   │
│  ├── Repository를 Mock으로 대체                             │
│  └── Use Case 흐름 검증                                     │
│                                                             │
│  infrastructure/ → 통합 테스트                              │
│  ├── @DataJpaTest (DB 필요)                                 │
│  ├── 실제 DB 쿼리 검증                                      │
│  └── Testcontainers로 실제 DB 사용 가능                     │
│                                                             │
│  presentation/ → API 테스트                                 │
│  ├── MockMvc / WebTestClient 사용                           │
│  ├── HTTP 요청/응답 형식 검증                               │
│  └── 입력값 검증 테스트                                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

9. 규모별 권장 구조 요약

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  소규모 (~30개 클래스, 1~2인):                              │
│  → Package by Layer                                         │
│  → 빠르게 시작, 나중에 리팩토링                             │
│                                                             │
│  중간 규모 (~100개 클래스, 3~5인):                          │
│  → Package by Feature + 계층 서브패키지                     │
│  → feature/service/, feature/repository/ 형태               │
│                                                             │
│  대규모 (100개+ 클래스, 5인+):                              │
│  → 멀티 모듈 + Package by Feature + DDD 계층               │
│  → 빌드 수준 의존성 강제                                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

5가지 핵심 원칙

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  1. 관련 코드는 가까이 둬라 (Package by Feature)            │
│                                                             │
│  2. 의존성은 안쪽(도메인)으로 향해야 한다                   │
│     presentation → application → domain ← infrastructure   │
│                                                             │
│  3. 도메인 계층은 프레임워크에 의존하지 않는다              │
│     domain/ 에 @Entity, @Service 없음                       │
│                                                             │
│  4. 구조만 보고 "이 앱이 뭘 하는지" 알 수 있어야           │
│     order/, product/, user/ → Screaming Architecture        │
│                                                             │
│  5. 현재 규모에 맞게, 과도한 구조는 피한다                  │
│     10개 클래스에 멀티 모듈 + DDD는 오버엔지니어링          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

관련 키워드

Kotlin, 프로젝트 구조, Package by Layer, Package by Feature, Screaming Architecture, DDD, Bounded Context, 멀티 모듈, Gradle, internal, sealed class, data class, Convention over Configuration, 의존성 역전, DIP, Clean Architecture, Layered Architecture, Aggregate Root, Value Object, Use Case