자바 ORM 표준 JPA 프로그래밍 - (7) 고급 매핑 - 복합키

 Date: 2022-04-25

✓︎ 복합키와 식별 관계 매핑
    1) 식별 관계 vs 비식별 관계
    2) 복합키: 비식별 관계 매핑
    3) 복합키: 식별 관계 매핑
    4) 비식별 관계로 구현
    5) 일대일 식별 관계
    6) 식별, 비식별 관계의 장단점

복합키와 식별 관계 매핑

복합키는 두 개 이상의 컬럼을 조합하여 기본키 역할을 할 수 있게 만든 키를 뜻한다.
예를들어 동물원은 많지만 서울광진구에있는 동물원은 하나인것처럼 2개 이상의 기준이 되었을경우에 식별이 가능한 값들이 있다.

1) 식별 관계 vs 비식별 관계

데이터베이스 테이블 사이의 관계는 외래키가 기본키에 포함되는지 여부에 따라 식별 관계와 비식별 관계로 구분한다.

  • 식별 관계 : 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본키 + 외래키로 사용하는 관계
  • 비식별 관계 : 부모 테이블의 기본 키를 받아서 자식 테이블의 외래키로만 사용하는 관계
    • 비식별 관계는 외래키에 NULL을 허용하는지에 따라 필수적 비식별 관계와 선택적 비식별 관계로 구분한다.
      필수적 비식별 관계(Mendatory) : 외래키에 NULL을 허용하지 않는다. 연관관계를 필수적으로 맺어야 한다.
      선택적 비식별 관계(Optional) : 외래키에 NULL을 허용한다. 연관관계를 맺을지 선택할 수 있다.
  • 최근에는 비식별 관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 추세다.

2) 복합키: 비식별 관계 매핑

  • 기본키

    @Entity
    public class Hello {
        @Id
        private String id;
    }
    
  • 복합 기본키
    • 다음과 같이 사용하면 매핑 예외가 발생한다.
    • 복합키에는 @GenerateValue를 사용할 수 없다. 복합키를 구성하는 여러 컬럼 중 하나에도 사용할 수 없다.
    @Entity
    public class Hello {
        @Id
        private String id1;
        @Id
        private String id2;  // 실행 시점에 매핑 예외 발생
    }
    

    JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용한다. 그리고 식별자를 구분하기 위해 equals와 hashCode를 사용해서 동등성 비교를 한다. 그런데 식별자 필드가 2개 이상이면 별도의 식별자 클래스를 만들고 그곳에 equals와 hashCode를 구현해야 한다.

    JPA는 복합키를 지원하기 위해 @IdClass@EmbeddedId 2가지 방법을 제공한다.
    @IdClass가 데이터베이스에 맞춘 방법이라면 @EmbeddedId는 좀 더 객체지향적인 방법이다.

    1) @IdClass

    • 부모 클래스
      @Entity
      @IdClass(ParentId.class)
      public class Parent {
          @Id @Column(nema = "PATRENT_ID1")
          private String id1;  // 식별자 클래스의 ParentId.id1 과 연결
          @Id @Column(nema = "PATRENT_ID2")
          private String id2;  // 식별자 클래스의 ParentId.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 인터페이스를 구현해야 한다.
      - equals, hashCode를 구현해야 한다.
      - 기본 생성자가 있어야 한다.
      - 식별자 클래스는 public이어야 한다.

    • 자식 클래스
      public class Child {
          @Id
          private String id;
              
          @ManyToOne
          @JoinColumns({
              @JoinColumn(name = "PARENT_ID1", referencedColumnName = "PARERNT_ID1"),
              @JoinColumn(name = "PARENT_ID2", referencedColumnName = "PARERNT_ID2"),
          })
          private Parent parent;
      }
      

    2) @EmbeddedId

    • 부모 클래스
      @Entity
      public class Parent {
          @EmbeddedId
          private ParentId parentId;
          private String name;
          ...
      }
      
    • 식별자 클래스
      @Embeddable
      public class ParentId implements Serializable {
          @Column(name = "PARENT_ID1")
          private String id1;
          @Column(name = "PARENT_ID2")
          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() { ... }
      }
      
      • @IdClass와는 다르게 식별자 클래스에 기본키를 직접 매핑한다.

      식별자 클래스는 다음 조건을 만족해야 한다.
      - @Embeddable 어노테이션을 붙여주어야 한다.
      - Serializable 인터페이스를 구현해야 한다.
      - equals, hashCode를 구현해야 한다.
      - 기본 생성자가 있어야 한다.
      - 식별자 클래스는 public이어야 한다.

▪ 복합키와 equals(), hashCode()

복합키는 equals()와 hashCode()를 필수로 구현해야 한다.

ParentId id1 = new ParentId();
id1.setId1("myId1");
id1.setId2("myId2");

ParentId id2 = new ParentId();
id2.setId1("myId1");
id2.setId2("myId2");

id1.equals(id2) 는 참일까 거짓일까?
equals()를 적절히 오버라이딩했다면 참이겠지만 equals()를 적절히 오버라이딩 하지 않았다면 결과는 거짓이다. equals()는 인스턴스 참조값 비교인 == 비교(동일성 비교)를 하기 때문이다.

▪︎ @IdClass vs @EmbeddedId
  • @IdClass@EmbeddedId는 각각 장단점이 있으므로 취향에 맞는 것을 사용하면 된다.
  • @EmbeddedId@IdClass와 비교해서 더 객체지향적이고 중복도 없어서 좋아보이긴 하지만 특정 상황에 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

3) 복합키: 식별 관계 매핑

  • 식별 관계에서 자식 테이블은 부모 테이블의 기본 키를 포함해서 복합키를 구성해야 하므로 @IdClass@EmbeddedId를 사용해서 식별자를 매핑해야 한다.
  • 식별 관계는 기본키와 외래키를 같이 매핑해야 한다.
  • 식별자 매핑인 @Id와 연관관계 매핑인 @ManyToOne을 같이 사용하면 된다.

    @IdClass
    • 부모 클래스
      @Entity
      public class Parent {
          @Id @Column(name = "PARENT_ID")
          private ParentId 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;
          ...
      }
      
    • Child 식별자 클래스
      public class ChildId implements Serializable {
          private String parent;  // Child.parent 매핑
          private String childId;  // Child.childId 매핑
            
          // equals, hashCode
          ...
      }
      
    • 손자 클래스
      @Entity
      @IdClass(GrandChildId.class)
      public class GrandChild {
          @Id                                  // 기본키 매핑
          @ManyToOne                           // 외래키 매핑
          @JoinColumns({                       // 외래키 매핑
                  @JoinColumn(name = "PARENT_ID"),
                  @JoinColumn(name = "CHILD_ID")
          })
          public Child child;
            
          @Id @Column(name = "GRANDCHILD_ID")
          private String id;
            
          private String name;
          ...
      }
      
    • GrandChild 식별자 클래스
      public class GrandChildId implements Serializable {
          private ChildId child;  // GrandChild.child 매핑
          private String id;  // GrandChild.id 매핑
            
          // equals, hashCode
          ...
      }
      
    @EmbeddedId
    • @EmbeddedId로 식별 관계를 구성할 때는 @MapsId를 사용해야 한다.
    • @MapsId는 외래키와 매핑한 연관관계를 기본키에도 매핑하겠다는 뜻이다.
    • @MapsId의 속성값은 @EmbeddedId를 사용한 식별자 클래스의 기본키 필드를 지정하면 된다.

    • 부모 클래스
      @Entity
      public class Parent {
          @Id @Column(name = "PARENT_ID")
          private ParentId id;
          private String name;
          ...
      }
      
    • 자식 클래스
      @Entity
      public class Child {
          @EmbeddedId
          public ChildId id;
            
          @MapsId("parentId")  // ChildId.parentId 매핑
          @ManyToOne
          @JoinColumn(name = "PARENT_ID")
          public Parent parent;
            
          private String name;
          ...
      }
      
    • Child 식별자 클래스
      @Embeddable
      public class ChildId implements Serializable {
          private String parentId;  // @MapsId("parentId")로 매핑
          @Column(name = "CHILD_ID")
          private String id;
            
          // equals, hashCode
          ...
      }
      
    • 손자 클래스
      @Entity
      public class GrandChild {
          @EmbeddedId
          private GrandChildId id;
            
          @MapsId("childId")  // GrandChildId.childId 매핑
          @ManyToOne                    
          @JoinColumns({
                  @JoinColumn(name = "PARENT_ID"),
                  @JoinColumn(name = "CHILD_ID")
          })
          public Child child;
            
          private String name;
          ...
      }
      
    • GrandChild 식별자 클래스
      @Embeddable
      public class GrandChildId implements Serializable {
          private ChildId childId;  // @MapsId("childId")로 매핑
          @Column(name = "GRANDCHILD_ID")
          private String id;
            
          // equals, hashCode
          ...
      }
      

4) 비식별 관계로 구현

식별관계의 복합키 클래스를 만들지 않아도 되므로 매핑도 쉽고 코드도 단순하다.

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

// 자식
@Entity
public class Child {
	@Id @GeneratedValue
	@Column(name "CHILD_ID")
	private Long id;
	private String name;
	
	@ManyToOne
	@JoinColumn(name = "PARENT_ID")
	private Parent parent;
	...
}

// 손자
@Entity
public class GrandChild {
	@Id @GeneratedValue
	@Column(name = "GRANDCHILD_ID")
	private Long id;
	private String name;
	
	@ManyToOne
	@JoinColumn(name = "CHILD_ID")
	private Child child;
	...
}

5) 일대일 식별 관계

  • 일대일 식별 관계는 자식테이블의 기본키 값으로 부모 테이블의 기본키 값만 사용하면 된다.
  • 식별자가 단순히 컬럼 하나면 @MapsId를 사용하고 속성값은 비워두면 된다.
    • 이때 @MapsId@Id를 사용해서 식별자로 지정한 컬럼과 매핑된다.

        // 부모
        @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  // BoardDetail.boardId 매핑
            @OneToOne
            @JoinColumn(name = "BOARD_ID")
            private Board board;
              	
            private String content;
            ...
        }
      

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

✓ 데이터베이스 설계 관점
  • 데이터베이스 설계 관점에서 식별관계 보다 비식별 관계를 선호한다.
    • 식별 관계는 부모 테이블의 기본키를 자식 테이블로 전파하면서 자식 테이블의 기본키 컬럼이 점점 늘어난다.
    • 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본키를 만들어야 하는 경우가 많다.
    • 반면에 비식별 관계의 기본키는 비즈니스와 전혀 관계없는 대리키를 주로 사용한다.
✓ 객체 관계 매핑 관점
  • 객체 관계 매핑의 관점에서 비식별 관계를 선호한다.
    • 일대일 관계를 제외하고 식별 관계는 2개 이상의 컬럼을 묶은 복합 기본키를 사용한다.
    • JPA에서 복합키는 별도의 복합키 클래스를 만들어서 사용해야 한다. 컬럼이 하나인 기본키를 매핑하는 것보다 많은 노력이 필요하다.
    • 비식별 관계의 기본키는 주로 대리키를 사용하는데 JPA는 @GenerateValue처럼 대리키를 생성하기 위한 편리한 방법을 제공한다.
✓ 식별 관계의 장점
  • 기본키 인덱스를 활용하기 좋다.
  • 상위 테이블의 기본키 컬럼을 자식, 손자, 테이블들이 가지고 있으므로 특정 상황에 조인 없이 하위 테이블만으로 검색을 완료할 수 있다.
✓ ORM 신규 프로젝트 진행시 추천하는 방법
  • 될 수 있으면 비식별 관계를 사용하자.
  • 기본키는 Long 타입의 대리키를 사용하자.
    • 대리키는 비즈니스와 아무 관련이 없기 때문에 비즈니스가 변경되어도 유연한 대처가 가능하다.
    • JPA에서는 @GenerateValue를 통해 간편하게 대리키를 생성할 수 있다.
  • 선택적 비식별 관계보다는 필수적 비식별 관계를 사용하는 것이 좋다.
    • 선택적인 비식별 관계는 NULL을 허용하므로 조인할 때에 외부 조인을 사용해야 한다.
    • 반면에 필수적 비식별 관계는 NOT NULL로 항상 관계가 있다는 것을 보장하므로 내부 조인만 사용해도 된다.