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.
'개발 > Spring Boot' 카테고리의 다른 글
| MDC를 활용한 추적 가능한 로그 남기기 (1) | 2024.04.18 |
|---|---|
| Spring 테스트할 때 @Transactional 써야 할까 말아야 할까 (0) | 2023.07.30 |