Order
엔티티와 OrderItem
엔티티가 있을 때, Order
는 여러 OrderItem
을 가질 수 있고, OrderItem
은 하나의 Order
에 속한다고 할 때 사용한다.Product
엔티티가 여러 개의 ProductImage
엔티티를 가지고 있지만, ProductImage
에서 Product
을 직접 참조하지 않는 경우가 이에 해당한다.단방향 예시 (FoodTruckJpaEntity
)
@Entity
@Table(name = "food_trucks")
class FoodTruckJpaEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@OneToMany(cascade = [CascadeType.ALL])
@JoinColumn(name = "food_truck_id")
val menus: List<MenuJpaEntity> = mutableListOf()
)
@Entity
@Table(name = "menus")
class MenuJpaEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
)
위 같이 Food Truck에 중점적인 단방향 매핑은 아래와 같은 장점이 있다.
단점으로는 아래와 같다.
Food Truck 전체 조회시 메뉴가 불 필요하지만 가져오게 된다.
다른 Entity들과 연관관계 매핑이 추가 된다면, Food Truck가 책임이 무거워진다.
또한 지금 JPA Entity와 Domain Entity(비즈니스적 처리를 위함)를 같이 사용하고 있어서 두 Entity간에 변환을 Mapper 클래스로 사용하는데 Food Truck의 Mapper 클래스가 집중화가 된다.
class FoodTruckMapper(
private val userMapper: UserMapper,
private val menuMapper: MenuMapper,
private val reviewMapper: ReviewMapper,
private val ...Mapper: ...Mapper, // 연관관계가 있는 모든 Entity Mapper
) {
fun mapToDomainEntity(foodTruckJpaEntity: FoodTruckJpaEntity): FoodTruck = FoodTruck(
id = foodTruckJpaEntity.id,
name = foodTruckJpaEntity.name,
foodType = foodTruckJpaEntity.foodType,
operatingStatus = foodTruckJpaEntity.operatingStatus,
starRating = foodTruckJpaEntity.starRating,
reviewCount = foodTruckJpaEntity.reviewCount,
user = userMapper.mapToDomainEntity(foodTruckJpaEntity.userJpaEntity),
menu = menuMapper.mapToDomainEntity(foodTruckJpaEntity.menuJpaEntity),
review = reviewMapper.mapToDomainEntity(foodTruckJpaEntity.reviewJpaEntity),
... = ...Mapper.mapToDomainEntity(foodTruckJpaEntity....JpaEntity),
)
fun mapToJpaEntity(foodTruck: FoodTruck): FoodTruckJpaEntity = FoodTruckJpaEntity(
name = foodTruck.name,
foodType = foodTruck.foodType,
userJpaEntity = userMapper.mapToJpaEntity(foodTruck.user),
menuJpaEntity = menuMapper.mapToJpaEntity(foodTruck.menu),
reviewJpaEntity = reviewMapper.mapToJpaEntity(foodTruck.review),
...JpaEntity = ...Mapper.mapToJpaEntity(foodTruck....),
)
}
단방향 예시 (MenuJpaEntity
)
@Entity
@Table(name = "food_trucks")
class FoodTruckJpaEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null
)
@Entity
@Table(name = "menus")
class MenuJpaEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@ManyToOne
@JoinColumn(name = "food_truck_id")
val foodTruck: FoodTruckJpaEntity
)
Food Truck에 대한 책임이 감소하였다. → 책임의 대한 분산
JPA Entity와 Domain Entity의 변환 Mapper 클래스의 집중 분리
class FoodTruckMapper(
private val userMapper: UserMapper
) {
fun mapToDomainEntity(foodTruckJpaEntity: FoodTruckJpaEntity): FoodTruck = FoodTruck(
id = foodTruckJpaEntity.id,
name = foodTruckJpaEntity.name,
foodType = foodTruckJpaEntity.foodType,
operatingStatus = foodTruckJpaEntity.operatingStatus,
starRating = foodTruckJpaEntity.starRating,
reviewCount = foodTruckJpaEntity.reviewCount,
user = userMapper.mapToDomainEntity(foodTruckJpaEntity.userJpaEntity)
)
fun mapToJpaEntity(foodTruck: FoodTruck): FoodTruckJpaEntity = FoodTruckJpaEntity(
name = foodTruck.name,
foodType = foodTruck.foodType,
userJpaEntity = userMapper.mapToJpaEntity(foodTruck.user)
)
}
Menu를 추가했을 때 독립적인 관리가 가능하다. → 확장성을 고려했을 때 장점
양방향 예시
@Entity
@Table(name = "food_trucks")
class FoodTruckJpaEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@OneToMany(mappedBy = "foodTruck", cascade = [CascadeType.ALL], orphanRemoval = true)
val menus: List<MenuJpaEntity> = mutableListOf()
)
@Entity
@Table(name = "menus")
class MenuJpaEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@ManyToOne
@JoinColumn(name = "food_truck_id")
val foodTruck: FoodTruckJpaEntity
)
@Entity
@Table(name = "food_trucks")
class FoodTruckJpaEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
var name: String,
@Enumerated(EnumType.STRING)
var foodType: FoodType,
var operatingStatus: Boolean = false,
)
@Entity
@Table(name = "menus")
class MenuJpaEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
var name: String,
var description: String,
var price: Float,
var foodTruckId: Long
)
Menu
가 FoodTruck
에 의존적이지만, FoodTruck
은 Menu
의 존재를 명시적으로 알 필요가 없어, 양쪽 엔티티 간 결합도를 낮출 수 있습니다. → 의존성 최소화Menu
는 FoodTruck
이 있어야만 존재할 수 있으므로, FoodTruckId
를 통해 직접적인 연결을 유지함으로써 데이터의 일관성을 보장할 수 있습니다. → 데이터 일관성@Test
fun test() {
val foodTruckId = 1L
val foodTruck = FoodTruck(id = foodTruckId, name = "Test Truck")
val menu = Menu(id = 1L, name = "Deluxe Burger", price = 9.99, foodTruck = foodTruck)
foodTruck.menus.add(menu)
// 연관관계로 인해 FoodTruck 저장 시 Menu도 함께 저장됨
when(foodTruckRepository.findById(foodTruckId)).thenReturn(Optional.of(foodTruck))
val foundMenus = foodTruckService.findMenusByFoodTruck(foodTruckId)
verify(foodTruckRepository).findById(foodTruckId)
assertTrue(foundMenus.isNotEmpty())
assertEquals("Deluxe Burger", foundMenus.first().name)
}
@Test
fun test() {
val foodTruckId = 1L
val menus = listOf(Menu(id = 1L, name = "Deluxe Burger", price = 9.99, foodTruckId = foodTruckId))
when(menuRepository.findByFoodTruckId(foodTruckId)).thenReturn(menus)
val foundMenus = foodTruckService.findMenusByFoodTruck(foodTruckId)
verify(menuRepository).findByFoodTruckId(foodTruckId)
assertTrue(foundMenus.isNotEmpty())
assertEquals("Deluxe Burger", foundMenus.first().name)
}
FoodTruck
과 Menu
의 데이터 준비와 검증이 더 독립적으로 이루어진다. 이는 테스트 간 격리를 쉽게 유지할 수 있게 해준다.Menu
조회를 위해 FoodTruck
엔티티의 실제 인스턴스 생성이 필요 없습니다. 이는 테스트 데이터 준비를 간소화하고, 테스트 실행 속도를 개선할 수 있다.FoodTruck
과 Menu
사이의 결합도를 낮춘다. 이로 인해 각 엔티티의 변경이 다른 엔티티의 테스트에 미치는 영향을 줄일 수 있으며, 시스템의 전반적인 유연성이 증가한다.MenuRepository
의 findByFoodTruckId
메소드만 모킹하면 되기 때문에, 복잡한 연관관계를 설정할 필요가 없다.