티스토리 뷰

이전 글: https://gojs.tistory.com/46

 

JPA 연관관계 매핑 (1)

관계형 데이터베이스의 테이블간의 관계를 객체간의 연관관계로 표현할 수 있다.크게 단방향 연관관계과 양방향 연관관계로 구분할 수 있으며 이에 대해 알아보자. 단방향 연관관계회원과 팀

gojs.tistory.com

 

이전 글에서 JPA에서의 기본적인 연관관계 매핑하는 방법에 대해서 알아보았다.

그렇다면 실제로 충분히 마주하게 될만한 특수한 케이스들에 대해서 연관관계 매핑을 어떻게 할 수 있을지 알아보자.

 

상속 관계 매핑

상속 관계의 도메인 모델

위와 같이 상속 관계를 포함한 도메인 모델을 테이블로 구현할 때에는 크게 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;

    ...
}

@Entity
@DiscriminatorValue("A")
public class Album extends Item {

    private String artist;
    ...
}

@Entity
@DiscriminatorValue("M")
public class Movie extends Item {

    private String director;
    private String actor;
}

@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID")
public class Book extends Item {

    private String author;
    private String isbn;
}

위 코드에서 새로 등장한 어노테이션들은 아래와 같은 의미를 가지고 있다.

  • @Inheritance(strategy = InheritanceType.JOINED)
    • 부모 클래스에 선언하는 어노테이션
    • 매핑 전략에 따라 속성의 값이 바뀐다
  • @DiscriminatorColumn(name = “DTYPE”)
    • 부모 클래스에 구분 컬럼을 지정
  • @DiscriminatorValue(”M”)
    • 엔티티 저장 시 구분 컬럼에 입력할 값을 지정
  • @PrimaryKeyJoinColumn(name = “BOOK_ID”)
    • 기본적으로 자식 테이블은 부모 테이블의 ID 컬럼명 사용
    • ID 컬럼명 명시하여 사용할 필요가 있을 때 해당 어노테이션 사용

 

조인 전략으로 상속 관계의 도메인 모델을 구현하는 경우 아래와 같은 특징이 있다.

  • 장점
    • 테이블이 정규화된다.
    • 외래 키 제약조건을 활용할 수 있다.
    • 저장공간을 효율적으로 사용한다.
  • 단점
    • 조회 시 조인이 발생하여 성능 저하될 수 있다.
    • 조회 쿼리가 복잡하다.
    • 데이터 등록 시 insert 쿼리 두 개 발생한다.

 

하나의 통합 테이블로 변환

부모-자식 관계의 도메인 모델을 하나의 통합 테이블로 변환하는 전략을 단일 테이블 전략이라고 한다.

@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
@DiscriminatorValue("A")
public class Album extends Item {
    private String artist;
}

@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
    private String director;
    private String actor;
}

@Entity
@DiscriminatorValue("B")
public class Book extends Item {
    private String author;
    private String isbn;
}

단일 테이블 전략을 사용하기 위해 @Inheritance 어노테이션의 value를 InheritanceType.SINGLE_TABLE로 설정했다.

테이블 하나에 모든 것을 통합하므로 구분 컬럼을 필수적으로 사용해야 하며 각 컬럼들은 null이 허용된다.

  • 장점
    • 조인이 필요 없으므로 조회 성능이 빠르다
    • 조회 쿼리가 단순하다
  • 단점
    • 자식 엔티티가 매핑한 컬럼은 모두 nullable하다
    • 테이블이 커질 수 있어서 오히려 성능이 느려지는 경우가 발생할 수 있다

 

서브타입 테이블로 변환

각 자식 엔티티마다 테이블을 만드는 전략을 구현 클래스마다 테이블 전략이라고 한다.

@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 {
    private String artist;
}

@Entity
public class Movie extends Item {
    private String director;
    private String actor;
}

@Entity
public class Book extends Item {
    private String author;
    private String isbn;
}

구현 클래스마다 테이블 전략을 사용하기 위해 @Inheritance 어노테이션의 value를 InheritanceType.TABLE_PER_CLASS로 설정했다.

 

  • 장점
    • 서브 타입을 구분해서 처리할 때 효과적이다
    • not null 제약조건을 사용할 수 있다
  • 단점
    • 여러 자식 테이블을 한번에 조회할 때 성능이 느리다 (UNION)
    • 자식 테이블을 통합해서 쿼리하기 어렵다
    • 일반적으로 비추천

 

@MappedSuperclass

@MappedSuperclass 어노테이션을 부모 클래스에 선언하게되면 부모 클래스는 테이블 매핑을 하지 않고 이를 상속받는 자식 클래스만 테이블로 매핑한다.

@MappedSuperclass
public abstract class BaseEntity {
    @Id @GeneratedValue
    private Long id;
    private String name;
}

@Entity
public class Member extends BaseEntity {
    private String email;
}

@Entity
public class Seller extends BaseEntiry {
    private String shopName;
}

BaseEntity에 공통적인 정보를 정의하여 자식 엔티티들은 상속을 통해 BaseEntity의 매핑정보를 물려받았다.

부모로부터 물려받은 정보를 재정의 하기 위해서는 아래의 어노테이션을 사용하면 된다.

  • @AttributeOverrides
  • @AssociationOverrides

 

복합 키와 식별 관계 매핑

테이블 사이에는 외래 키가 기본 키가 속하는지의 여부에 따라 식별/비식별 관계로 나눈다.

비식별 관계는 외래 키의 nullable에 따라 필수적/선택적 비식별 관계로 구분된다.

비식별관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 것을 추천한다.

 

비식별 관계 매핑 (복합 키)

@Entity
public class Parent {
    @Id
    private String id;

    @Id
    private String id2;
}

복합 키를 기본 키로 엔티티에 매핑하기 위해서 위와 같은 코드를 작성하면 매핑 예외가 발생한다.

JPA에서는 영속성 컨텍스트에서 엔티티를 구분하기 위해 hashCode() , equals() 메서드를 사용한다.

따라서 복합 키를 지원하는 @IdClass, @EmbeddedId를 사용하고 hashCode(), equals()메서드를 오버라이드 해야한다.

 

@IdClass

@Entity
@IdClass(ParentId.class)
public class Parent {
    @Id
    @Column(name = "PARENT_ID1")
    private String id1;

    @Id
    @Column(name = "PARENT_ID1")
    private String id2;

    private String name;

    ...
}
public class ParentId implements Serializable {
	
    private String id1;
    private String id2;

    public ParentId(){}

    public ParentId(String id1, String id2) {
        this.id1 = id1;
        this.id2 = id2;
    }

    @Override
    public boolean equals() {...}

    @Override
    public int hashCode() {...}
}

@IdClass를 사용할 때 식별자 클래스는 아래의 조건을 만족해야 한다.

  • 식별자 클래스의 속성명 == 엔티티 클래스의 속성명
  • Serializable을 구현
  • equals, hashCode를 구현
  • 기본 생성자 존재
  • public 클래스

 

// 엔티티 등록 전 ParentId 인스턴스를 만들어서 엔티티 클래스의 key로 사용
Parent parent = new Parent();
parent.setId1("id1");
parent.setId2("id2");
parent.setName("name");
em.persist(parent);

// Parent를 복합 키로 조회
ParentId parentId = new ParentId("id1", "id2");
Parent parent = em.find(Parent.class, parentId);

// Child class에서 연관관계를 매핑할 때 @JoinColumns로 복합 키 명시 
@ManyToOne
@JoinColumns({
    @JoinColumn(name = "PARENT_ID1", referencedCloumnName = "PARENT_ID1"),
    @JoinColumn(name = "PARENT_ID2", referencedCloumnName = "PARENT_ID2")	
})
private Parent parent;

위와 같이 @IdClass로 구현한 복합키 엔티티를 핸들링할 수 있다.

 

@EmbeddedId

@EmbeddedId는 @IdClass보다는 좀 더 객체지향적인 방법이다.

@Entity
public class Parent {
    @EmbeddedId
    priavte 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, hashCode
    ...
}

@EmbeddedId는 직접 식별자 클래스에 기본 키를 직접 매핑한다. @EmbeddedId를 적용한 식별자 클래스는 아래의 조건을 만족해야 한다.

  • @Embeddable을 붙여준다.
  • Serializable을 구현
  • equals, hashCode를 구현
  • 기본 생성자
  • public 클래스

 

// 엔티티 등록
Parent parent = new Parent();
ParentId parentId = new ParentId("id1", "id2");
parentId.setId(parentId);
parent.setName("name");
em.persist(parent);

// Parent를 복합 키로 조회
ParentId parentId = new ParentId("id1", "id2");
Parent parent = em.find(Parent.class, parentId);

위와 같이 @EmbeddedId로 구현한 복합키 엔티티를 핸들링할 수 있다.

 

복합 키와 equals(), hashCode()

ParentId id1 = new ParentId("id1", "id2");

ParentId id2 = new ParentId("id1", "id2");

Assertions.assertTrue(id1.equals(id2));

JPA에서 같은 값을 가진 식별자 클래스는 같다고 봐야하기 때문에 equals(...) 메서드는 true를 반환해야한다.

따라서 영속성 컨텍스트에서 엔티티를 정상적으로 관리하기 위해서는 equals(), hashCode() 메서드를 오버라이드해야한다.

 

@IdClass vs @EmbeddedId

이 두 어노테이션은 각각의 장단점이 존재하며 본인의 취향에 맞는 것을 일관성 있게 사용하면된다.

EmbeddedId는 보다 객체지향적이지만 특정 JPQL을 사용할 때에는 더 길어질 수 있다.

 

식별 관계 매핑 (복합 키)

@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")
    private Parent parent;

    @Id @Column(name = "CHILD_ID")
    private String childId;

    private String name;	

    ...
}

public class ChildId implements Serializable {

    private String parent;
    private String childId;

    // equals, hashCode
    ...
}

@Entity
@IdClass(GrandChildId.class)
public class GrandChild {

    @Id
    @ManyToOne
    @JoinColumns({
        @JoinColumn(name = "PARENT_ID")
        @JoinColumn(name = "CHILD_ID")
    })
    private Child child;
	
    @Id @Column(name = "GRANDCHILD_ID")
    private String id;

    private String name;

    ...
}

public class GrandChildId implements Serializable {

    private ChildId child;
    private String id;

    // equals, hashCode
    ...
}

 

@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")
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    private Parent parent;

    private String name;	

    ...
}

@Embeddable
public class ChildId implements Serializable {

    private String parentId;
	
    @Column(name = "CHILD_ID")
    private String id;

    // equals, hashCode
    ...
}

@Entity
public class GrandChild {

    @EmbeddedId
    private GrandChildId id;

    @MapsId("childId")
    @ManyToOne
    @JoinColumns({
        @JoinColumn(name = "PARENT_ID")
        @JoinColumn(name = "CHILD_ID")
    })
    private Child child;

    private String name;

    ...
}

@Embeddable
public class GrandChildId implements Serializable {

    private ChildId child;

    @Column(name = "GRANDCHILD_ID")
    private String id;

    // equals, hashCode
    ...
}

외래키가 복합키에 포함되는 경우 복합키 클래스와 엔티티 클래스에 두 번 필드를 정의하였다.

그리고 엔티티 클래스에 @MapsId 어노테이션을 설정해서 두 필드를 매핑하는 과정을 거쳤다.

개인적으로는 @IdClass 보다는 @EmbeddedId를 사용하는 편이 코드가 더 쉽게 읽히고 관계를 이해하기 편해서 선호한다.

 

비식별 관계로 구현 (복합 키 X)

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

    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    private Parent parent;

    private String name;	

    ...
}

@Entity
public class GrandChild {

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

    @ManyToOne
    @JoinColumn(name = "CHILD_ID")
    private Child child;

    private String name;

    ...
}

대리키를 활용하여 비식별 관계를 매핑해보니 복합 키를 사용한 코드보다 훨씬 더 깔끔해졌다.

복합 키가 없기 때문에 복합 키 클래스를 따로 만들지 않아도 된다.

 

식별, 비식별 관계의 장단점

데이터베이스 설계 관점에서 보면 아래와 같은 이유로 비식별 관계를 더 선호한다.

  1. 식별 관계는 하위 테이블로 갈 수록 기본 키 컬럼이 점점 늘어난다. (조인 성능 저하)
  2. 식별 관계는 복합 기본 키를 사용하는 경우가 많아진다.
  3. 식별 관계는 부모 테이블 기본 키를 자식 테이블 기본 키로 사용하므로 테이블 구조가 유연하지 못하다.
  4. 비식별 관계는 대리 키를 사용하기 때문에 비즈니스에 의존없는 설계가 가능하다.

 

객체 관계 관점에서 보더라도 아래와 같은 이유로 비식별 관계를 더 선호한다.

  1. 식별 관계는 복합 키 클래스가 필요한 경우가 많아져서 더 많은 리소스를 쓰게된다.
  2. 비식별 관계는 JPA에서 @GeneratedValue를 사용할 수 있으므로 편리하다.

 

하지만 식별 관계가 가지는 장점도 분명히 존재한다.

  1. 특정 상황에서 하위 테이블만 조회하더라도 원하는 정보를 검색할 수 있다.

⇒ 전반적으로 JPA에서 추천하는 방법은 비식별 관계, Long 타입 대리 키를 사용하는 방법이다.

 

조인 테이블

데이터베이스 테이블의 연관관계를 설계하는 방법은 크게 2가지이다.

  1. 조인 컬럼 사용 (외래 키)
  2. 조인 테이블 사용 (테이블)

일반적으로 다대다 관계에서 조인테이블을 사용하며그 외의 연관관계는 조인 컬럼을 사용한다.

@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<>();

    ...
}

@Entity
public class Child {
	
    @Id @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;

    private String name;

    ...
}

 

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

@SecondaryTable을 사용하면 하나의 엔티티에 여러 테이블을 매핑할 수 있다.

예를 들어 Board Entity - (Board Table + Board_Detail Table)을 매핑해보자.

@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 = "BOARD_DETAIL")
    private String content;

    ...
}

@Table을 사용해서 BOARD 테이블과 매핑하고, @SecondaryTable을 사용해서 BOARD_DETAIL 테이블과도 매핑했다.

pkJoinColumns 속성은 매핑할 다른 테이블의 기본 키 컬럼 속성을 명시한다.

그리고 content 필드에 @Column의 table 속성으로 BOARD_DETAIL 테이블과 따로 매핑을 해주었다.

 

그러나 이와 같은 방법은 항상 두번 조회하기 때문에 최적화가 어렵고, 일대일 매핑에 비해 유연하지도 못하다.

따라서 여러 테이블을 매핑하는 것은 권장하지 않는다.

 

다음 글: https://gojs.tistory.com/48

 

JPA 프록시와 페치 전략

특정 엔티티를 조회할 때 연관된 엔티티를 항상 같이 조회하는 것은 아니다.한번에 모든 연관관계를 로딩하게 되면 너무 많은 데이터를 조회하게 되어서 OOM이 발생할 수도 있다.그렇다면 JPA에

gojs.tistory.com

'공부 > JPA' 카테고리의 다른 글

JPA 값 타입 구현하기  (0) 2024.07.27
JPA 프록시와 로딩 전략  (0) 2024.07.27
JPA Entity 간 연관관계 매핑 (1)  (0) 2024.07.26
JPA Entity 구성하기  (0) 2024.07.24
JPA 영속성 관리 기법  (0) 2024.07.23
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/08   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함