개발/Spring Boot

JPA 복합키 @IdClass vs @EmbeddedId

registry 2023. 7. 24. 00:17

JPA에서 복합키(Composite Key)를 매핑하는 방법은 크게 두 가지가 있다.

  • @IdClass
  • @EmbeddedId

실제 서비스 코드 기준에서의 선택 기준을 정리해봤다.


1. @IdClass 방식

@IdClass는 엔티티 외부에 PK 클래스를 정의하고, 엔티티의 각 필드에 @Id를 붙여 매핑하는 방식이다.

예시:

@Entity  
@IdClass(MyId::class)  
class MyEntity(  
@Id  
val id1: Long,
@Id  
val id2: Long,  

val name: String  
)

data class MyId(  
val id1: Long,  
val id2: Long  
) : Serializable

 

특징:

  • PK 클래스는 Serializable 구현 필수
  • 엔티티 필드에 식별자가 그대로 노출됨

JPQL 예시는 다음과 같다.

SELECT m FROM MyEntity m
WHERE m.id1 = :id1 AND m.id2 = :id2

2. @EmbeddedId 방식

@EmbeddedId는 복합키를 하나의 값 객체로 캡슐화해서 사용하는 방식이다.

예시:

@Entity  
class MyEntity(  
@EmbeddedId  
val id: MyId,
val name: String  
)

@Embeddable  
data class MyId(  
val id1: Long,  
val id2: Long  
) : Serializable

JPQL에서는 다음과 같이 접근한다.

SELECT m FROM MyEntity m
WHERE m.id.id1 = :id1 AND m.id.id2 = :id2

3. 가장 중요한 차이: 설계 관점

구분: @IdClass

  • 모델링 관점: 테이블 구조 중심
  • 캡슐화: 없음
  • 불변성 표현: 약함
  • 연관관계 매핑: 복잡
  • 유지보수성: 보통

구분: @EmbeddedId

  • 모델링 관점: 도메인 모델 중심
  • 캡슐화: 있음 (값 객체)
  • 불변성 표현: 강함
  • 연관관계 매핑: 단순
  • 유지보수성: 높음

정리하면:

  • @IdClass → “컬럼 묶음”
  • @EmbeddedId → “의미 있는 식별자 값 객체”

이 차이가 실무에서 꽤 크게 체감된다.


4. 연관관계가 들어가는 순간 차이가 벌어진다

복합키는 대부분 이런 형태다.

  • Order + Product → OrderItem
  • User + Role → UserRole
  • User + Target → Like

이때 연관관계를 매핑해보면 차이가 극명해진다.

@IdClass 방식:

@ManyToOne
@JoinColumn(name = "order_id", insertable = false, updatable = false)
lateinit var order: Order

  • insertable = false
  • updatable = false
  • 컬럼 중복 관리
  • 실수 여지 많음

@EmbeddedId + @MapsId 방식:

@MapsId("orderId")
@ManyToOne
@JoinColumn(name = "order_id")
lateinit var order: Order

  • 구조적으로 자연스럽다
  • 컬럼 중복 없음
  • 실수 포인트 적음

실무에서 @EmbeddedId를 더 선호하는 가장 큰 이유가 이 지점이다.

 


5. QueryDSL / Spring Data에서의 체감 차이

JPQL만 보면 별 차이 없어 보이지만, 실제 실무에서는 여기서 차이가 난다.

@IdClass:

findById1AndId2(id1, id2)

@EmbeddedId:

findById_Id1AndId_Id2(id1, id2)

  • 메서드 네이밍 가독성
  • QueryDSL path 표현
  • DTO projection 작성 난이도

이 부분에서 팀 생산성 차이가 실제로 발생한다.


6. “복합키는 DB 개념”이라는 오해

복합키는 DB에서 출발한 개념이 맞다.
하지만 도메인 모델에서도 충분히 의미 있는 개념이다.

대표적인 예:

  • OrderItem(OrderId, ProductId)
  • UserRole(UserId, Role)
  • Like(UserId, TargetId)

그래서 JPA가 @EmbeddedId를 제공하는 것이고,
이건 단순히 DB를 흉내 내기 위한 기능이 아니다.


7. 그래서 무엇을 선택해야 할까?

정리하면 이렇게 나뉜다.

@IdClass가 어울리는 경우:

  • 레거시 스키마를 그대로 따라야 할 때
  • 복합키를 단순 컬럼 묶음으로 취급하고 싶을 때
  • 도메인 모델에 식별자 객체를 만들고 싶지 않을 때
  • QueryDSL path 단순함을 중시할 때

@EmbeddedId가 어울리는 경우:

  • 도메인 모델링을 중요하게 보는 경우
  • 연관관계 매핑이 들어가는 경우
  • 복합키 자체가 의미 있는 개념인 경우
  • 불변성, 안정성을 중요하게 보는 경우

10. 나는 왜 @IdClass를 선택했나

나는 @IdClass를 선택했다.

이유는 다음과 같다.

  • 기존 테이블 구조를 그대로 유지해고 싶었고
  • 복합키를 도메인 개념으로 끌어올릴 필요가 없었고
  • JPQL 가독성과 DTO projection 편의성을 더 중요하게 봤기 때문이다.

하지만 연관관계가 많아지거나,
도메인 모델링 비중이 커진다면 @EmbeddedId로 갔을 것이다.


결론

@IdClass와 @EmbeddedId는 우열의 문제가 아니다.

  • DB 중심 설계 → @IdClass
  • 도메인 중심 설계 → @EmbeddedId

중요한 건 느낌이 아니라 설계 기준이다.

  • 연관관계가 있는가?
  • 복합키에 의미가 있는가?
  • 변경 가능성을 구조적으로 막아야 하는가?
  • 팀이 어떤 스타일을 선호하는가? (중요한듯)

이 기준으로 선택하면 된다.


한 줄 요약

컬럼 묶음이면 @IdClass,
의미 있는 식별자면 @EmbeddedId.