1. 클린 아키텍처 정의
: 클린 아키텍처는 도메인 중심 아키텍처로,
1) 비즈니스 규칙(Entity, Use Case)을 가장 안쪽에 두고,
2) 외부 의존성(웹, DB, 메시지 브로커, 프레임워크)을 바깥쪽에 둠
3) 의존성은 항상 안쪽으로만 향한다 (DIP: Dependency Inversion Principle)
즉, 외부 기술은 교체 가능하고, 핵심 도메인은 독립적이라는 것이 핵심!
2. 구조 설명
1) Entities (Domain Model)
- 순수한 비즈니스 규칙 (ex. Concert, User, Order 등)
- 프레임워크에 전혀 의존하지 않는 POJO/POKO
2) Use Cases (Application Service)
- 애플리케이션 규칙 (비즈니스 절차)
- 엔티티를 조합하여 유스케이스 수행
- 외부 시스템(DB, API)은 인터페이스를 통해서만 접근
3) Interface Adapters
- 외부와 내부 간의 변환 계층
- Controller, Presenter, Repository 구현체, Mapper 등이 위치
4) Frameworks & Drivers (Infrastructure)
- Spring, DB, Kafka, Redis, UI 등
- 가장 바깥쪽 계층, 쉽게 교체 가능해야 함
3. 핵심가치
: SOLID + 컴포넌트 수준 원칙(REP, CCP, CRP, ADP, SDP, SAP)
1) SOLID(Class level)
"SRP (단일 책임 원칙)"
: 클래스는 오직 하나의 변경 이유만 가져야 함
"OCP (개방-폐쇄 원칙)"
: 확장에는 열려있고, 변경에는 닫혀있어야 함 (→ 인터페이스, 다형성 활용)
"LSP (리스코프 치환 원칙)"
: 상위 타입을 하위 타입으로 치환 가능해야 함
"ISP (인터페이스 분리 원칙)"
: 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 작게 쪼개라
"DIP (의존성 역전 원칙)"
: 고수준 모듈은 저수준 구현에 의존하지 않고, 둘 다 추상에 의존해야 함
2) 컴포넌트(여러 클래스들의 집합) 레벨 원칙
"REP (재사용/릴리스 등가 원칙)"
- 재사용 단위 = 릴리즈 단위.
예: payment-core 라이브러리를 따로 빼서 배포하면, 그 안에는 반드시 하나의 응집된 책임만 있어야 함.
"CCP (공통 폐쇄 원칙)"
- 함께 변경되는 클래스는 같은 컴포넌트에 두어야 함.
예: Order와 OrderValidator는 항상 같이 변경되므로 같은 domain.order에 위치.
"CRP (공통 재사용 원칙)"
- 쓰지도 않는 클래스들에 의존하지 않도록 컴포넌트를 분리.
예: payment-core를 썼는데 내부적으로 coupon도 딸려오면 안 됨.
"ADP (비순환 의존 원칙)"
- 컴포넌트 간 의존성은 DAG(방향 비순환 그래프)여야 한다.
- 순환 참조가 생기면 변경과 빌드, 배포가 지옥됨. DIP로 인터페이스를 추가해 깨야 함.
"SDP (안정된 의존 원칙)"
- 불안정한 컴포넌트는 안정된 컴포넌트에 의존해야 한다.
- 도메인 계층은 안정된 계층이고, 프레젠테이션/UI 계층은 변하기 쉬움 → UI가 도메인에 의존하는 건 OK, 반대는 안 됨.
"SAP (안정된 추상 원칙)"
- 안정적인 컴포넌트일수록 추상화돼 있어야 한다.
예: domain은 가장 안정적이므로 인터페이스(Repository, Service Port) 중심
4. 예제 코드
com.example.concert
├── domain
│ ├── model
│ │ └── Concert.kt
│ ├── repository
│ │ └── ConcertRepository.kt // Port (interface)
│ └── service
│ └── ConcertValidator.kt
│
├── application
│ └── usecase
│ └── ReserveConcertUseCase.kt
│
├── adapter
│ ├── inbound
│ │ └── web
│ │ └── ConcertController.kt
│ └── outbound
│ ├── persistence
│ │ └── ConcertJpaRepository.kt // Adapter 구현체
│ └── messaging
│ └── ConcertKafkaProducer.kt
│
└── infrastructure
├── config
│ └── JpaConfig.kt
└── persistence
└── ConcertJpaEntity.kt
(1) Domain Layer - Entity
package com.example.concert.domain.model
data class Concert(
val id: Long? = null,
val title: String,
val availableSeats: Int
) {
fun reserveSeat(): Concert {
require(availableSeats > 0) { "No seats available" }
return this.copy(availableSeats = this.availableSeats - 1)
}
}
(2) Domain Layer - Repository Port
package com.example.concert.domain.repository
import com.example.concert.domain.model.Concert
interface ConcertRepository {
fun findById(id: Long): Concert?
fun save(concert: Concert): Concert
}
(3) Application Layer - Use Case
package com.example.concert.application.usecase
import com.example.concert.domain.repository.ConcertRepository
class ReserveConcertUseCase(
private val concertRepository: ConcertRepository
) {
fun reserve(concertId: Long) {
val concert = concertRepository.findById(concertId)
?: throw IllegalArgumentException("Concert not found")
val updatedConcert = concert.reserveSeat()
concertRepository.save(updatedConcert)
}
}
(4) Adapter Layer - Inbound (Controller)
package com.example.concert.adapter.inbound.web
import com.example.concert.application.usecase.ReserveConcertUseCase
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/concerts")
class ConcertController(
private val reserveConcertUseCase: ReserveConcertUseCase
) {
@PostMapping("/{id}/reserve")
fun reserve(@PathVariable id: Long) {
reserveConcertUseCase.reserve(id)
}
}
(5) Adapter Layer - Outbound (Persistence Adapter)
package com.example.concert.adapter.outbound.persistence
import com.example.concert.domain.model.Concert
import com.example.concert.domain.repository.ConcertRepository
import org.springframework.stereotype.Repository
@Repository
class ConcertJpaRepository(
private val jpa: SpringDataConcertJpaRepository
) : ConcertRepository {
override fun findById(id: Long): Concert? =
jpa.findById(id).orElse(null)?.toDomain()
override fun save(concert: Concert): Concert =
jpa.save(ConcertJpaEntity.from(concert)).toDomain()
}
(6) Infrastructure - JPA Entity & Spring Data Repository
package com.example.concert.infrastructure.persistence
import com.example.concert.domain.model.Concert
import jakarta.persistence.*
@Entity
@Table(name = "concerts")
data class ConcertJpaEntity(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
val title: String,
val availableSeats: Int
) {
fun toDomain() = Concert(id, title, availableSeats)
companion object {
fun from(concert: Concert) = ConcertJpaEntity(
id = concert.id,
title = concert.title,
availableSeats = concert.availableSeats
)
}
}
interface SpringDataConcertJpaRepository : org.springframework.data.jpa.repository.JpaRepository<ConcertJpaEntity, Long>
5. 장점
"비즈니스 로직 독립성"
- 도메인 모델과 유즈케이스(application logic)는 프레임워크, UI, DB에 의존하지 않음.
- 예를 들어 DB를 JPA에서 MongoDB로 교체해도 도메인/유즈케이스는 영향을 거의 안 받음.
"테스트 용이성"
- 도메인과 유즈케이스 계층이 외부 의존성(DB, Kafka, Redis 등) 없이 순수 Kotlin/Java 코드로 존재.
- 단위 테스트 작성이 쉬움 (Mock Adapter만 교체).
"유연한 확장성"
- 다양한 Adapter (REST, GraphQL, Kafka Consumer 등)를 붙이기 용이.
- 동일한 도메인/유즈케이스에 대해 REST API, gRPC, CLI를 동시에 제공할 수 있음.
"의존성 역전(DIP)"
- 항상 바깥 계층이 안쪽 계층(도메인, 유즈케이스)에 의존 → 변경에 강함.
- 핵사고날(Ports & Adapters)과 개념적으로 유사.
"관심사 분리 (Separation of Concerns)"
- 도메인 로직, 유즈케이스, 인프라, UI가 명확히 나뉘어 책임 구분이 확실.
6. 단점
"초기 복잡도 높음"
- 패키지 구조와 계층이 많아져 작은 프로젝트에는 오버엔지니어링.
- 레이어드 아키텍처보다 학습 곡선이 큼.
"초반 개발 속도 저하"
- 의존성 역전을 위해 인터페이스(Port) + Adapter를 항상 고려해야 함.
- 단순 CRUD에도 Interface/Adapter를 작성해야 하는 부담.
"네이밍 혼란 가능"
- UseCase / ApplicationService / DomainService / Adapter 등 구분이 모호할 수 있음.
- 팀원 간 "어디에 뭘 넣어야 하는지" 컨벤션이 없으면 혼란 발생.
"과도한 추상화 위험"
- 인터페이스(Port)가 늘어나면서 실제 구현보다 추상화가 많아져 관리 비용 증가.
- 잘못 설계하면 DIP가 불필요한 계층을 양산.
"DDD 적용 어려움"
- DDD를 잘 이해하지 못하면 "클린 아키텍처"가 단순히 레이어 분리 수준으로 전락.
- 결국 SRP(단일책임원칙)가 무너져 Controller, UseCase, Service가 뒤섞일 수 있음.
'Kotlin Spring > Kotlin Spring 강의 내용' 카테고리의 다른 글
6) 개발 architecture (5) 강의용 Architecture (0) | 2025.09.01 |
---|---|
6) 개발 architecture (4) EDA(Event-Driven Architecture) 패턴 (1) | 2025.08.31 |
6) 개발 architecture (2) 헥사고날 아키텍처(Hexagonal Architecture) (0) | 2025.08.31 |
6) 개발 architecture (1) 레이어드 아키텍처(Layered Architecture) (0) | 2025.08.31 |
5) 요구사항 분석 및 ERD 설계 (0) | 2025.08.31 |