| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | |||||
| 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 31 |
- Functional Programming
- define
- 코루틴
- 코틀린
- ㅋㅁ
- 팩토리 메소드
- Design Pattern
- builderPattern
- Coroutines
- compose
- 함수형프로그래밍
- Kotlin
- 옵저버 패턴
- PrototypePattern
- 디자인패턴
- 디자인패턴 #
- kotlin multiplatform
- 코틀린멀티플랫폼
- designPattern
- 빌터패턴
- 프로토타입 패턴
- 추상 팩토리
- android designsystem
- Abstract Factory
- 안드로이드 디자인시스템
- material3
- factory method
- Observer Pattern
- kmp
- 추상팩토리패턴
- Today
- Total
오늘도 더 나은 코드를 작성하였습니까?
도메인 주도 설계 (Domain Driven Design - DDD)의 기초 개념정리2 본문
전술적 설계 (Tactical Design)

1. Entity (엔티티)
- 고유한 식별자(ID)로 구별되는 객체입니다. 속성이 바뀌어도 같은 것으로 취급됩니다.
핵심 특징:
- ID가 같으면 같은 객체로 간주
- 시간에 따라 상태가 변함 (가변)
- 생명주기를 가짐 (생성 → 변경 → 삭제)
class User(
val id: UserId, // 식별자
var name: String, // 바뀔 수 있는 속성
var email: Email
) {
override fun equals(other: Any?): Boolean {
if (other !is User) return false
return this.id == other.id // ID로만 동등성 판단
}
override fun hashCode(): Int = id.hashCode()
}
2. Value Object (값 객체)
- 식별자 없이 값 자체로 동등성이 판단되는 불변 객체입니다.
핵심 특징:
- 모든 속성이 같으면 같은 객체
- 불변(Immutable) — 한번 만들면 못 바꿈, 바꾸려면 새로 생성
- ID 없음
- 생명주기 없음
data class Money(
val amount: BigDecimal,
val currency: Currency
) {
operator fun plus(other: Money): Money {
require(currency == other.currency) { "통화가 달라요" }
return Money(amount + other.amount, currency) // 새 객체 반환
}
}
data class Address(
val zipCode: String,
val street: String,
val city: String
)
*도메인 모델링에서는 가능한 한 Value Object를 많이 쓰는 게 좋다. 원시 타입(String, Int)을 그대로 쓰지 말고 의미를 담은 Value Object로 감싸서 의미를 전달한다.
// 나쁨 - 그냥 String
fun sendEmail(email: String)
// 좋음 - Email Value Object
@JvmInline value class Email(val value: String) {
init { require(value.contains("@")) }
}
fun sendEmail(email: Email)
Aggregate (애그리거트)
- 함께 변경되어야 하는 엔티티·값 객체의 묶음이며, 트랜잭션과 일관성의 단위입니다.
핵심 특징:
- 외부에서는 Aggregate Root를 통해서만 접근
- 1개의Root Entity를 가짐
- 내부 객체에 직접 접근 금지
- 하나의 트랜잭션에서 변경되는 단위
- 불변식(invariant)을 보장하는 책임
// Aggregate Root
class Order(
val id: OrderId,
private val items: MutableList<OrderItem> = mutableListOf(), // 내부 엔티티
private var status: OrderStatus = OrderStatus.DRAFT,
val shippingAddress: Address // 값 객체
) {
// 외부는 Order를 통해서만 OrderItem 조작 가능
fun addItem(product: Product, quantity: Int) {
require(status == OrderStatus.DRAFT) { "확정된 주문은 수정 불가" }
require(items.size < 100) { "한 주문에 100개 초과 불가" }
items.add(OrderItem(product.id, quantity, product.price))
}
fun totalAmount(): Money = items
.map { it.subtotal() }
.reduce { acc, money -> acc + money }
fun confirm() {
require(items.isNotEmpty()) { "빈 주문은 확정 불가" }
status = OrderStatus.CONFIRMED
}
// 외부에 노출할 땐 읽기 전용
fun items(): List<OrderItem> = items.toList()
}
// Aggregate 내부 엔티티 (외부에서 직접 생성/접근 X)
class OrderItem(
val productId: ProductId,
var quantity: Int,
val unitPrice: Money
) {
fun subtotal(): Money = unitPrice * quantity
}
*Aggregate 설계 원칙
- 작게 유지: 큰 애그리거트는 동시성·성능 문제를 일으킴
- ID로 다른 애그리거트 참조: Order가 User 객체를 직접 들고 있지 말고 userId: UserId로 참조
- 한 트랜잭션엔 하나의 애그리거트만 변경: 여러 애그리거트를 한 번에 바꿔야 한다면 도메인 이벤트로 비동기 처리
4. Repository (리포지토리)
- 애그리거트를 저장하고 조회하는 추상화입니다. 도메인 입장에서는 컬렉션처럼 보이고, 실제 DB 구현은 인프라 계층에 숨깁니다.
// 도메인 계층 - 인터페이스만 정의
interface OrderRepository {
fun findById(id: OrderId): Order?
fun save(order: Order)
fun findByUserId(userId: UserId): List<Order>
}
// 인프라 계층 - 실제 구현 (Room, Retrofit 등)
class OrderRepositoryImpl(
private val orderDao: OrderDao,
private val mapper: OrderMapper
) : OrderRepository {
override fun findById(id: OrderId): Order? {
return orderDao.findById(id.value)?.let { mapper.toDomain(it) }
}
override fun save(order: Order) {
orderDao.insertOrUpdate(mapper.toEntity(order))
}
override fun findByUserId(userId: UserId): List<Order> {
return orderDao.findByUserId(userId.value).map { mapper.toDomain(it) }
}
}
중요 원칙:
- 애그리거트 단위로만 조작: OrderItemRepository 같은 건 만들지 않음. Order를 통해 접근
- 인터페이스는 도메인 계층, 구현체는 인프라 계층: 의존성 역전(DIP)
- 컬렉션처럼 설계: findAll(), save(), remove() 같은 메서드. SQL 냄새 풍기지 않게
안드로이드의 Repository 패턴과 비슷하지만 DDD의 Repository는 반드시 애그리거트만 다룬다는 점이 다릅니다.
안드로이드 Repository는 보통 DTO나 단순 모델까지 다 다루죠.
5. Domain Service (도메인 서비스)
- 특정 엔티티나 값 객체에 자연스럽게 속하지 않는 도메인 로직을 담는 곳입니다.
언제 쓰나?
- 여러 애그리거트를 협력시켜야 할 때
- 어느 한 객체에 책임을 두기 어색할 때
- 외부 시스템과 협력해야 하는 도메인 로직
// 두 계좌 사이 송금 - Account 하나에 두기 어색함
class TransferService(
private val accountRepository: AccountRepository
) {
fun transfer(fromId: AccountId, toId: AccountId, amount: Money) {
val from = accountRepository.findById(fromId) ?: error("계좌 없음")
val to = accountRepository.findById(toId) ?: error("계좌 없음")
from.withdraw(amount) // 도메인 로직은 여전히 Account 안에
to.deposit(amount)
accountRepository.save(from)
accountRepository.save(to)
}
}
주의: 도메인 서비스로 도망치는 게 너무 쉬워서 남발하면 빈혈 도메인 모델(Anemic Domain Model) 이 됩니다. 엔티티는 데이터만 들고 서비스가 모든 로직을 갖는 안티패턴이에요. 먼저 엔티티/값 객체에 책임을 두려고 시도하고, 정말 어색할 때만 도메인 서비스로 빼야 합니다.
6. Domain Event (도메인 이벤트)
- 도메인에서 의미 있는 일이 일어났음을 알리는 객체입니다. 과거형으로 이름 짓습니다.
sealed class DomainEvent {
abstract val occurredAt: Instant
}
data class OrderPlaced(
val orderId: OrderId,
val userId: UserId,
val totalAmount: Money,
override val occurredAt: Instant = Instant.now()
) : DomainEvent()
data class PaymentCompleted(
val orderId: OrderId,
val paymentId: PaymentId,
override val occurredAt: Instant = Instant.now()
) : DomainEvent()
왜 쓰나?
- 애그리거트 간 결합도 낮춤: Order가 Payment를 직접 호출하지 않고, "주문됨" 이벤트만 발행. 결제 모듈이 이벤트를 구독해서 알아서 처리.
- 컨텍스트 간 통합: 주문 컨텍스트의 이벤트를 배송·알림 컨텍스트가 구독.
- 이벤트 소싱(Event Sourcing)의 기반: 상태가 아닌 이벤트의 흐름으로 시스템을 구성.
class Order(/* ... */) {
private val events = mutableListOf<DomainEvent>()
fun confirm() {
require(items.isNotEmpty())
status = OrderStatus.CONFIRMED
events.add(OrderPlaced(id, userId, totalAmount())) // 이벤트 발생
}
fun pullEvents(): List<DomainEvent> {
val pulled = events.toList()
events.clear()
return pulled
}
}
안드로이드로 비유하면 LiveData/Flow로 상태 변화를 알리는 것과 발상이 비슷해요. 다만 도메인 이벤트는 비즈니스 사실의 기록이라는 점에서 의미가 더 무겁습니다.
7. Factory (팩토리)
- 복잡한 애그리거트 생성 로직을 캡슐화하는 객체입니다.
언제 쓰나?
- 생성 자체에 복잡한 규칙이 있을 때
- 여러 객체를 조합해야 할 때
- 다양한 형태로 생성될 수 있을 때
class OrderFactory(
private val pricingPolicy: PricingPolicy,
private val discountService: DiscountService
) {
fun createFromCart(cart: Cart, user: User): Order {
require(cart.items.isNotEmpty()) { "빈 장바구니로 주문 불가" }
val orderItems = cart.items.map { cartItem ->
val price = pricingPolicy.calculate(cartItem.product, user)
val discounted = discountService.applyDiscount(price, user)
OrderItem(cartItem.product.id, cartItem.quantity, discounted)
}
return Order(
id = OrderId.generate(),
userId = user.id,
items = orderItems.toMutableList(),
shippingAddress = user.defaultAddress
)
}
}
팁: 생성 로직이 단순하면 굳이 Factory를 만들지 말고 생성자나 정적 팩토리 메서드로 충분합니다.
class Order private constructor(...) {
companion object {
fun create(userId: UserId, items: List<OrderItem>): Order {
require(items.isNotEmpty())
return Order(OrderId.generate(), userId, items.toMutableList())
}
}
}
DDD 개념안드로이드 일반적 위치
| Entity, Value Object | domain/model/ 안의 클래스 |
| Aggregate | 동일하게 domain/model/ |
| Repository (인터페이스) | domain/repository/ |
| Repository (구현체) | data/repository/ |
| Domain Service | domain/service/ 또는 UseCase로 흡수되기도 함 |
| Domain Event | Flow/Channel로 발행하거나 EventBus 활용 |
| Factory | domain/factory/ 또는 companion object |
