외래 키가 기본 키에 포함되는지 여부에 따라 식별 Identifying Relationship, 비식별 Non-Identifying Relationship 관계로 나뉩니다.
식별 관계
식별 관계는 부모 테이블의 기본 키를 자식 테이블의 기본 키 + 외래 키로 사용하는 방식입니다.
비식별 관계
비식별 관계는 부모 테이블의 기본 키를 자식 테이블의 외래 키로만 사용하는 방식입니다.
비식별 관계는 외래 키(부모의 기본 키)가NULL이 올 수 있냐 없냐에 따라 다시 구분됩니다.
- 필수적 비식별 관계: 외래 키에 NULL을 허용하지 않음 (즉, 연관관계 설정이 필수)
- 선택적 비식별 관계: 외래 키에 NULL을 허용
그러면 이제 식별/비식별 관계 시 매핑을 하는 방법들에 대해 알아보겠습니다.
기본 키를 구성하게 되는 컬럼이 하나인 경우는 컬럼에 @Id를 붙여서 매핑을 했었습니다.
그러나 복합 키를 매핑할 때는 @Id만 붙이면 오류가 발생하게 됩니다. 영속성 컨텍스트에서 식별자 구분을 위해 equals(), hashCode()를 이용한 동등성 비교를 수행합니다. 따라서 복합 키의 경우에는 별도의 식별자 클래스를 생성하고 식별자 클래스에 equals(), hashCode()를 구현해야합니다.
잠시 후 식별/비식별 매핑에서 자세하게 예시를 들 예정이지만 식별자 클래스를 구현해보면 다음과 같은 구조를 갖습니다.
public class IdClass implements Serializable {
//복합 키를 구성하는 식별자 필드들
private String id1;
private String id2;
//기본 생성자는 필수
public IdClass() {}
public IdClass(String id1, String id2) {...}
//equals, hashCode Override 필수
@Override
public boolean equals(Object o) {..}
@Override
public int hashCode() {...}
}
JPA에서는 복합 키 매핑을 위해 @IdClass와 @EmbeddedId 두 가지 방법을 제공하고 있습니다.
@IdClass는 RDBMS의 개념에 가까운 방식의 매핑 방법입니다.
@IdClass를 사용하는 식별자 클래스는 다음과 같은 조건을 만족시켜야 합니다.
Serializable 인터페이스를 구현equals(), hashCode() 구현public@EmbeddedId 객체지향 프로그래밍 개념에 가가운 매핑 방법입니다.
@EmbeddedId를 사용하는 식별자 클래스는 다음과 같은 조건을 만족시켜야 합니다.
@Embeddable을 붙여야한다.Serializable 인터페이스를 구현equals(), hashCode() 구현public이렇게 알아본 두 방식의 식별자 클래스와 식별/비식별 관계 매핑을 예제 코드를 통해 알아보겠습니다.
p_id1, p_id2 복합 키를 사용하는 비식별 관계를 매핑해보겠습니다.
//식별자 클래스 정의
public class IdClass implements Serializable {
//Parent 클래스의 각 기본 키 필드와 매핑
private String pId1;
private String pId2;
//기본 생성자는 필수
public IdClass() {}
public IdClass(String pId1, String pId2) {...}
//equals, hashCode Override 필수
@Override
public boolean equals(Object o) {..}
@Override
public int hashCode() {...}
}
//부모 클래스
@Entity
@IdClass(IdClass.class)
public class Parent {
@Id
@Column(name = p_id1")
private String pId1;
@Id
@Column(name = p_id2")
private String pId2;
//생성자 및 getter
}
//자식 클래스
@Entity
public class Child {
@Id
private String id;
@ManyToOne
@JoinColumns({
@JoinColumn(name = "p_id1", referencedColumnName = "p_id1"),
@JoinColumn(name = "p_id2", referencedColumnName = "p_id2")
})
private Parent parent;
}
위와 같이 정의된 Parent 엔티티의 저장 및 조회는 다음과 같습니다.
//저장
Parent parent = new Parent();
parent.setPId1("pid1");
parent.setPId2("pid2");
em.persist(parent);
//조회
IdClass idc = new IdClass("pid1", "pid2");
Parent parent = em.find(Parent.class, idc);
저장 시에는 Parent 클래스의 복합 키 필드를 이용해 식별자 클래스를 호출해서 복합 키를 영속성 컨텍스트의 키로 사용합니다.
반면 조회 시에는 식별자 클래스르 이용해서 조회하게 됩니다.
마찬가지로 동일한 구조에 대해 @EmbeddedId를 사용한 비식별 관계 매핑을 해보겠습니다.
@Embeddable
public class IdClass implements Serializable {
@Column(name = "p_id1")
private String pId1;
@Column (name = "p_id2")
private String pId2;
//기본 생성자 및 equals, hashCode Override
}
//부모 클래스
@Entity
public class Parent {
@EmbeddedId
private IdClass id;
//생성자 및 getter
}
//자식 클래스는 동일하므로 생략
위와 같이 정의된 Parent 엔티티의 저장 및 조회는 다음과 같습니다.
//저장
Parent parent = new Parent();
IdClass idc = new IdClass("pid1", "pid2");
parent.setId(idc);
em.persist(parent);
//조회
IdClass idc = new IdClass("pid1", "pid2");
Parent parent = em.find(Parent.class, idc);
@IdClass에서는 조회 시에만 IdClass를 생성하고 사용했었지만 @EmbeddedId 방식에서는 저장과 조회에 모두 IdClass를 생성하고 사용합니다.

//식별자 클래스
public class IdClass implements Serializable {
private String cId;
private String pId;
//생성자 및 getter, equals, hashCode
}
//부모 엔티티
@Entity
public class Parent {
@Id
@Column(name = "p_id")
private String pId;
//생성자 및 getter
}
//자식 엔티티
@Entity
@IdClass(IdClass.class)
public class Child {
// 단일 식별자 필드
@Id
@Column(name = "c_id")
private String cId;
// 부모 PK를 복합 PK의 일부로 사용
@Id
@ManyToOne
@JoinColumn(name = "p_id", referencedColumnName = "p_id")
private Parent parent;
//생성자 및 getter
}
//식별자 클래스
@Embeddable
public class IdClass implements Serializable {
@Column(name = "c_id")
private String cId;
@Column(name = "p_id")
private String pId;
//생성자 및 getter, equals, hashCode
}
//부모 클래스는 동일하여 생략
//자식 클래스
@Entity
public class Child {
//복합 PK 매핑
@EmbeddedId
private IdClass id;
//기본 키 중 pId 필드를 부모와 연관관계로 매핑
@MapsId("pId")
@ManyToOne
@JoinColumn(name = "p_id", referencedColumnName = "p_id")
private Parent parent;
//생성자 및 getter
}
일대일 관계에서 식별 관계 매핑은 조금 특별한 방식으로 수행됩니다.
위와 같은 관계에서 자식 테이블의 기본 키로 부모 테이블의 기본 키를 사용합니다. 이 경우에 부모의 기본 키가 복합 키가 아닌 경우에는 자식의 기본 키를 복합 키로 구성하지 않아도 됩니다. (= 식별자 클래스를 생성하지 않아도 된다.)
@Entity
public class Product {
@Id
@Column(name = "p_id)
private String pId;
@OneToOne(mappedBy = "board")
private ProductImage image;
}
@Entity
public class ProductImage {
@Id
private String pId;
@MapsId
@OneToOne
@JoinColumn(name = "p_id")
private Product product;
}
@MapsId를 사용하면 ProductImage.pId가 Product.pId와 동일한 값을 사용하도록 만들어 줍니다.
데이터베이스 설계의 관점에서 식별 관계는 다음과 같은 문제점들을 가지고 있습니다.
객체 관계 매핑 관점에서 식별 관계는 다음과 같은 문제점들을 가지고 있습니다.
위와 같은 이유들로 비식별 관계로 설정하는 것이 추천됩니다.
식별 관계는 다음과 같은 장점도 있습니다.
- 조회 시 하위 테이블 조회만으로도 가능한 경우가 있다 (부모 테이블의 기본 키를 자손들이 가지고 있기 때문)
- 기본 키 인덱스 활용도가 높다