
개발을 진행하며 DB Table 구조를 설계할 때 깊은 고민을 하지 않고 양방향과 연관성이 보이면 무조건 외래키를 걸어서 구현한 결과, 규모가 커질수록 미친듯한 불편함과 문제점이 발생해서 연관관계 매핑에 대한 학습을 진행하고 정리한다.
OneToMany와 OneToOne 관계는 성능상 이슈를 야기할 수 있어 되도록 사용하지 않는다.
특히 OneToMany의 경우 N+1 문제나 추가적인 UPDATE 쿼리가 발생할 가능성이 높다.
양방향 매핑은 순환 참조 문제와 객체 관계의 복잡성을 증가시킨다. 단방향 매핑으로도 충분히 비즈니스 요구사항을 만족할 수 있다.
라이프사이클이 완전히 동일한 경우에만 연관관계 매핑을 고려한다.
이마저도 신중하게 검토한 후 단방향 ManyToOne 관계만을 적용한다.
- 리뷰는 무조건 리뷰 이미지를 가져야 하는가? → X
- 리뷰를 호출할 때 무조건 이미지도 있어야 하는가? → X
- 리뷰 이미지는 리뷰가 있어야 하는가? → O
이 분석을 바탕으로 ReviewImageEntity에서 ReviewEntity로 향하는 단방향 ManyToOne 관계만 설정한다.
@Entity
@Table(name = "review_image")
class ReviewImageEntity(
val reviewId: Long,// ManyToOne 관계를 위한 외래키
private var imageUrl: String,
private var order: Int
) : BaseEntity()
ReviewEntity에서는 ReviewImage에 대한 직접적인 참조를 두지 않는다. 필요시 별도 쿼리로 조회한다.
@Entity
@Table(name = "qna_question")
class QuestionEntity(
val providerId: Long,
val userKey: String,
val targetKey: String,
private var content: String,
private var imageUrlJson: String,
private var providerCreatedAt: LocalDateTime,
private var providerUpdatedAt: LocalDateTime
) : BaseEntity() {
constructor(
provider: Provider,
target: UserWithTargetKey,
content: QuestionContent,
dateTime: DefaultDateTime,
) : this(
providerId = provider.id,
userKey = target.userKey,
targetKey = target.targetKey,
content = content.content,
imageUrlJson = JsonUtil.to(content.imageUrls),
providerCreatedAt = dateTime.createdAt,
providerUpdatedAt = dateTime.updatedAt,
)
fun toQnaQuestion(): Question {
return Question(
// 도메인 객체 변환 로직
)
}
}
@Entity
@Table(name = "qna_answer")
class AnswerEntity(
val providerId: Long,
val userKey: String,
val questionId: Long,// Question에 대한 외래키
private var content: String,
private var imageUrlJson: String,
private var providerCreatedAt: LocalDateTime,
private var providerUpdatedAt: LocalDateTime
) : BaseEntity() {
constructor(
provider: Provider,
userKey: String,
questionId: Long,
content: QuestionContent,
dateTime: DefaultDateTime,
) : this(
providerId = provider.id,
userKey = userKey,
questionId = questionId,
content = content.content,
imageUrlJson = JsonUtil.to(content.imageUrls),
providerCreatedAt = dateTime.createdAt,
providerUpdatedAt = dateTime.updatedAt,
)
fun toQnaAnswer(): Answer {
return Answer(
id!!,
// 도메인 객체 변환 로직
)
}
}
Question이 있다고 해서 무조건 Answer가 존재하는 것은 아니다.
질문에 대한 답변은 다음과 같은 특성을 갖는다:
따라서 AnswerEntity에서는 questionId를 통해 Question을 참조하지만, QuestionEntity에서는 Answer에 대한 직접적인 참조를 두지 않는다.
Entity는 JPA의 프록시 객체 생성과 상속 구조를 고려하여 일반 class로 선언한다.
Entity의 기본 생성자 외에 도메인 객체를 받는 보조 생성자를 제공하여 객체 생성의 편의성을 높인다.
JPA 연관관계 매핑은 신중하게 접근해야 한다. 무분별한 양방향 매핑이나 OneToMany 관계는 성능 문제를 야기할 수 있다. 비즈니스 요구사항을 정확히 분석하고, 정말 필요한 경우에만 단방향 ManyToOne 관계를 설정하는 것이 바람직하다. 대부분의 경우 외래키를 통한 별도 조회로도 충분히 요구사항을 만족할 수 있다.