[JPA] JPA에서 복합키 사용하는 방법
복합키는 두 개 이상의 필드가 합쳐져서 하나의 고유한 키를 형성할 때 사용됩니다. 먼저 복합키를 사용하는 이유를 알아보고 JPA에서 이를 구현하기 위한 두 가지 주요 방법인 @ Embeddable와 @IdClass에 대해 알아보겠습니다.
복합키를 사용하는 이유
복합 키(Composite Key)를 사용하는 주된 이유는 데이터 모델링의 필요성과 특정한 기술적 요구에 기인합니다. 복합 키를 사용하는 주요 이유들은 다음과 같습니다:
- 고유성 보장: 복합 키는 두 개 이상의 필드를 조합하여 레코드의 고유성을 보장합니다. 단일 필드만으로는 고유성을 충분히 보장할 수 없는 경우에 이 방법이 유용합니다.
- 도메인 모델 반영: 복합 키는 현실 세계의 관계를 데이터베이스 모델에 더 자연스럽게 매핑할 수 있게 해 줍니다. 예를 들어, '학생'과 '강좌' 엔티티가 있을 때, 각 학생이 수강하는 각각의 강좌를 나타내는 레코드는 학생 ID와 강좌 ID의 조합으로 고유하게 식별될 수 있습니다.
- 관계 표현: 복합 키는 다대다(N:M) 관계를 효과적으로 표현하는 데 자주 사용됩니다. 예를 들어, 사용자 간의 '팔로우' 관계를 표현할 때, 팔로워 ID와 팔로잉 ID의 조합을 통해 각각의 팔로우 관계를 고유하게 식별할 수 있습니다.
- 성능 최적화: 특정 경우에 복합 키는 데이터 조회와 같은 작업의 성능을 최적화할 수 있습니다. 특히, 인덱싱 전략과 함께 사용될 때, 데이터베이스 쿼리의 효율성을 높일 수 있습니다.
- 레거시 시스템 호환성: 기존의 레거시 시스템이나 외부 시스템과의 통합 과정에서 복합 키가 필요할 수 있습니다. 이러한 시스템들은 이미 복합 키를 사용하고 있을 수 있으며, 이에 맞추어 데이터 모델을 설계할 필요가 있을 수 있습니다.
- 데이터 무결성: 복합 키는 관련된 여러 엔티티 간의 데이터 무결성을 유지하는 데 도움이 됩니다. 각 키 구성 요소는 다른 엔티티에 대한 외래 키(Foreign Key) 역할을 할 수 있으며, 이를 통해 데이터 간의 일관성을 유지할 수 있습니다.
복합 키의 사용은 경우에 따라 데이터 모델의 복잡성을 증가시킬 수 있으므로, 설계 단계에서 이의 필요성과 장단점을 충분히 고려하는 것이 중요합니다. 특히, JPA와 같은 ORM(Object-Relational Mapping) 툴을 사용할 때는 복합 키의 구현과 관리가 더 복잡해질 수 있으므로, 이를 고려한 설계가 필요합니다.
@ Embeddable
JPA에서 @Embeddable을 사용하여 복합 키를 구현하는 방법은 여러 단계로 구성됩니다. 이 방법은 복합 키를 나타내는 클래스를 만들고, 이를 엔티티 클래스에 내장(embed)하는 것을 포함합니다. 다음은 이 과정을 자세히 설명한 것입니다:
1단계: 복합 키 클래스 생성 (@Embeddable 클래스)
복합 키를 나타내는 클래스를 만들기 위해 @Embeddable 어노테이션을 사용합니다. 이 클래스는 Serializable 인터페이스를 구현해야 하며, 복합 키를 구성하는 필드들을 포함합니다. 또한, equals()와 hashCode() 메서드를 오버라이드하여 키의 동등성 비교를 적절하게 처리해야 합니다.
예시 (Kotlin)
import jakarta.persistence.Embeddable
import java.io.Serializable
@Embeddable
data class FollowId(
val followerId: Long,
val followingId: Long
): Serializable {
constructor(): this(0, 0)
}
** 코틀린의 data class를 사용해서 equals()와 hashCode() 메서드를 자동 생성하였습니다.
** java로 작성할 경우 Lombok을 사용하거나 직접 구현해야 합니다.
2단계: 엔티티 클래스 내에 복합 키 클래스 내장
엔티티 클래스에서 @EmbeddedId 어노테이션을 사용하여 복합 키 클래스를 내장합니다. 이 필드는 엔티티의 기본 키(primary key)를 나타냅니다.
예시 (Kotlin)
import jakarta.persistence.EmbeddedId
import jakarta.persistence.Entity
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import java.time.LocalDateTime
@Entity
data class Follow(
@EmbeddedId
val followId: FollowId,
@CreatedDate
val createdAt: LocalDateTime = LocalDateTime.now(),
@LastModifiedDate
val updatedAt: LocalDateTime = LocalDateTime.now()
)
고려 사항
- 동등성 처리: equals()와 hashCode() 메서드는 엔티티의 동등성을 올바르게 처리하기 위해 필수적입니다. JPA는 엔티티의 동등성을 이 메서드를 통해 결정합니다.
- 성능 최적화: 복합 키를 사용할 때는 데이터베이스 조회 성능을 고려해야 합니다. 필요에 따라 적절한 인덱싱을 고려하는 것이 좋습니다.
- 생성자: 복합 키 클래스에는 기본 생성자와 필드를 초기화하는 생성자를 제공하는 것이 좋습니다.
- 접근 방식: 클래스 필드에 직접 접근하는 대신 getter와 setter를 사용하는 것이 좋습니다. 이는 JPA 프로바이더가 프록시 등을 통해 엔티티를 관리하는데 도움을 줍니다.
@Embeddable을 사용하는 방식은 복합 키를 하나의 클래스로 캡슐화하여 관리할 수 있다는 장점이 있습니다. 이는 코드의 가독성과 유지보수성을 향상하는 데 도움이 됩니다.
@IdClass
JPA에서 @Embeddable을 사용하여 복합 키를 구현하는 방법은 여러 단계로 구성됩니다. 이 방법은 복합 키를 나타내는 클래스를 만들고, 이를 엔티티 클래스에 내장(embed)하는 것을 포함합니다. 다음은 이 과정을 자세히 설명한 것입니다:
1단계: 복합 키 클래스 생성 (@Embeddable 클래스)
복합 키를 나타내는 클래스를 만들기 위해 @Embeddable 어노테이션을 사용합니다. 이 클래스는 Serializable 인터페이스를 구현해야 하며, 복합 키를 구성하는 필드들을 포함합니다. 또한, equals()와 hashCode() 메서드를 오버라이드하여 키의 동등성 비교를 적절하게 처리해야 합니다.
예시 (Kotlin)
import java.io.Serializable
data class FollowId(
val followerId: Long,
val followingId: Long
): Serializable {
constructor(): this(0, 0)
}
** 코틀린의 data class를 사용해서 equals()와 hashCode() 메서드를 자동 생성하였습니다.
** java로 작성할 경우 Lombok을 사용하거나 직접 구현해야 합니다.
2단계: 엔티티 클래스 내에 복합 키 필드 정의
엔티티 클래스에서 @Id 어노테이션을 사용하여 복합 키를 구성하는 각 필드를 정의합니다. 그리고 클래스 레벨에서 @IdClass 어노테이션을 사용하여 복합 키 클래스를 지정합니다.
예시 (Kotlin)
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.IdClass
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import java.time.LocalDateTime
@IdClass(FollowId::class)
@Entity
data class Follow(
@Id
val followerId: Long,
@Id
val followingId: Long,
@CreatedDate
val createdAt: LocalDateTime = LocalDateTime.now(),
@LastModifiedDate
val updatedAt: LocalDateTime = LocalDateTime.now()
)
고려 사항
- 동등성 처리: equals()와 hashCode() 메서드는 엔티티의 동등성을 올바르게 처리하기 위해 필수적입니다.
- 생성자: 복합 키 클래스에는 기본 생성자와 필드를 초기화하는 생성자를 제공하는 것이 좋습니다.
- 접근 방식: 클래스 필드에 직접 접근하는 대신 getter와 setter를 사용하는 것이 좋습니다.
- 성능 최적화: 데이터베이스 조회 성능을 고려하여 적절한 인덱싱을 고려하는 것이 좋습니다.
@IdClass 방식은 복합 키를 구성하는 필드를 엔티티 내에 직접 정의하므로, 엔티티 클래스의 구조가 좀 더 명확하고 직관적일 수 있습니다. 하지만, 복합 키를 별도의 클래스로 분리하여 관리하지 않기 때문에, 이 키를 다른 엔티티에서 재사용하기 어려울 수 있습니다.
@EmbeddedId와 @IdClass, 사이의 주요 차이점
JPA에서 복합 키를 구현하는 두 가지 주요 방법, @EmbeddedId와 @IdClass, 사이의 주요 차이점은 다음과 같습니다:
@EmbeddedId
- 캡슐화: 복합 키가 하나의 클래스(@Embeddable 클래스)로 캡슐화되어 있어, 키를 구성하는 필드들이 하나의 단위로 그룹화됩니다.
- 엔티티 내부 표현: 복합 키는 엔티티 내에서 단일 객체로 표현되며, @EmbeddedId 어노테이션으로 표시됩니다.
- 재사용성: 같은 복합 키를 다른 엔티티에서 재사용하기 용이합니다.
- 복잡도: @Embeddable 클래스의 추가로 인해 약간의 복잡도가 증가하지만, 복잡한 관계를 명확하게 표현할 수 있습니다.
@IdClass
- 직접적인 필드 정의: 복합 키를 구성하는 필드들이 엔티티 클래스 내부에 직접 정의됩니다.
- 클래스 레벨 참조: @IdClass 어노테이션은 엔티티 클래스 레벨에서 사용되며, 복합 키를 구성하는 필드들의 클래스를 지정합니다.
- 재사용성의 제한: 복합 키를 다른 엔티티에서 재사용하기가 더 어려워집니다, 각 필드가 개별적으로 엔티티 내에 정의되기 때문입니다.
- 간결함: 별도의 @Embeddable 클래스가 필요 없어 구현이 좀 더 간단하고 직관적일 수 있습니다.
공통점과 선택 기준
두 방법 모두 복합 키를 정의하고 사용하는 데 필요한 기능을 제공하지만, 구현 방식과 표현 방식에 차이가 있습니다. @EmbeddedId는 복합 키를 하나의 단위로 취급하고, @IdClass는 복합 키를 엔티티의 일부로 더 직접적으로 표현합니다.
선택 기준은 주로 개발자의 선호도, 프로젝트의 요구 사항, 그리고 복합 키의 재사용성 필요성에 따라 달라질 수 있습니다. 복잡한 도메인 모델이나 재사용성이 중요한 경우 @EmbeddedId를 선택하는 것이 좋을 수 있고, 구현의 간결함을 선호한다면 @IdClass가 더 나은 선택일 수 있습니다.