[JPA] 복합 키와 식별/비식별 관계 매핑

Bam·2025년 5월 21일
0

Spring

목록 보기
61/73
post-thumbnail

식별/비식별 관계

외래 키가 기본 키에 포함되는지 여부에 따라 식별 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

@IdClass는 RDBMS의 개념에 가까운 방식의 매핑 방법입니다.

@IdClass를 사용하는 식별자 클래스는 다음과 같은 조건을 만족시켜야 합니다.

  • 식별자 클래스의 속성명과 엔티티의 식별자 속성명이 동일해야한다
  • Serializable 인터페이스를 구현
  • equals(), hashCode() 구현
  • 기본 생성자 필수
  • 식별자 클래스의 접근 제어자는 public

@EmbeddedId

@EmbeddedId 객체지향 프로그래밍 개념에 가가운 매핑 방법입니다.

@EmbeddedId를 사용하는 식별자 클래스는 다음과 같은 조건을 만족시켜야 합니다.

  • 식별자 클래스에 @Embeddable을 붙여야한다.
  • Serializable 인터페이스를 구현
  • equals(), hashCode() 구현
  • 기본 생성자 필수
  • 식별자 클래스의 접근 제어자는 public

이렇게 알아본 두 방식의 식별자 클래스와 식별/비식별 관계 매핑을 예제 코드를 통해 알아보겠습니다.


복합 키와 비식별 관계 매핑

@IdClass 비식별 관계 매핑

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 비식별 관계 매핑

마찬가지로 동일한 구조에 대해 @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를 생성하고 사용합니다.


복합 키와 식별 관계 매핑

@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
}

@EmbeddedId 식별 관계 매핑

//식별자 클래스
@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.pIdProduct.pId와 동일한 값을 사용하도록 만들어 줍니다.


모델링 시 비식별 관계를 추천한다

데이터베이스 설계의 관점에서 식별 관계는 다음과 같은 문제점들을 가지고 있습니다.

  • 식별 관계에서는 서브 테이블이 늘어남에 따라 자식 테이블의 기본 키 컬럼이 늘어난다. 이는 설계 및 SQL 사용 시 불편함을 불러일으킨다.
  • 식별 관계는 복합 기본 키를 따로 생성해야하는 경우가 많다.
  • 식별 관계에서는 유의미한 자연 키 컬럼을 조합하게 되는데, 요구 사항 변경 시 키 컬럼들이 변경 되면 자손 테이블들까지 변경해 주어야한다.
  • 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하므로 테이블 구조가 유연하지 않다.

객체 관계 매핑 관점에서 식별 관계는 다음과 같은 문제점들을 가지고 있습니다.

  • 별도의 복합 키 클래스를 생성해야하는 수고가 든다.
  • 비식별 관계는 대리 키를 생성하게되는데 JPA의 @GenerateValue를 통해 편리하게 값을 생성할 수 있다

위와 같은 이유들로 비식별 관계로 설정하는 것이 추천됩니다.

식별 관계는 다음과 같은 장점도 있습니다.

  • 조회 시 하위 테이블 조회만으로도 가능한 경우가 있다 (부모 테이블의 기본 키를 자손들이 가지고 있기 때문)
  • 기본 키 인덱스 활용도가 높다

0개의 댓글