- 이전 게시글
이번 포스팅에서 다룰 내용
이전 포스팅에서 다룬 고급 매핑 내용은 아래와 같다.
- 상속 관계 매핑
@MappedSuperclass
아래는 이번 포스팅에서 다룰 고급 매핑 내용이다.
- 복합 키와 식별 관계 매핑
아래는 다음 포스팅에서 다룰 고급 매핑 내용이다.
- 조인 테이블
- 엔티티 하나에 여러 테이블 매핑하기
복합 키와 식별 관계 매핑
개요
- 기본 복합 키를 매핑하는 방법
- 식별 관계, 비식별 관계를 매핑하는 방법
위 두가지에 대해 지금부터 설명하겠다.
식별 관계 vs 비식별 관계
DB 테이블 사이 관계는 ‘외래키가 기본키에 포함되는지 여부’에 따라 식별 관계와 비식별 관계로 구분한다. 지금부터 각 관계의 특징을 이해해보자.
식별 관계
- 식별 관계는 부모 테이블의 기본 키를 내려받아서 ‘자식 테이블의 기본키+외래키’로 사용하는 관계이다.

- 위 그림을 보자.
PARENT테이블의 기본키PARENT_ID가CHILD테이블의 기본키이자 외래키로 사용되었다. - 이것을 식별관계라고 한다.
비식별 관계
- 비식별 관계는 부모 테이블의 기본키를 받아서 ‘자식 테이블의 외래키’로 사용하는 관계이다.
- 비식별 관계에는 두 가지 종류가 있다.
- 필수적 비식별 관계
- 선택적 비식별 관계

- 위 그림을 보자.
PARENT테이블의 기본키PARENT_ID를CHILD테이블의 외래키로만 사용한다. - 이것을 비식별 관계라고 한다.
- 필수적 비식별 관계 vs 선택적 비식별 관계
- 필수적 비식별 관계
CHILD테이블의PARENT_ID칼럼이 NOT NULL 제약조건을 갖는다.- 연관관계를 필수적으로 맺어야 한다.
- 선택적 비식별 관계
CHILD테이블의PARENT_ID칼럼이 NULL을 허용한다.- 연관관계를 맺을지 말지 선택할 수 있다.
- 필수적 비식별 관계
최근에는 비식별 관계를 주로 사용하고, 꼭 필요한 곳에만 식별 관계를 사용하는 추세이다.
복합 키: 비식별 관계 매핑
복합 키를 매핑하기 위한 기본적인 지식
- JPA에서 식별자를 둘 이상 사용하려면, 별도의 식별자 클래스를 만들어야 한다.
-
즉 아래 코드는 불가능한 코드이다.
@Entity public class Hello { @Id private String id1; @Id private String id2; // 실행 시점에 매핑 예외가 발생한다. } - JPA는 영속성 컨텍스트에 엔티티를 보관할 때 ‘엔티티의 식별자’를 키로 사용한다.
- 식별자를 구분하기 위해,
equals와hashCode메서드를 사용하여 동등성 비교를 한다. - 엔티티를 저장하는 공간인, (영속성 컨텍스트의) 1차 캐시는 HashMap과 유사한 구조를 갖는다.
-
따라서 어떤 엔티티를 저장하거나 조회할 때, 키에 해당하는 객체(여기선 식별자 객체)의
equals와hashCode메서드가 필요하다.equals와hashCode메서드에 대해선, 이전에 다룬 포스팅을 참고하자
- 식별자를 구분하기 위해,
- JPA는 복합 키를 지원하기 위해 아래 방법들을 제공한다.
@IdClass- 관계형 DB에 가까운 방법이다.
@EmbeddedId- 객체지향에 가까운 방법이다.
- 지금부터 위 방법들에 대해 하나씩 알아보자.
@IdClass
예시를 통해 @IdClass 애너테이션을 알아보자.
- 예시
-
가정: 테이블 상태는 아래 그림과 같다.

- 이때,
PARENT테이블은 기본 복합키를 사용한다. (기본키 칼럼이 2개이다.) - 따라서 기본 복합키를 매핑하기 위해, 식별자 클래스를 별도로 만들어야 한다.
-
부모 클래스
@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과 연결된다. // getter, setter 생략 } -
부모 식별자 클래스
//식별자 클래스는 Serializable 인터페이스를 implements 해야한다. public class ParentId implements Serializable { private String id1; //Parent.id1 매핑 private String id2; //Parent.id2 매핑 public ParentId() {} // 기본 생성자 public ParentId(String id1, String id2) { this.id1 = id1; this.id2 = id2; } //영속성 컨텍스트의 1차 캐시에서 key로 사용되기 위해 위해 필요한 메서드 @Override public boolean equals(Object o) { //... } @Override public int hashCode() { //... } }
-
@IdClass(A.class)애너테이션의 의미- ‘해당 애너테이션이 붙은 엔티티 클래스’의 각 속성(필드) 중,
@Id가 붙은 속성(필드)를A 식별자 클래스의 속성(필드)와 매핑시키겠다는 의미이다. - 이때, 각 속성의 이름을 비교하여 매핑시킨다.
- 즉, 아래처럼 매핑된다.
- 테이블의 기본키 칼럼 ↔ 식별자 클래스의 속성 ↔ 엔티티 클래스의 속성
- ‘해당 애너테이션이 붙은 엔티티 클래스’의 각 속성(필드) 중,
- 식별자 클래스의 조건
- ‘식별자 클래스의 속성명’과 ‘엔티티에서 사용하는 식별자의 속성명’이 같아야 한다.
Parent.id1과ParentId.id1끼리 매핑된다.Parent.id2와ParentId.id2끼리 매핑된다.
Serializable인터페이스를 구현해야 한다.equals,hashCode를 구현해야 한다.- 기본 생성자가 있어야 한다.
- 식별자 클래스는
public클래스이어야 한다.
- ‘식별자 클래스의 속성명’과 ‘엔티티에서 사용하는 식별자의 속성명’이 같아야 한다.
- 식별자 클래스의 필드
- 식별자 클래스의 각 필드는 ‘복합키를 갖는 엔티티의 각 기본키 필드와 매핑’된다.
- 복합키를 갖는 엔티티의 종류
- 엔티티의 기본키 필드가 객체형인 경우
- 해당 엔티티가 다른 엔티티와 연관관계를 맺는다는 뜻이다.
- 따라서, 식별자 클래스의 필드는 ‘다른 엔티티의 기본키’를 담아야 한다.
- 왜냐하면, 다른 엔티티를 구분해야 하기 때문이다.
- 엔티티의 기본키 필드가 기본형인 경우
- 식별자 클래스의 필드는 그대로 ‘해당 엔티티의 필드값’을 담는다.
- 엔티티의 기본키 필드가 객체형인 경우
-
복합 키를 사용하는 엔티티 저장하기
Parent parent = new Parent(); parent.setId1("myId1"); //식별자 설정 parent.setId2("myId2"); //식별자 설정 parent.setName("parentName"); em.persist(parent);- 위 코드는 parent 엔티티를 저장하는 코드이다. 여기서 이상한 점을 눈치챘는가?
- 식별자 클래스인
ParentId와 관련된 코드가 존재하지 않는다!em.persist()를 호출시 영속성 컨텍스트에 엔티티를 등록하기 직전에, 내부에서Parent.id1,Parent.id2값을 사용하여 식별자 클래스ParentId를 생성하고 영속성 컨텍스트의 키로 사용한다.
-
복합 키를 사용하는 엔티티 조회하기
ParentId parentId = new ParentId("myId1", "myId2"); //복합키 생성 Parent parent = em.find(Parent.class, parentId); // 식별자 인스턴스로 조회하기
-
복합키를 가진 부모 엔티티와 ‘선택적 비식별 연관관계’를 갖는 자식 클래스 추가
@Entity public class Child { @Id private String childId; @ManyToOne @JoinColumns({ @JoinColumn(name = "PARENT_ID1", referencedColumnName = "PARENT_ID1"), @JoinColumn(name = "PARENT_ID2", referencedColumnName = "PARENT_ID2") }) private Parent parent; // getter, setter 생략 }
@JoinColumns애너테이션- 외래키 매핑시, 여러 칼럼을 매핑해야 할 때
@JoinColumns를 사용한다.- 위 경우 부모 테이블이 기본 복합키를 사용하므로, 자식 테이블의 외래키도 ‘기본 복합키로 사용된 여러 칼럼’을 외래키로 사용해야 한다.
name속성CHILD테이블의 FK 칼럼명
referencedColumnName속성CHILD테이블의 FK 칼럼과 매핑될 ‘PARENT테이블’의 칼럼명name값과referencedColumnName값이 같다면,referencedColumnName은 생략할 수 있다.
- 외래키 매핑시, 여러 칼럼을 매핑해야 할 때
@EmbeddedId
@EmbeddedId 는 좀 더 객체지향적인 방법이다. 이것 역시, 예시를 통해 알아보자.
- 예시
- 가정: 테이블 상태는 위(
@IdClass설명 부분)와 같다. -
부모 클래스
@Entity public class Parent { @EmbeddedId private ParentId parentId; private String name; // getter, setter 생략 } -
부모 식별자 클래스
@Embeddable public class ParentId implements Serializable { @Column(name = "PARENT_ID1") // PARENT 테이블의 PARENT_ID1 칼럼과 매핑된다. private String id1; @Column(name = "PARENT_ID2") // PARENT 테이블의 PARENT_ID2 칼럼과 매핑된다. private String id2; public ParentId() {} // 기본 생성자 public ParentId(String id1, String id2) { this.id1 = id1; this.id2 = id2; } //영속성 컨텍스트의 1차 캐시에서 key로 사용되기 위해 위해 필요한 메서드 @Override public boolean equals(Object o) { //... } @Override public int hashCode() { //... } }@Column@IdClass와는 다르게, 식별자 클래스의 필드에@Column을 붙여줘야 한다.- 왜냐하면 ‘복합키를 쓰는 엔티티’에서
@Column을 사용하지 않아, 각 필드를 어떤 칼럼과 매핑해야하는지 모르기 때문이다. - 즉
@IdClass와는 다르게,@Embeddable를 적용한 식별자 클래스는 식별자 클래스에 기본키를 직접 매핑한다.
- 가정: 테이블 상태는 위(
- 식별자 클래스의 조건
@Embeddable애너테이션을 붙여야 한다.Serializable인터페이스를 구현해야 한다.equals,hashCode메서드를 오버라이딩해야 한다.- 기본 생성자가 있어야 한다.
- 식별자 클래스는
public클래스이어야 한다.
-
복합키를 사용하는 엔티티 저장하기
Parent parent = new Parent(); ParentId parentId = new ParentId("myId1", "myId2"); // 키 생성 parent.setId(parentId); //식별자 설정 parent.setName("parentName"); em.persist(parent);@IdClass와는 다르게,ParentId를 직접 생성해서 사용한다.
-
복합키를 사용하는 엔티티 조회하기
ParentId parentId = new ParentId("myId1", "myId2"); // 키 생성 Parent parent = em.find(Parent.class, parentId);
복합 키와 equals() , hashCode()
- JPA는 영속성 컨텍스트에 엔티티를 보관할 때 ‘엔티티의 식별자’를 키로 사용한다.
- 식별자를 구분하기 위해,
equals와hashCode메서드를 사용하여 동등성 비교를 한다. - 엔티티를 저장하는 공간인, (영속성 컨텍스트의) 1차 캐시는 HashMap과 유사한 구조를 갖는다.
-
따라서 어떤 엔티티를 저장하거나 조회할 때, 키에 해당하는 객체(여기선 식별자 객체)의
equals와hashCode메서드가 필요하다.equals와hashCode메서드에 대해선, 이전에 다룬 포스팅을 참고하자.
@IdClass vs @EmbeddedId
@EmbeddedId가 대체로 좋아보이지만, 반드시 그렇지는 않다.@EmbeddedId는 특정 상황에서 JPQL이 좀더 길어질 수 있다.
참고사항
- 복합키에는
@GenerateValue를 사용할 수 없다. - 복합 키를 구성하는 여러 칼럼 중 하나에도 사용할 수 없다!
복합 키: 식별 관계 매핑
- 위에서 비식별 관계를 매핑하는 방법에 대해 다뤘다.
- 이제 식별 관계를 매핑하는 방법을 알아보자.
설명을 위한 기본 가정
- 테이블 상태는 아래 그림과 같다고 가정한 뒤, 설명을 계속하도록 하겠다.

- 위 그림은 부모, 자식, 손자까지 계속 기본키를 전달하는 식별관계이다.
- 식별 관계에서 자식·손자 테이블은 부모 테이블의 기본키를 포함해서 복합키를 구성한다.
- 따라서,
@IdClass나@EmbeddedId를 사용해서 식별자를 매핑해야 한다.
- 따라서,
- 이제 본격적으로 식별관계 매핑에 대해 알아보자.
@IdClass 와 식별 관계
바로 예시 코드를 통해 알아보자.
-
부모 클래스
@Entity public class Parent { @Id @Column(name = "PARENT_ID") private String id; private String name; // getter, setter 생략 }
-
자식 클래스
@Entity @IdClass(ChildId.class) // 식별자 클래스 설정 public class Child { @Id @ManyToOne @JoinColumn(name = "PARENT_ID") // CHILD 테이블의 PARENT_ID와 매핑 private Parent parent; @Id @Column(name = "CHILD_ID") private String childId; private String name; // getter, setter 생략 }
-
자식 식별자 클래스
public class ChildId implements Serializable { //Child.parent와 매핑된다 //**Parent 엔티티의 기본키 값을 담는다** private String parent; //필드명이 같아야 한다. //Child.childId와 매핑된다 //**Child 엔티티의 기본키 childId 값을 담는다** private String childId; //필드명이 같아야 한다. public ChildId() {} // 기본 생성자 public ChildId(String id1, String id2) { this.id1 = id1; this.id2 = id2; } //영속성 컨텍스트의 1차 캐시에서 key로 사용되기 위해 위해 필요한 메서드 @Override public boolean equals(Object o) { //... } @Override public int hashCode() { //... } }
-
손자 클래스
@Entity @IdClass(GrandChildId.class) // 식별자 클래스 설정 public class GrandChild { @Id @ManyToOne @JoinColumns({ @JoinColumn(name = "PARENT_ID"), @JoinColumn(name = "CHILD_ID") }) // 여러 칼럼으로 구성된 PK를 가져와, FK로 사용하므로 @JoinColumns 를 사용한다. private Child child; @Id @Column(name = "GRANDCHILD_ID") private String id; private String name; // getter, setter 생략 }
-
손자 식별자 클래스
public class GrandChildId implements Serializable { // GrandChild.child 와 매핑된다 // **Child 엔티티의 기본키 값이 담긴다** private ChildId child; //기본 복합키이므로, 식별자 클래스 객체를 담는다 // GrandChild.id 와 매핑된다 // **GrandChild 엔티티의 기본키 id 값이 담긴다** private String id; }
- 상세 설명
- 식별 관계는 기본키와 외래키를 같이 매핑해야 한다.
- 따라서,
@Id와@ManyToOne을 같이 사용한다.
@EmbeddedId 와 식별 관계
@EmbeddedId 로 식별 관계를 구성할 땐, @MapsId 를 사용해야 한다. 바로 예시를 통해 알아보자.
-
부모 클래스
부모 클래스는 변경사항이 없다.
@Entity public class Parent { @Id @Column(name = "PARENT_ID") private String id; private String name; // getter, setter 생략 }
-
자식 클래스
@Entity public class Child { @EmbeddedId private ChildId id; @MapsId("parentId") //ChildId.parentId 와 매핑 (연관관계 정보 전달) @ManyToOne @JoinColumn(name = "PARENT_ID") // CHILD 테이블의 PARENT_ID와 매핑 private Parent parent; private String name; // getter, setter 생략 }- 만약 자식 테이블의 칼럼
PARENT_ID가 단순히 PK 칼럼이라면,parent필드를Child엔티티 클래스에서 선언할 필요가 없다. (물론 이런 경우에는parent필드가 기본 타입이다.)- 왜냐하면 식별자 클래스를 통해, 매핑 설정을 할 수 있기 때문이다.
- 하지만 식별 관계인 경우(자식 테이블의 칼럼
PARENT_ID가 PK·FK 칼럼인 경우),parent필드를Child엔티티 클래스에서 선언해야 한다.- 왜냐하면 식별자 클래스에서 ‘pk 매핑 설정과 함께 연관관계를 설정’할수는 없기 때문에,
Child클래스에서 연관관계를 설정해야하기 때문이다.
- 왜냐하면 식별자 클래스에서 ‘pk 매핑 설정과 함께 연관관계를 설정’할수는 없기 때문에,
@MapsId("parentId")- 식별자 클래스의
parentId필드에 아래 연관관계를 적용하겠다는 의미와 같다.@ManyToOne,@JoinColumn
- 식별자 클래스의
- 만약 자식 테이블의 칼럼
-
@EmbeddedId사용 시, 기본키·외래키 설정 위치매핑 설정 설정 위치 PK 설정 식별자 클래스에서 설정한다. FK 설정 먼저 엔티티 클래스에서 설정한다.
그 후, 식별자 클래스에게도 연관관계 정보를 전달한다.
-
자식 식별자 클래스
@Embeddable public class ChildId implements Serializable { // @MapsId가 전달해준 연관관계 매핑 private String parentId; @Column("CHILD_ID") private String id; public ChildId() {} // 기본 생성자 public ChildId(String parentId, String id) { this.parentId = parentId; this.id = id; } //영속성 컨텍스트의 1차 캐시에서 key로 사용되기 위해 위해 필요한 메서드 @Override public boolean equals(Object o) { //... } @Override public int hashCode() { //... } }
-
손자 클래스
@Entity public class GrandChild { @EmbeddedId private GrandChildId id; @MapsId("childId") // GrandChildId.childId 와 매핑 (연관관계 정보 전달) @ManyToOne @JoinColumns({ @JoinColumn(name = "PARENT_ID"), @JoinColumn(name = "CHILD_ID") }) private Child child; private String name; // getter, setter 생략 }
-
손자 식별자 클래스
@Embeddable public class GrandChildId implements Serializable { // @MapsId가 전달해준 연관관계 매핑 private ChildId childId; @Column("GRANDCHILD_ID") private String id; public GrandChildId() {} // 기본 생성자 public GrandChildId(ChildId childId, String id) { this.childId = childId; this.id = id; } //영속성 컨텍스트의 1차 캐시에서 key로 사용되기 위해 위해 필요한 메서드 @Override public boolean equals(Object o) { //... } @Override public int hashCode() { //... } }
- 상세 설명
- ‘식별 관계로 사용할 연관관계의 속성(필드)’에
@MapsId를 적용하면 된다. @IdClass와의 차이점@IdClass는 엔티티의 연관관계 속성(필드)에@Id를 사용한다.@EmbeddedId는 엔티티의 연관관계 속성(필드)에@MapsId를 사용한다.
@MapsId의 의미- ‘외래키와 매핑한 연관관계’를 기본키에도 매핑하겠다는 뜻이다.
- 즉, 연관관계 정보를 식별자 클래스에게도 넘겨 매핑하겠다는 뜻이다.
- 따라서 속성 값으로 ‘식별자 클래스의 기본키 필드’를 지정하면 된다.
- ‘식별 관계로 사용할 연관관계의 속성(필드)’에
비식별 관계로 구현
위에서 설명한 식별 관계를 비식별 관계로 변경하고, 이에 따라 코드가 어떻게 변화하는지 보자.
-
테이블 상태

-
부모 클래스
@Entity public class Parent { @Id @GeneratedValue @Column(name = "PARENT_ID") private Long id; private String name; // getter, setter 생략 }
-
자식 클래스
@Entity public class Child { @Id @GeneratedValue @Column(name = "CHILD_ID") private Long id; @ManyToOne @JoinColumn(name = "PARENT_ID") // CHILD 테이블의 PARENT_ID와 매핑 private Parent parent; private String name; // getter, setter 생략 }
-
손자 클래스
@Entity public class GrandChild { @Id @GeneratedValue @Column(name = "GRANDCHILD_ID") private Long id; @ManyToOne @JoinColumn(name = "CHILD_ID") private Child child; private String name; // getter, setter 생략 } - 기본 복합키를 사용하지 않으니, 상당히 단순해졌다.
- 식별자 클래스를 만들 필요가 없다.
- 매핑도 매우 간단하다.
일대일 식별관계
일대일 식별 관계는 약간 특별하다. 바로 예제를 통해 알아보자.
-
테이블 상태

- 일대일 식별관계는 ‘자식 테이블의 기본키 값으로 부모 테이블의 기본키 값만 사용’한다.
- 그래서 부모 테이블의 기본키가 복합키가 아니면, 자식 테이블의 기본 키는 복합 키로 구성하지 않아도 된다.
-
부모 클래스 (
BOARD)@Entity public class Board { @Id @GeneratedValue @Column(name = "BOARD_ID") private Long id; private String title; //양방향 @OneToOne(mappedBy = "board") private BoardDetail boardDetail; // getter, setter, 편의메서드 생략 }
-
자식 클래스 (
BOARDDETAIL)@Entity public class BoardDetail { @Id private Long boardId; @MapsId // BoardDetail.boardId 와 매핑 (연관관계 정보 전달) @OneToOne @JoinColumn(name = "BOARD_ID") private Board board; private String content; // getter, setter, 편의메서드 생략 }@MapsId를 통해, 자신의 PK 필드에 연관관계를 전달한다.
- 상세 설명
BoardDetail처럼 식별자가 단순히 칼럼 하나인 경우,@MapsId를 사용하고, 속성값은 비워두면 된다.- 이때
@MapsId는@Id를 사용해서 식별자로 지정한BoardDetail.boardId와 매핑된다.
-
일대일 식별관계를 사용하는 코드
public void save() { Board board = new Board; board.setTitle("제목"); em.persist(board); BoardDetail boardDetail = new BoardDetail(); boardDetail.setContent("내용"); boardDetail.setBoard(board); em.persist(boardDetail); }
식별, 비식별 관계의 장단점
- 식별 관계의 단점
- 부모 테이블의 키본키를 자식 테이블로 전파하면서, 자식 테이블의 기본키 칼럼이 점점 늘어난다.
- 결국 조인할 때, SQL이 복잡해지고 기본키 인덱스가 불필요하게 커질 수 있다.
- 2개 이상의 칼럼을 합해서 복합 기본키를 만들어야 하는 경우가 많다.
- 비즈니스 의미가 있는 자연키 칼럼을 조합하여 기본키로 사용하는 경우가 많다.
- 식별 관계의 자연키 칼럼들이 자식에 손자까지 전파되면 변경하기 힘들다.
- 비즈니스 요구사항은 언제가는 바뀐다.
- 반면에, 비식별 관계에서는 주로 대리키를 기본키로 사용한다.
- 부모 테이블의 기본키를 자식 테이블의 기본키로 사용하므로, 비식별 관계보다 테이블 구조가 유연하지 못하다.
- 일대일 관계를 제외한 식별 관계는 2개 이상의 칼럼을 묶은 복합 기본키를 사용한다.
- 따라서, 칼럼이 하나인 기본키를 매핑하는 것보다 많은 노력이 필요하다.
- 부모 테이블의 키본키를 자식 테이블로 전파하면서, 자식 테이블의 기본키 칼럼이 점점 늘어난다.
- 식별 관계의 장점
- 기본 키 인덱스를 활용하기 좋다.
- 상위 테이블들의 기본키 칼럼을 자식 및 손자 테이블들이 가지고 있으므로, 특정 상황에 조인 없이 하위 테이블만으로 검색을 완료할 수 있다.
- 정리
- 웬만하면 비식별 관계를 사용하자.
- 그리고 기본키로 Long 타입의 대리키를 사용하자.
- 비즈니스가 변경되어도 유연하게 대처할 수 있다.
@GeneratedValue를 통해, 간편하게 대리키를 생성할 수 있다.- 식별자 칼럼이 하나여서 쉽게 매핑할 수 있다.
- ‘선택적 비식별 관계’보단 ‘필수적 비식별 관계’를 사용하자.
- 선택적 비식별 관계는 NULL을 허용하므로, 조인할 때 외부조인을 사용해야 한다.
- 필수적 비식별 관계는 NULL을 허용하지 않으므로, 조인할 때 내부조인만을 사용할 수 있다.
외부조인: 연관되지 않은 것까지 포함하여 출력
내부조인: 연관되지 않은 것은 제외하고 출력
- 김영한, 『자바 ORM 표준 JPA 프로그래밍』, 에이콘