미운 오리 새끼의 우아한 개발자되기

[JPA] 고급 매핑 (3) 복합 키 본문

Spring & Spring Boot

[JPA] 고급 매핑 (3) 복합 키

Serina_Heo 2023. 5. 9. 12:29

1. 식별 관계 vs. 비식별 관계

- 식별 관계

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

 

- 비식별 관계

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

비식별 관계는 외래 키에 NULL 을 허용하는 지에 따라 필수적 비식별 관계와 선택적 비식별 관계로 나뉨.

  • 필수적 비식별 관계(Mandatory): 외래 키에 NULL 허용 x. 연관관계가 필수적으로 맺어져야함.
  • 선택적 비식별 관계(Optional): 외래 키에 NULL 을 허용. 연관관계를 맺을 지 말지 선택가능.

최근에는 비식별관계를 주로 사용하고, 꼭 필요한 곳에만 식별 관계를 사용하는 추세.

 

1.2. 복합 키: 비식별 관계 매핑

JPA 에서 식별자를 둘 이상 사용하기 위해서는 별도의 식별자 클래스를 만들어야함.

@Entity
public class Hello {
    
    @Id
    private String id;
    @Id
    private String id2; // 실행 시점에 매핑 예외 발생

}

JPA 는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용하기 때문에 식별자를 구분하기 위해 equals 와 hashCode 를 사용해서 동등성 비교를 해야한다. 

 

JPA 에서는 복합 키를 지원하기 위해 @IdClass 와 @EmbeddedId 이 두 가지 방법을 제공하는데 
@IdClass 가 관계형 DB 에 가까운 방법ㅂ이고 @EmbeddedId 는 좀 더 객체지향에 가까운 방법이다.

 

@IdClass

@Entity
@IdClass(ParentId.class) // 복합키 클래스
public class Parent {

    @Id
    @Column(name = "PARENT_ID1")
    private String id1; // ParentId.id1 과 연결
    
    @Id
    @Column(name = "PARENT_ID2")
    private String id2; // ParentId.id2 와 연결
    
    private String name;
    ...
}
public class ParentId implements Serialize {

    private String id1; // Parent.id1 매핑
    private Stirng id2; // Parent.id2 매핑
    
    public ParentId(String id1, String id2) {
    	this.id1 = id1;
        this.id2 = id2;
    }
    
    @Override
    public boolean equals(Object c) { ... }
    
    @Override
    public int hashCode() { ... }

}

 

@IdClass 사용 시 만족해야할 조건

  • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 동일해야 함
  • Serializable 인터페이스를 구현해야 함
  • equals, hashCode 구현
  • 기본 생성자 필요
  • 식별자 클래스는 public
@Entity
public class Child {

    @Id
    private String id;
    
    @ManyToOne
    @JoinColumns({
    	@JoinColumn(name = "PARENT_ID1",
        	referenceColumnName = "PARENT_ID1"),
        @JoinColumn(name = "PARENT_ID2",
        	referenceColumnName = "PARENT_ID2")
    })
	private Parent parent;
    
    ...
}

아래는 실제로 사용하는 예시 코드

// 복합 키를 사용하는 엔티티의 저장 예시코드
Parent parent = new Parent();
parent.setId1("myId1");
parent.setId2("myId2");
parent.setName("parentName");
em.persist(parent);

// 엔티티 조회
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);

 

@EmbeddedId

@IdClass 에 비해 @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;
    
    // equals and hashCode 구현
    ...

}

 

@EmbeddedId 를 적용하는 식별자 클래스의 조건

  • 식별자 클래스에 기본 키를 직접 매핑 해야한다. (위의 ParentId 클래스 참고)
  • @Embeddable 어노테이션
  • Serializable 인터페이스 구현
  • equals, hashCode 구현
  • 기본생성자 필요
  • 식별자 클래스는 public
// 엔티티 저장
Parent parent = new Parent();
ParentId parentId = new ParentId("myId1", "myId2");
parent.setId(parentId);
parent.setName("parentName");
em.persist(parent);

// 엔티티 조회
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);

 

복합 키와 equals(), hashCode()

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

Java 에서 equals() 를 오버라이딩하지 않는 경우는 인스턴스의 참조 값 비교의 == (동일성)비교 만 하기 때문.

영속성 컨텍스트는 엔티티의 식별자를 키로 사용해서 엔티티를 관리하는데 이때 식별자를 비교할 때, equals() 와 hashCode() 를 사용하기 때문에 이 두 메서드가 오버라이딩 되지 않으면 예상과 다른 결과값이 나오거나 엔티티를 찾을 수 없는 등의 심각한 문제가 발생함.

@IdClass vs. EmbeddedId

각각의 장단점을 고려하여 취향에 맞는 걸로 일관성있게 사용하면 됨.

참고: 복합 키에는 @GeneratedValue 를 사용할 수 없음. 복합 키를 구성하는 컬럼 중 하나에도 사용 할 수 없음

 

Reference : 자바 ORM 표준 JPA 프로그래밍 (김영한 저)