DDD Practice No.1 자동차 공장
DDD와 유사한 코드를 작성하는 방법을 검사하는 자동차 공장의 예입니다.
JPA를 기반으로 작성된 예제였으며 더 많은 핵심 도메인에 집중할 수 있는 이점이 있음을 확인했습니다.
아래 링크로 가셔서 README.md나 소스코드를 켜시면 됩니다.
(테스트 코드 포함)
자동차 공장에 대한 설명
도메인 간의 유기적 관계는 많이 사용되지 않습니다.
유산 JPA의 예제와 다양한 옵션을 사용하여 집계의 내부 일관성을 유지하고 도메인의 핵심인 집계 루트를 쉽게 식별할 수 있습니다.
일 나는 예를 썼다.
자동차 공장에는 자동차와 바퀴가 존재하지만, 자동차 없이는 바퀴도 존재하지 않는다는 전제 하에 자동차를 집합근으로 가정하고, 바퀴는 자동차의 일부로 생성되고 삭제된 하위 개념이다.
즉, 시스템의 핵심이 자동차에 쓰여졌습니다.
레거시 대 DDD 비교 미리 보기
레거시에는 ddd보다 WheelRepository가 하나 더 있는 것을 볼 수 있습니다.
익명의 사람이 위의 파일 목록을 본다면 바퀴의 상태와 용도는 자동차와 비슷하다고 여겨집니다.
물론 바퀴와 자동차를 보면 누구나 자동차가 핵이라고 생각하겠지만 실제로는 복잡한 도메인 영역에서 어떤 물체가 핵인지 한눈에 알기 어려울 수 있다.
레거시 예
자동차(LegacyCarEntity)를 OneToMany로 연결하여 가져오기까지 참여했는데, 자동차와 바퀴가 독립적으로 관리되는 것을 확인할 수 있습니다.
테스트 코드휠 저장소가 존재하고 독립적으로 유지되기 때문에 휠이 자동차에 완전히 종속되어 있는지 확인하기 어렵습니다.
즉 어떤 개념이 핵심인지 한눈에 파악하기 어렵다.
@Entity
class LegacyCarEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var carId: Long = 0
@OneToMany
@JoinColumn(name = "carId", referencedColumnName = "carId")
var wheels: MutableList<LegacyWheelEntity> = mutableListOf()
companion object {
fun of(): LegacyCarEntity = LegacyCarEntity()
}
}
@Entity
class LegacyWheelEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false)
var wheelId: Long = 0
@Column(nullable = false)
var carId: Long = 0
companion object {
fun of(carId: Long): LegacyWheelEntity =
LegacyWheelEntity().apply {
this.carId = carId
}
}
}
바퀴와 관련이 있지만 바퀴는 독립적으로 조작할 수 있는 가능성(CRUD)이 있습니다.
이 코드에서는 프로그램이 실행되는 동안 car 객체 자체가 인식하지 못한 채 다른 바퀴가 실행되는 문제가 있을 수 있습니다.
레거시CarFactoryService보면 car와 wheel을 따로 생성해야 하는 것을 알 수 있고 서비스가 2개의 의존성을 가지고 있음을 알 수 있다.
비즈니스가 더 복잡해짐에 따라 더 많은 리포지토리 또는 빈이 이에 의존합니다.
@Service
class LegacyCarFactoryService(
private val carRepository: LegacyCarRepository,
private val wheelRepository: LegacyWheelRepository,
) {
@Transactional
fun createCar() {
val car = LegacyCarEntity.of()
carRepository.save(car)
repeat((1..4).count()) {
val wheel = LegacyWheelEntity.of(car.carId)
wheelRepository.save(wheel)
}
}
}
예
ddd 예에서 자동차는 (CarEntity), OneToMany로 바퀴를 관리할 때의 차이점은 캐스케이드 옵션을 주기 때문에 자동차를 생성하거나 삭제하면 바퀴도 생성되거나 삭제되기 때문에 자동차에서 바퀴의 수명이 달려있다고 볼 수 있다.
더 이상 휠 저장소가 없기 때문에 휠을 독립적으로 제어할 수 없습니다.
또한 자동차와 바퀴에 대한 AggregateRoot 및 DomainEntity 인터페이스가 둘의 관계와 중요성을 보다 명확하게 표현하도록 수정되었습니다.
@Entity
class CarEntity : AggregateRoot {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var carId: Long = 0
// 양방향 매핑, car 생성/삭제할 때 wheel도 함께 생성, 삭제
@OneToMany(cascade = (CascadeType.PERSIST, CascadeType.REMOVE), mappedBy = "car")
var wheels: MutableList<WheelEntity> = mutableListOf()
// 바퀴의 추가를 자동차가 관리한다.
fun addWheel(wheel: WheelEntity) {
wheels.add(wheel)
}
// 바퀴의 제거를 자동차가 관리한다.
fun removeWheel(wheel: WheelEntity) {
wheels.remove(wheel)
}
companion object {
fun of(): CarEntity =
CarEntity()
}
}
@Entity
class WheelEntity : DomainEntity<CarEntity> {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false)
var wheelId: Long = 0
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "carId", nullable = false)
lateinit var car: CarEntity
protected set
companion object {
fun of(car: CarEntity): WheelEntity =
WheelEntity().apply {
this.car = car
}
}
}
레거시와 달리 자동차가 바퀴를 직접 관리합니다.
즉, 자동차와 바퀴의 상태는 집계 루트 car에 의해서만 변경될 수 있으며 car 개체는 프로그램이 실행되는 동안 바퀴가 실행 중인지 여부를 항상 알 수 있습니다.
즉, 자동차와 바퀴 세트가 일관되게 변경됩니다.
CarFactoryService레거시와 달리 WheelRepository는 더 이상 존재하지 않으며 자동차를 통해서만 바퀴를 추가할 수 있습니다.
Legacy에 비해 의존성도 줄어들었고 로직의 핵심이 무엇인지 더 쉽게 알 수 있습니다.
@DomainService
class CarFactoryService(
private val carRepository: CarRepository,
) {
@Transactional
fun createCar() {
val car = CarEntity.of()
repeat((1..4).count()) {
val newWheel = WheelEntity.of(car)
car.addWheel(newWheel)
}
carRepository.save(car)
}
}
묻다
- 새로운 요구 사항으로 인해 휠이 Aggregate Root로 승격되는 경우가 있습니까? 이 경우 자동차와는 다른 집합체로 전개되어야 하는데, 그렇다면 자동차와 바퀴의 관계는 어떻게 전개되어야 하는가?
지름길
Github 프로젝트: https://github.com/traeper/ddd-practice/tree/main/car_factory