Spring & JPA

고급 매핑 - 3. 복합 키와 식별 관계 매핑

진진리 2025. 3. 19. 21:08

식별 관계 vs 비식별 관계

  • 외래 키가 기본 키에 포함되는지 여부에 따라 식별 관계와 비식별 관계로 구분

식별 관계

부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계

 

비식별 관계

부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계

  • 필수적 비식별 관계: 외래 키에 NULL을 허용하지 않는다.
  • 선택적 비식별 관계: 외래 키에 NULL을 허용한다.

필수적 비식별 관계
선택적 비식별 관계

 


복합 키: 비식별 관계 매핑

  • 둘 이상의 컬럼으로 구성된 복합 기본 키는 매핑 오류가 발생한다.
  • JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자 클래스를 만들어야 한다.
    • JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용한다. 그리고 구분을 위해 equals와 hashCode를 사용해서 동등성 비교를 한다.
    • 식별자 필드가 2개 이상이면 별도의 식별자 클래스를 만들고 그곳에 equals와 hashCode를 구현해야 한다.
  • JPA는 복합 키를 지원하기 위해 @IdClass와 @EmbeddedId 2가지 방법을 제공한다.
    • @IdClass는 관계형 데이터베이스에 가까운 방법이고 @EmbeddedId는 객체지향에 가까운 방법이다.

 

@IdClass

@Entity
@IdClass(ParentId.class)
public class Parent {
	
    @Id
    @Column(name = "PARENT_ID1")
    private String id1;
    
    @Id
    @Column(name = "PARENT_ID2")
    private String id2;
    
    private String name;
}

public class ParentId implements Serializable {
	
    private String id1;
    private String id2;
    
    public ParentId() {}
    
    public ParentId(String id1, String id2) {
        this.id1 = id1;
        this.id2 = id2;
    }
    
    @Override
    public boolean equals(Object o) { ... }
    
    @Override
    public int hashCode() { ... }
}

 

  • 식별자 클래스는 다음 조건을 만족해야 한다.
    • 엔티티에서 사용하는 속성명과 속성명이 같아야 한다.
    • Serializable 인터페이스를 구현해야 한다.
    • 기본 생성자가  있어야 한다.
    • 식별자 클래스는 public이어야 한다.
  • 자식 클래스
    • 외래 키가 복합 키이므로 @JoinColumns 어노테이션을 사용한다.
    • @JoinColumn의 name 속성과 referencedColumnName 속성의 값이 같이면 후자는 생략 가능하다.
@Entity
public class Child {
	
    @Id
    private String id;
    
    @ManyToOne
    @JoiinColumns({
        @JoinColumn(name = "PARENT_ID1", referencedColumnName = "PARENT_ID1"),
        @JoinColumn(name = "PARENT_ID2", referencedColumnName = "PARENT_ID2")
    })
    private Parent parent;
}

 

 

@EmbeddedId

@Entity
public class Parent {
	
    @EmbeddedId
    private ParentId id;
    
    private String name;
    
    // ...
}

@Embeddable
public class ParentId implements Serializable {
	
    @Column(name = "PARENT_ID1")
    private String id1;
    @Column(name = "PARENT_ID2")
    private String id2;
    
    
    @Override
    public boolean equals(Object o) { ... }
    
    @Override
    public int hashCode() { ... }
}
  • 식별자 클래스
    • @IdClass와 달리 식별자 클래스에 기본 키를 직접 매핑한다.
    • 다음 조건을 만족해야 한다.
      • @Embeddable 어노테이션을 붙여야 한다.
      • Serializable 인터페이스 구현해야 한다.
      • equals, hashCode 구현해야 한다.
      • 기본 생성자가 있어야 한다.
      • 식별자 클래스는 public이어야 한다.

 

@IdClass vs @EmbeddedId

@EmbeddedId가 중복이 없어 더 좋아보이긴 하지만 특정 상황에 JPQL이 조금 더 길어질 수 있다.

em.createQuery("select p.id.id1, p.id.id2 from Parent p"); //@EmbeddedId
em.createQuery("select p.id1, p.id2 from Parent p"); //@IdClass

 

+ 복합 키에는 @GeneratedValue를 사용할 수 없다.


복합 키: 식별 관계 매핑

 

@IdClass와 식별 관계

  • 부모
@Entity
public class Parent {
	
    @Id @Column(name = "PARENT_ID")
    private String id;
    private String name;
    
    // ...
}

 

  • 자식
@Entity
@IdClass(ChildId.class)
public class Child {
	
    @Id
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent parent;
    
    @Id @Column(name = "CHILD_ID")
    private String childId;
    
    private String name;
    
    // ...
}

public class ChildId implements Serializable {
	
    private String parent;
    private String childId;
    
    //equals, hashCode ...
}

 

  • 손자
@Entity
@IdClass(GrandChildId.class)
public class GrandChild{
	
    @Id
    @ManyToOne
    @JoinColumns({
        @JoinColumn(name = "PARENT_ID"),
        @JoinColumn(name = "CHILD_ID")
    })
    private Child child;
    
    @Id @Column(name = "GRANDCHILD_ID")
    private String id;
    
    private String name;
    
    // ...
}

public class GrandChildId implements Serializable {
	
    private ChildId child;
    private String id;
    
    //equals, hashCode ...
}

 

  • 식별 관계는 기본 키와 외래 키를 같이 매핑해야 한다. 따라서 @Id와 @ManyToOne을 같이 사용하면 된다.

 

@EmbeddedId와 식별 관계

@EmbeddedId로 식별 관계를 구성할 때는 @MapsId를 사용해야 한다.

  • 부모
@Entity
public class Parent {
	
    @Id @Column(name = "PARENT_ID")
    private String id;
    
    private String name;
    // ...
}

 

  • 자식
@Entity
public class Child {
	
    @Embeddedid
    private ChildId id;
    
    @MapsId("parentId")
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent parent;
    
    private String name;
    
    // ...
}

@Embeddable
public class ChildId implements Serializable {
	
    private String parentId;
    
    @Column(name = "CHILD_ID")
    private String id;
    
    // equals, hashCode ...
}

 

  • 손자
@Entity
public class GrandChild {
	
    @EmbeddedId
    private GrandChildId id;
    
    @MapsId("childId")
    @ManyToOne
    @JoinColumns({
        @JoinColumn(name = "PARENT_ID"), 
        @JoinColumn(name = "CHILD_ID")
    })
    private Child child;
    
    private String name;
    //...
}

@Embbedable
public class GrandChildId implements Serializable {
	
    private ChildId childId;
    
    @Column(name = "GRANDCHILD_ID")
    private String id;
    
    //equals, hashCode ...
}

 


일대일 식별 관계

  • 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만 사용한다.
  • 따라서 복합 키로 구성하지 않아도 된다.
@Entity
public class Board{
	
    @Id @GeneratedValue
    @Column(name = "BOARD_ID")
    private Long id;
    
    private String title;
    
    @OneToOne(mappedBy = "board")
    private BoardDetail boardDetail;
    
    //...
}

@Entity
public class BoardDetail {
	
    @Id
    private Long boardId;
    
    @MapsId
    @OneToOne
    @JoinColumn(name = "BOARD_ID")
    private Board board;
    
    private String content;
    
    //...
}

 

  • 식별자가 단순히 칼럼하나면 @MapsId를 사용하고 속성은 비워두면 된다.
  • 이때 @MapsId는 @Id를 사용해서 식별자로 지정한 BoardDetail.boardId와 매핑된다.

식별, 비식별 관계의 장단점

  • 데이터베이스 설계 관점에서 비식별 관계를 선호하는 이유
    • 식별 관계의 경우 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 칼럼이 점점 늘어난다.
      • 조인할 때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
    • 식별 관계는 2개 이상의 칼럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다.
    • 식별 관계를 사용할 때 기본 키로 비즈니스 의미가 있는 자연 키 칼럼을 조합하는 경우가 많다. 그러나 비즈니스 요구사항은 변하고 자식에게 전파되면 변경하기 힘들다.
      • 비식별 관계의 기본 키는 비즈니스와 관계없는 대리 키를 주로 사용한다.
    • 식별 관계는 비식별 관계보다 테이블 구조가 유연하지 못한다.
  • 객체 관계 매핑 관점에서 비식별 관계를 선호하는 이유
    • JPA에서 복합 키는 별도의 클래스를 만들어야 한다.
    • 비식별 관계의 기본 키는 주로 대리 키를 사용하므로 JPA는 @GeneratedValue처럼 대리 키를 생성하기 위한 편리한 방법을 제공한다.
  • 식별 관계가 가지는 장점
    • 기본 키 인덱스를 활용하기 좋고 특정 상황에 조인 없이 하위 테이블로 검색을 완료할 수 있다.
  • 추천하는 방법
    • 비식별 관계를 사용하고 기본 키는 Long 타입의 대리 키를 사용하는 것
    • 선택적 비식별 관계보다 필수적 비식별 관계를 사용하는 것이 좋다. 
      • NULL을 하용하면 조인 시 외부 조인을 해야 한다. NOT NULL이면 내부 조인만 사용해도 된다.

'Spring & JPA' 카테고리의 다른 글

[테코톡] JPA 연관관계 최적화  (0) 2025.04.03
[테코톡] API 중복 호출 해결기  (0) 2025.04.02
고급 매핑 - 2. @MappedSuperclass  (0) 2025.03.19
고급 매핑 - 1. 상속 관계 매핑  (0) 2025.03.19
연관관계  (0) 2025.03.17