[JPA-07] 고급매핑

이가희·2024년 12월 7일
1

JPA

목록 보기
7/16
post-thumbnail

이번시간에는 JPA 고급 매핑을 알아보겠다.
실무에서 잘 쓰이지 않는 개념도 있으니, 필요한 것만 알아가도 좋을 것 같다.

Chapter

  1. 상속 관계 매핑
  2. 복합 키와 식별 관계 매핑
  3. 조인 테이블
  4. 엔티티 하나에 여러 테이블 매핑

1. 상속 관계 매핑

관계형 데이터베이스에는 상속이라는 개념과 유사한 슈퍼타입 서브타입 관계 라는 모델링 기법이 있다.

이 슈퍼타입 서브타입 논리 모델을 물리 모델인 테이블로 구현할 땐 3가지 방법을 택할 수 있다.
(각각의 테이블로 변환 / 통합 테이블로 변환 / 서브타입 테이블로 변환)

조인 전략 (각각의 테이블로 변환)

조인 전략은 엔티티 각각을 모두 테이블로 만들고, 자식 테이블이 부모테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략이다.
조회 할 때 조인을 자주 사용하게 되며, 이 전략을 사용할 때 주의할 점으로
반드시 부모 엔티티에 타입을 구분하는 컬럼을 추가해야 (=여기서는 DTYPE) 한다.
-> 객체는 타입으로 구분할 수 있지만, 테이블은 타입의 개념이 없기 때문에 추가해 주어야 함.

코드로 살펴보자.

@Entity
@Inheritance (strategy = InheritanceType.JOINED) //(1)
@DiscriminatorColumn (name = "DTYPE") //(2)
public abstract class Item {
	
    @Id @GeneratedValue
    @Column (name = "ITEM_ID")
    private long id;
    
    private String name;
}

@Entity
@DiscriminatorValue("A")
@PrimaryKeyJoinColumn (name = "FOOD_ID") //(4)
public class Food extends Item{
	private String chef;
}

//... Movie, Book 도 매핑...

(1) @Inheritance(strategy = InheritanceType.JOINED)
: 상속 매핑은 부모 클래스에 @Inheritance 를 사용 해야 하고, 매핑 전략을 지정해야 함. 여기서는 조인 전략을 사용하므로 InheritanceType.JOINED 를 사용함.
(2) @DiscriminatorColumn(name ="DTYPE")
: 부모 클래스에 구분 컬럼을 지정하는 어노테이션. 기본값이 DTYPE 이므로 여기서는 @DiscriminatorColumn 으로 줄여 사용해도 무관함.
(3) @DiscriminatorValue("A")
: 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정함. 여기서 푸드 엔티티를 저장하면 DTYPE에 A 가 저장됨.
(4) @PrimaryKeyJoinColumn
: 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용하는데, 자식 테이블의 기본 키 컬럼명을 변경하고 싶으면 이 어노테이션을 이용하면 됨.

✅ 조인 전략의 장단점

  • 장점
  1. 테이블이 정규화 됨
  2. 외래 키 참조 무결성 제약조건을 활용할 수 있음
  3. 저장공간을 효율적으로 사용함
  • 단점
  1. 조회할 때 조인이 많이 사용되어 성능이 저하될 수도 있음
  2. 조회 쿼리가 복잡해 질 수 있음
  3. 데이터를 등록할 때 INSERT SQL 이 두 번 실행되게 됨

단일 테이블 전략 (통합 테이블로 변환)

단일 테이블 전략은 위의 테이블과 같이 말 그대로, 하나의 테이블에 모든 값을 담아 사용하는 전략이다. 그리고 구분 컬럼 (여기서는 DTYPE) 으로 어떤 자식 테이터가 저장되었는지 구분한다.
이렇게 하면 조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다.

이 전략을 사용할 땐 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해 주어야 한다.
Food 엔티티를 저장하면, 다른 엔티티와 매핑된 director, actor, author은 null 값이 입력되기 때문이다.

코드로 살펴보겠다.

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

@Entity
@DiscriminatorValue("A")
@PrimaryKeyJoinColumn (name = "FOOD_ID") 
public class Food extends Item{
	private String chef;
}

// Movie, Book 도 mapping... //

코드 상으로는 전략을 InheritanceType.SINGLE_TABLE 로 지정한 것 말고는
앞선 코드와 변화한 점은 없다.

✅ 단일 테이블 전략 장단점

  • 장점
  1. 조인이 필요 없어 일반적으로 조회 성능이 가장 빠름
  2. 조회 쿼리가 단순함
  • 단점
  1. 자식 엔티티가 매핑한 컬럼은 모두 Null을 허용해야 함
  2. 단일 테이블에 모든 것을 저장하여 테이블이 커질 수 있고, 그러므로 상황에 따라서 조회 성능이 오히려 느려질 수도 있음

+) @DiscrmininatorValue를 지정하지 않으면 기본적으로 엔티티 이름을 사용한다.

구현 클래스마다 테이블 전략 (서브타입 테이블로 변환)

위의 그림과 같이, 자식 엔티티마다 테이블을 만들고, 자식 테이블 각각에 필요한 컬럼을 모두 넣는 전략이다.

코드로 알아보겠다.

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

@Entity
public class Food extends Item{
	private String chef;
}

// Movie, Book 도 mapping... //

이전 전략들과 가장 큰 차이점은 구분 컬럼을 사용하지 않는 것이다.
당연히, 필요한 모든 컬럼들이 있는 테이블을 자식 엔티티 마다 생성해주니 굳이 구분 컬럼이 필요하지 않다.

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

  • 장점
  1. 서브 타입을 구분해서 처리해야 할 때 효과적
  2. not null 제약조건을 사용할 수 있음
  • 단점
  1. 여러 자식 테이블을 함께 조회할 때 성능이 느림 (SQL 에 UNION 을 사용해야 함)
  2. 자식 테이블을 통합해서 쿼리하기 어려움

DB 설계자와 ORM 전문가 모두 추천하지 않는 전략이다. 가급적 사용하지 말자.

+ @MappedSuperClass
부모 클래스는 테이블과 매핑하지 않고, 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공하고 싶으면 @MappedSuperClass 어노테이션을 사용하면 된다.

흔히 엔티티마다 있는 등록일자, 수정일자, 등록자, 수정자 같은 AUDIT 컬럼과 같이 여러 엔티티에서 공통으로 사용하는 속성을 관리하고자 할 때 유용하다.

코드로 살펴보자.
Member 와 Seller에 공통으로 id 와 name 컬럼이 있다고 가정했을 때, 테이블은 그대로 두고, 이 두 공통 속성을 부모 클래스로 모으고 객체 상속 관계로 만들어 보겠다.

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

@Entity
public class Member extends BaseEntity {
	private String email;
    // ID와 NAME은 상속받기 때문에
    // 자식 엔티티에서 매핑할 이유가 없다
}

//Seller 도 Member 와 유사하게 매핑시켜 준다...

부모로부터 물려받은 매핑 정보를 재정의 하려면 @AttributeOverrides 나 @AttributeOverride를 사용하고,
연관관계를 재정의하려면 @AssociationOverrides 나 @AssociationOverride를 사용하면 된다.

사용 예시

@Entity
@AttributeOverrides ( {
	@AttributeOverride (name = "id", column = @Column(name = "MEMBER_ID)),
    @AttributeOverride (name = "name", column = @Column(name = "MEMBER_NAME))
        })
public class Member extends BaseEntity (...)

// 부모의 id 속성의 컬럼명을 MEMBER_ID 로,
// 부모의 name 속성의 컬럼명을 MEMBER_ID 로 재정의 하였다.

@MappedSupperclass로 지정한 클래스는 엔티티가 아니므로, 당연히 em.find() 나 JPQL에서 사용할 수 없다.
그리고 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것이 권장된다.

정리하자면 테이블에 매핑되지 않고, 여러 엔티티에서 사용하는 공통 컬럼들을 관리하고자 할 때 사용되는 어노테이션이다.


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

용어 알고 가기
식별 관계 : 부모 테이블의 기본 키를 내려 받아 자식 테이블에서 기본 키 + 외래 키로 사용하는 관계
비식별 관계 : 부모 테이블의 기본 키를 자식 테이블에서 외래 키로만 사용하는 관계
필수적 비식별 관계 : 외래 키에 NULL 허용 X
선택적 비식별 관계 : 외래 키에 NULL 허용 0

복합 키 : 비식별 관계 매핑
앞선 JPA 포스팅에서 말했듯 복합 키를 사용하려는 경우 별도의 식별자 클래스를 만들고 , 그곳에 equals 와 hashCode를 구현해야 한다.
JPA에서는 복합 키를 지원하기 위해 @IdClass 와 @EmbeddedId 2가지 방법을 제공하고 있다.
여기서 @IdClass 는 관계형 데이터베이스에 가깝고, @EmbeddedId는 좀 더 객체지향에 가까운 방법이다.

위의 관계를 먼저 @IdClass 로 표현 해 보겠다.


//식별자 클래스
public class ParentId implements Serializable {
	private String id1; //Parent.id1 에 매핑 될 것임
   	private String id2; //Parent.id2 에 매핑 될 것임
    
    //equals, hashCode override 하기
}

//부모 클래스
@Entity
@IdClass (ParentId.class ) //앞서 만든 식별자 클래스를 넣어줌
publi class Parent {
	@Id
    @Column (name = "PARENT_ID1")
    private String id1; //ParentId.id1과 연결. 둘의 필드 이름이 같아야 함.
    
    @Id
    @Column (name = "PARENT_ID2")
    private String id2; //ParentId.id2와 연결. 마찬가지로 둘의 필드 이름이 같아야 함.
    
    private Strng name;
}


//사용 예시 (1) - 저장
Parent parent = new Parent();
parent.setId1("id1");
parent.setId2("id2");
parent.setName("name");
entityManager.persist(parent);
//영속성 컨텍스트에 등록하기 직전에, 내부에서 Parent.id1, Parent.id2의 값을 사용해서 식별자 클래스인 ParentId를 생성하고 영속성 컨텍스트의 키로 사용함.

//사용 예시 (2) - 조회
ParentId = parentId = new ParentId("id1","id2");
Parent parent = entityManager.find(Parent.class , parentId);
//식별자 클래스인 ParentId를 사용해서 엔티티를 조회하였음.


//자식 클래스
@ntity
public class Child {
	
    @Id
    private String id;
    
    @ManyToOne
    @JoinColums( {
    	@JoinColumn (name = PARENT_ID1",
        	referencedColumnName = "PARENT_ID1"),
        @JoinColumn (name = "PARENT_ID2",
        	referencedColumnName = "PARENT_ID2")
    } )
    private Parent parent;
    
    //자식 테이블의 복합 외래 키를 위와 같이 표현함.
    // 각각의 외래 키 컬럼을 @JoinColumn으로 매핑하고,
    // 위의 예시처럼 @JoinColumn 의 name 속성과
    // referencedColumnName 속성의 값이 같으면 referencedColumnName은 생략해도 무방함.

이제 @EmbeddedId로 표현 해 보겠다.

//식별자 클래스
@Embeddable  //이 어노테이션을 붙여줘야 함
public class ParentId implements Serializable {
	
    @Column (name = "PARENT_ID1")
    private String id;
    @Column (name = "PARENT_ID2")
    private String id2;
    
    //equals , hashCode overring 구현...
 }
 
 //부모 클래스
 @Entity
 public class Parent {
 	@EmbeddedId
    private ParentId id;
    
    priavte String name;
}

//사용 예시 1 - 저장
Parent parent = new Parent();
ParentId parentId = new ParentId("id1","id2");
parent.setId(parentId);
parent.setName("name");
entityManage.persist(parent);
//이번에는 직접 parentId를 생성해서 사용한다.

//사용 예시 2 - 조회
ParentId parentId = new ParentId("id1","id2");
Parent parent = entityManger.find(Parent.class, parentId);
//조회도 parentId를 직접 사용해서 조회한다.

👊 IdClass vs EmbeddedId
각각의 장단점이 있어 취향껏 선택하면 되는데,
특정 상황에서 @EmbeddedId가 JPQL이 조금 더 길어질 수도 있다.
(ex:
"select p.id.id1, p.id.id2 from Parent p " //embeddedId
"select p.id1, 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; 
    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 = "GRANDHCILD_ID")
   private String id;
   
   priavet String name;
}

//손자 ID 클래스 구현... 앞선 Child 와 유사하게 구현하면 됨...
        

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

@EmbeddedId와 식별 관계
@EmbeddedId 로 식별 관계를 나타낼 때는 @MapsId를 사용해야 한다.

//부모 엔티티는 위와 동일하다.

//자식
@Entity
public class Child {
	@EmbeddedId
    private ChildId id;
    
    @MapsId("parentId") //ChildId.parentId에 매핑
    @ManyToOne
    @JoinColumn (name = "PARENT_ID")
    public Parent parent;
    
    priavet String name;
}

//자식 ID
@Embeddable
public class ChildId implements Serializable {
	private String parentId; //@MapsId("parentId")로 매핑
    @Column (name = "CHILD_ID")
    private String id;
    
    //equals, hashCode overriding
 }

코드에서 볼 수 있듯이,
@MapsId는 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻이다.

비식별 관계로 구현
비식별 관계로 구현하면, 앞선 포스팅에서 보았듯이
기본키나 외래 키가 복합키로 구성되지 않기에 매핑도 쉽고 코드도 단순해진다.
그리고 무엇보다 따로 복합 키 클래스를 만들지 않아도 된다.

//자식
@Entity
public class Child {
	
    @Id @GeneratedValue
    @Column (name = "CHILD_ID")
    private Long id;
    
    @ManyToOne
    @JoinColumn (name = "PARENT_ID")
    private Parent parent;
}

//손자
@Entity
public class GrandChild {
	
    @Id @GeneratedValue
    @Column (name = "GRANDCHILD_ID")
    private Long id;
    
    @ManyToOne
    @JoinColumn (name = "CHILD_ID")
    private Child child;
}

일대일 식별 관계
일대일 식별 관계의 경우, 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만 사용하게 된다. 따라서 부모 테이블의 기본 키가 복합 키가 아니라면 자식 테이블의 기본 키는 복합 키로 구성하지 않아도 된다.
이 경우 연관관계이 주인 테이블에서 @MapsId를 사용하고 속성 값은 비워두면 된다.

식별, 비식별 관계에서 보통 비식별 관계가 선호됨.
식별관계의 경우 부모 테이블 기본 키를 자식에게 전파하면서, 자식 테이블의 기본 키 컬럼이 점점 늘어나게 됨. -> 조인 시 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있음.
다만 식별 관계는 기본 키 인덱스를 활요하기 좋고, 특정 상황에서 조인 없이 하위 테이블만으로도 검색을 완료할 수 있기에 상황에 따라 식별 관계에서도 큰 이점을 얻을 수 있음.


3. 조인 테이블

그 동안 조인 컬럼을 사용 (외래 키)하여 연관관계를 설계 해 보았다.
이번에는 조인 테이블을 사용해서 연관관계를 설계하겠다.

조인 테이블을 사용하려면, 연관 관계를 관리하는 조인 테이블을 추가로 생성하고, 여기에 두 테이블의 외래 키를 가지고 연관관계를 관리해야 한다.
이 전략의 가장 큰 단점은 테이블을 하나 더 생성해야 한다는 점이다.
따라서 기본적으로는 조인 컬럼 전략을 사용하고 필요하다고 생각되면 조인 테이블을 사용하는 것이 좋다
.

일대일 조인 테이블

//parent

@Entity
public class Parent {
	@Id @GeneratedValue
    @Colun (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
public class Child {
	@Id @GeneratedValue
    @Column (name = "CHILD_ID")
    private Long id;
    priave String name;
}

@JoinColumn 대신에 @JoinTalbe을 사용한 것을 알 수 있다.

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

일대다, 다대일 조인 테이블은 앞선 포스팅을 다 읽었다면 무리 없이 구현할 수 있을 것이다.

테이블 관계만 짚어보자면,
일대다 관계에서는 조인 테이블의 컬럼 중 다와 관련된 컬럼에 유니크 제약조건을 걸어야 한다.
다대다 관계에서는 조인 테이블의 두 컬럼을 합해서 하나의 복합 유니트 제약조건을 걸어야 한다.

여기서 예측가능하듯이 조인 테이블에 컬럼을 추가하면 @JoinTable 전략을 사용할 수 없다. 이 경우 새로운 엔티티를 만들어서 조인 테이블과 매핑해야 한다.


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

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

@Entity
@Table (name ="BOARD")
@SecondaryTable (name = "BOARD_DETAIL",
		pkJoinColumns = @PrimaryKeyJoinColumn (name = "BOARD_DETAIL_ID"))
public class Board {
	
    @Id 
    @Column (name = "BOARD_ID"
    private Long id;
    
    private String title;
    
    @Column (talbe = "BOARD_DETAIL")
    private String content;
}

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

  • @SecondaryTable
    name : 매핑할 다른 테이블의 이름
    pkJoinColumns : 매핑할 다른 테이블의 기본 키 컬럼 속성

여기서 content 필드는 table = "BOARD_DETAIL"을 사용해서 BOARD_TALBLE 의 컬럼과 매핑이 되었다.
title처럼 테이블을 지정하지 않으면 기본 테이블 (=BOARD)에 매핑된다.

이 방법은 항상 두 테이블을 조회하므로 최적화하기가 어렵다.
그래서 테이블 당 엔티티를 가각 만들어서 일대일 매핑하는 것이 권장된다.


다음 시간에는 지연 로딩과 프록시에 대해 알아보겠다.
이번 시간에는 코드가 많았는데, 다시 말하지만 필요한 부분만 살펴보고 넘어가도 좋을 것 같다.

참조 : 자바 ORM 표준 JPA 프로그래밍 : 김영한

profile
안녕하세요 개발하는 사람입니다.

0개의 댓글

관련 채용 정보