7장. 고급 매핑

배유연·2023년 10월 26일

1. 상속 관계 매핑

관계형 데이터베이스에는 객체지향 언어에서 다루는 상속이라는 개념이 없다.
ORM에서 말하는 상속관계 매핑은 객체의 상속 구조와 데이터베이스의 슈퍼타입/서브타입 관계를 매핑하는 것이다.

논리모델을 테이블로 구현하는 3가지 방법

1) 각각의 테이블로 변환 : JPA의 조인전략을 사용하기
2) 통합 테이블로 변환 : 테이블을 하나만 사용하는 단일 테이블 전략
3) 서브타입 테이블로 변환 : 서브타입 마다 하나의 테이블로 만드는 전략

📌조인전략

조인전략은 엔티티 각각을 모두 테이블로 만들고 자식테이블이 부모테이블의 기본키를 받아서 기본키+외래키로 사용하는 전략이다. 타입을 구분하는 컬럼을 추가해주어야한다.

부모테이블 코드를 보자.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item{
	
    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    
    private String name;
    private int price;
    ...
}
  • @Inheritance(strategy = InheritanceType.JOINED)
    : 상속 매핑은 부모 클래스에 @Inheritance 를 사용한다. 매핑 전략을 JOINED로 사용한다.
  • @DiscriminatorColumn(name='DTYPE')
    : 부모 클래스에 구분 컬럼을 지정한다. 기본값이 DTYPE이라 @DiscriminatorColumn만 써도 된다.

자식 테이블 코드를 보자.

@Entity
@DisciminatorValue("A")
public class Album extends Item{
	private String artist;
    ...
}
  • @DiscriminatorValue('M')
    : 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다.

만약 자식 테이블에서 기본키 컬럼명을 변경하고 싶다면? @PrimaryKeyJoinColumn을 사용하면 된다.

@Entity
@DisciminatorValue("A")
@PrimaryKeyJoinColumn(name="ALBUM_ID") //ID 재정의
public class Album extends Item{
	private String artist;
    ...
}

조인 전략의 장점

  • 테이블이 정규화된다.
  • 외래키 참조 무결성 제약조건을 활용할 수 있다.
  • 저장공간을 효율적으로 사용한다.

조인 전략의 단점

  • 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.
  • 조회 쿼리가 복잡하다.
  • 데이터 등록시 INSERT 가 두번 실행한다.

특징

JPA 표준 명세는 구분 컬럼을 사용하도록 하지만, 하이버네이트를 포함한 몇몇 구현체는 구분컬럼 없이도 동작한다.

관련 어노테이션

@PrimaryKeyJoinColumn, @DiscriminatorColumn, @DisciminatorValue

📌단일 테이블 전략

단일테이블 전략은 테이블을 하나만 사용한다. 그리고 구분 컬럼으로 어떤 자식 데이터가 저장되어있는지 구분한다.
조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다.

코드를 보자.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item{
	@Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    
    private String name;
    private int price;
    ...
}

@Entity
@DisciminatorValue("A")
public class Album extends Item{
    ...
}

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)를 사용하면 단일 테이블 전략을 사용한다.

단일 테이블 전략 장점

  • 조인이 필요없으므로 조회 성능이 빠르다.
  • 조회 쿼리가 단순하다.

단일 테이블 전략 단점

  • 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야한다.
  • 단일 테이블에 모든것이 저장되므로 상황에 따라서 오히려 성능이 안좋을 수 있다.

특징

  • 구분 컬럼을 꼭 사용해야한다.(@DiscriminatorColumn)
  • @DisciminatorValue를 생략하면 기본으로 엔티티 이름을 사용한다.

📌구현 클래스마다 테이블 전략


자식 엔티티 마다 테이블을 만든다.

코드를 보자.

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item{
	@Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    
    private String name;
    private int price;
    ...
}

@Entity
public class Album extends Item{
    ...
}

@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)를 사용해서 구현 클래스마다 테이블 전략을 사용한다. 일반적으로 추천하지 않는 전략이다.

구현 클래스마다 테이블 전략 장점

  • 서브 타입을 구분해서 처리할 때 효과적
  • NOT NULL 제약조건을 사용할 수 있다.

구현 클래스마다 테이블 전략 단점

  • 여러 자식 테이블을 함게 조회할 때 성능이 느리다.(UNION 사용)

특징

  • 구분 컬럼을 사용하지 않는다.
  • 해당 전략은 추천하지 않고 조인이나 단일 테이블 전략을 고려하자.

2. @MappedSurperclass

부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속받은 자식 클래스에게 매핑 정보만 제공하고 싶을 때 사용한다.
@MappedSurperClass 는 실제 테이블과는 매핑되지 않는다.

회원과 판매자는 서로 관계가 없는 테이블이다. id, name 두 공통 속성만 부모 클래스로 모아준 BaseEntity 를 생성하였다.
코드를 보자.

@MappedSuperclass
public class BaseEntity {

	@Id
    @GeneratedValue
    private Long id;
    private String name;
    ...
}
@Entity
public class Member7_2 extends BaseEntity {
	private String email;
}
...

@MappedSuperClass 를 사용해서 테이블과 매핑할 필요가 없고 자식 엔티티에게 공통으로 사용되는 매핑 정보만 제공하였다.

@MappedSuperclass 특징

  • @MappedSupeerclass 로 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용할 수 없다.
  • 이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것을 권장한다.
  • 등록일자, 수정일자, 등록자, 수정자 같은 여러 엔티티에서 공통 사용하는 속성을 효과적으로 관리할 수 있다.

    참고
    @Entity 는 @Entity 이거나 @MappedSuperclass로 지정한 클래스만 상속받을 수 있다.


3. 복합키와 식별관계 매핑

식별관계

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

비식별관계

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

  • 필수적 비식별관계 : 외래키에 NULL을 허용하지 않는다. 연관관계를 필수적으로 맺어야한다.
  • 선택적 비식별관계 : 외래키에 NULL을 허용한다.

🔐복합키 : 비식별 관계 매핑

JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자 클래스를 만들어야한다.
JPA는 복합키를 지원하기 위해 @IdClass와 @EmbeddedId 2가지 방법을 제공하는데, @IdClass는 관계형 데이터베이스에 가까운 방법이고, @EmbeddedId는 좀 더 객체지향에 가까운 방법이다.

🔍@IdClass


PARENT 테이블을 보면 기본키를 PARENT_ID1, PARENT_ID2 로 묶은 복합키로 구성했다.

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


public class ParentId implements Serializable {

    private String id;
    private String id2;

    public ParentId() {
    }

    public ParentId(String id, String id2) {
        this.id = id;
        this.id2 = id2;
    }
    //equals, hashCode 오버라이드
    ...
}

@IdClass 를 이용한 식별자 클래스 조건

  • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야한다.
  • Serializable 인터페이스를 구현해야한다
  • equals, hashCode 를 구현해야한다.
  • 기본 생성자가 있어야한다.
  • 식별자 클래스는 public 이어야한다.

그러면 실제 저장을 어떻게 하는지 보자.

Parent parent = new Parent();
parent.setId("myId1"); //식별자
parent.setId("myId2"); //식별자
parent.setName("parentName");
em.persist(parent);

저장 코드를 보면, ParentId가 보이지 않는다. em.persist() 를 호출하면 영속성 컨텍스트에 엔티티를 등록하기 직전에 내부에서 Parent.id1, Parent.id2를 사용해서 ParentId를 생성하고 영속성 컨텍스트의 키로 사용한다.

조회 코드를 보자.

ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);

식별자 클래스인 ParentId를 통해서 조회를 한다.

이제 자식 클래스 코드를 보자.

@Entity
public class Child{
	@Id
    private Stirng id;
    
    @ManyToOne
    @JoinColumns({
    	@JoinColumn(name="PARENT_ID1", referecedColumnName = "PARENT_ID1"),
        @JoinColumn(name="PARENT_ID2", referecedColumnName = "PARENT_ID2")
    })
    private Parent parent;
}

외래키 매핑시 여러 컬럼을 매핑해야하므로 @JoinColumns 어노테이션을 사용한다.
참고로, @JoinColumn 의 name과 referencedColumnName 속성의 값이 같으면 referencedColumnName은 생략해도 된다.

🔍@EmbeddedId

@EmbeddedID 는 좀 더 객체지향적인 방법이다.

@Entity
public class Parent{
	@EmbeddedId
    private ParentId id;
    
    private String name;
    ...
}

Parent 엔티티에서 식별자 클래스(ParentId)를 직접 사용하고 @EmbeddedId 어노테이션을 적어주면 된다.

@Embeddable
public class ParentId implements Serializable{
	@Column(name="PARENT_ID1")
    private String id;
    
    @Column(name="PARENT_ID2")
    private String id2;
    
    //equals and hashCode 구현
	...
}

@IdClass와는 다르게 @EmbeddedId를 적용한 식별자 클래스는 식별자 클래스에 기본키를 직접 매핑한다.

@EmbeddedId를 적용한 식별자 클래스 조건

  • Serializable 을 구현해야한다.
  • equals, hashCode 를 구현해야한다.
  • 기본 생성자가 있어야한다.
  • 식별자 클래스는 public 이어야한다.

@EmbeddedId를 사용한 저장 코드

Parent parent  =new Parent();
ParentId parentId = new ParentId("myId1", "myId2");
parent.setId(parentId);
parent.setName("아무개");
em.persist();

식별자 클래스 parentId를 직접 생성해서 사용한다.
@EmbeddedId를 사용한 조회 코드

ParentId  parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);

조회 코드도 식별자 클래스 parentId를 직접 사용한다.

복합키와 equals(), hashCode()

복합키는 equals()와 hashCode()를 필수적으로 구현해야한다.
왜냐하면, 영속성 컨텍스트는 엔티티의 식별자를 키로 사용해서 엔티티를 관리하고, 식별자를 비교할 때 equals()와 hashCode()를 사용하기 때문이다.

@IdClass VS @EmbeddedId

@EmbeddedID 가 @IdClass와 비교해서 좀 더 객체지향적일 수 있고, 중복도 없어서 좋아보이지만, 상황에 따라서 JPQL이 더 길어줄 수 있다.

em.createQuery("select p.id.id1, p.id.id2 from Parent p"); //@Embedded
em.createQuery("select p.id, p.id2 from Parent p"); //@IdClass

🔐복합키 : 식별 관계 매핑


식별관계에서 자식 테이블은 부모 테이블의 기본키를 포함해서 복합키를 구성해야한다.

🔍@IdClass와 식별관계

코드를 보자.

//부모
@Entity
public class Parent {

    @Id
    @Column(name = "PARENT_ID")
    private String 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;
    ...

}
// 자식 ID클래스
public class ChildId implements Serializable{
	private String parent; // Child.parent 매핑
    private String childId;// Child.childId 매핑
    
    //equals, hashCode
    ...
}

식별관계는 기본키와 외래키를 같이 매핑해야 한다. 따라서 식별자 매핑인 @Id와 연관관계 매핑인 @ManyToOne 을 같이 사용하였다.

🔍@EmbeddedId 와 식별관계

코드를 보자.

//부모
@Entity
public class Parent {

    @Id
    @Column(name = "PARENT_ID")
    private String id;
    private String name;
    ...
}

//자식
@Entity
public class Child {

    @EmbeddedId
    private ChildId id;

    @MapsId("parentId") // ChildId.parentId와 매핑
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent2 parent;

    private String name;
	...
}
//자식 ID
@Embeddable
public class ChildId implements Serializable {

    private String parentId; // @MapsId("parentId")로 매핑

    @Column(name = "CHILD_ID")
    private String id;
    
    //equals, hashCode 구현
    ...
}

@IdClass 와 다른점은 @Id 대신에 @MapsId를 사용하였다.

@MapsId 는 외래키와 매핑한 연관관계를 기본키에도 매핑하겠다는 뜻이다.

🔐비식별관계로 구현

6장에 나왔던 예처럼 식별관계를 비식별관계로 변경하자.

//부모
@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 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;
    ...
}

BoardDetial처럼 식별자가 단순히 하나면 @MapsId 를 사용하고 속성 값은 비워두면 된다.
이때 @MapsId는 @Id를 사용해서 식별자로 지정한 BoardDetail.boardId와 매핑된다.


4. 조인 테이블

데이터베이스 테이블의 연관관계를 설계하는 방법은 조인컬럼(외래키)을 사용하거나 조인테이블을 사용하는 방법이 있다.

✔ 조인 컬럼 사용

테이블 간에 관계는 주로 조인 컬럼이라 부르는 외래키 컬럼을 사용해 관리한다.

회원이 아직 사물함을 사용하지 않으면 둘 사이의 관계가 없으므로, MEMBER테이블의 LOCKER_ID 외래키에 NULL을 허용하여야한다.(선택적 비식별관계)
따라서 두 테이블을 조회할 때 외부 조인으로 사용해야한다.

✔ 조인 테이블 사용


회원과 사물함 데이터를 각각 등록했다가 회원이 원할 때 사물함을 선택하면 MEMBER_LOCKER 테이블에만 값을 추가하면 된다.
단점은, 테이블을 추가로 관리해야하고, 회원과 사물함 두 테이블을 조인하려면 MEMBER_LOCKER 테이블까지 추가로 조인해야한다.

특징

  • 객체와 테이블을 매핑할 때 조인컬럼은 @JoinColumn 으로 매핑하고, 조인테이블은 @JoinTable로 매핑한다.
  • 조인 테이블은 주로 다대다 관계를 일대다, 다대일 관계로 풀어내기 위해 사용한다.

📌일대일 조인 테이블

일대일 관계를 만들기 위해 조인 테이블의 외래키 컬럼각각에 2개의 유니크 제약조건을 걸어야한다.

코드를 보자.

//Parent
@Entity
public class Parent {

    @Id
    @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;

    private String name;

    @OneToOne
    @JoinTable(name = "PARENT_CHILD",
            joinColumns = @JoinColumn(name = "PARENT_ID"),
            inverseJoinColumns = @JoinColumn(name = "CHILD_ID"))
    private Child child;
	...
}
//Child
@Entity
public class Child {

    @Id
    @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;

    private String name;

}

조인테이블을 이용해서 단방향 일대일 매핑을 하였다.

@JoinTable의 속성
name : 매핑할 조인 테이블 이름
joinColumns : 현재 엔티티를 참조하는 외래키
inverseJoinColumns: 반대방향 엔티티를 참조하는 외래키

참고로 나는 위코드를 Mysql DB로 설정해서 실행했더니, child_id 에 대해서 유니크 제약조건이 생성되지 않고, MUL타입으로 생성되었다.

MUL타입이란?
MULTIPLE 의 줄인 말로, 다른 테이블의 기본 키를 참조하는 외래키를 나타낸다. 해당 값은 인덱스로 등록되고 여러행이 동일한 값을 가질 수 있다.

📌일대다 조인 테이블


일대다중 다에 해당하는 CHILD컬럼에 유니크 제약조건이 걸려야한다.(기본키여서 유니크제약조건이 걸려있음)

//Parent
@Entity
public class Parent {

    @Id
    @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;

    private String name;

    @OneToMany
    @JoinTable(name = "PARENT_CHILD",
            joinColumns = @JoinColumn(name = "PARENT_ID"),
            inverseJoinColumns = @JoinColumn(name = "CHILD_ID"))
    private List<Child> child = new ArrayList<Child>();
	...
}
//Child
@Entity
public class Child {

    @Id
    @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;

    private String name;

}

조인테이블을 이용해서 단방향 일대다 매핑을 하였다.

📌다대다 조인 테이블


다대다 관계를 만들려면 조인 테이블의 두 컬럼을 합해서 하나의 복합 유니크 제약조건을 걸어야한다.

//Parent
@Entity
public class Parent {

    @Id
    @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;

    private String name;

    @ManyToMany
    @JoinTable(name = "PARENT_CHILD",
            joinColumns = @JoinColumn(name = "PARENT_ID"),
            inverseJoinColumns = @JoinColumn(name = "CHILD_ID"))
    private List<Child> child = new ArrayList<Child>();
	...
}
//Child
@Entity
public class Child {

    @Id
    @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;

    private String name;
	...
}

5. 엔티티 하나에 여러 테이블 매핑

잘 사용하지는 않지만 @SecondaryTable을 사용하면 한 엔티티에 여러테이블을 매핑할 수 있다.

코드를 보자.

//Parent
@Entity
@Table(name="BOARD")
@SecondaryTable(name = "BOARD_DETAIL", pkJoinColumns = @PrimaryKeyJoinColumn(name="BOARD_DETAIL_ID"))
public class Board {

    @Id
    @GeneratedValue
    @Column(name = "BOARD_ID")
    private Long id;

    private String title;

    @Column(table = "BAORD_DETAIL")
    private String content;
	...
}

@SecondaryTable을 사용해서 BOARD_DETAIL 테이블을 추가로 매핑했다.

@SecondaryTable 속성

  • @SecondaryTable.name : 매핑할 다른 테이블의 이름
  • @SecondaryTable.pkJoinColumns : 매핑할 다른 테이블의 기본 키 컬럼 속성지정
profile
기초를 중요시하는 백엔드개발자입니당

0개의 댓글