개발하는 쿠키
article thumbnail

이번 장에서는

- 상속 관계 매핑

- @MappedSuperclass

- 복합키와 식별 관계 매핑

- 조인 컬럼 vs 조인 테이블

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

에 대해 공부한다.

상속 관계 매핑

상속관계 논리모델, 물리모델

슈퍼타입-서브타입 논리모델을 실제 테이블로 구현하기 위한 방법

1. 각각의 테이블로 변환: 모두 테이블로 만들고, 테이블끼리 조인하는 전략

2. 통합 테이블로 변환: 단일 테이블만 사용하는 전략

3. 서브타입 테이블로 변환: 서브 클래스마다 테이블을 생성하는 전략

 

조인 전략

조인 전략

엔티티 각각을 테이블로 만들고 테이블끼리의 조인을 사용해서 조회한다.

슈퍼타입의 DTYPE 컬럼을 이용해서 서브타입 종류를 구분한다.

// 슈퍼타입
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE") // 구분 컬럼
public abstract class Item {
	...
}

// 서브타입
@Entity
@DiscriminatorValue("A") // 구분 컬럼에 입력할 값
public class Album extends Item {
    private String artist;
}

서브타입에서 @PrimaryKeyJoinColumn(name = "ALBUM_ID")를 사용하면 ID를 재정의할 수 있다.

장점

- 테이블이 정규화된다.

- 외래 키 참조 무결성 제약조건을 활용할 수 있다.

- 저장공간을 효율적으로 사용한다.

단점

- 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.

- 조회 쿼리가 복잡하다.

- 데이터를 입력할 때 INSERT SQL이 2번 실행된다.

 

단일 테이블 전략

단일 테이블 전략

테이블 1개에 모든 컬럼을 넣고 DTYPE컬럼으로 서브 타입을 구분한다.

DTYPE에 해당하는 서브타입이 아닌 컬럼들은 null을 입력한다.

// 슈퍼타입
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE") // 구분 컬럼
public abstract class Item {
	...
}

// 서브타입
@Entity
@DiscriminatorValue("A") // 구분 컬럼에 입력할 값
public class Album extends Item {
    private String artist;
}

장점

- 조인이 필요 없으므로 조회 성능이 좋다.

- 조회 쿼리가 단순하다.

단점

- 자식 엔티티 컬럼은 Null허용해야 한다.

- 단일 테이블에 모든 컬럼을 저장하므로 테이블이 커질 수 있다. 컬럼 수가 많다면 조회 성능이 떨어질 수 있다.

 

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

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

자식 엔티티들만 테이블을 만든다. 자식 테이블에 부모 테이블의 컬럼도 들어있다.

부모 엔티티의 ID를 통합해서 관리하지 않고 자식 엔티티들이 각각 관리하기 때문에 추천하지 않는다.

ex. ITEM_ID = 1, 2, 3, 4, 5 가 있다.

ALBUM에는 ITEM_ID 1, 3

MOVIE에는 ITEM_ID 2

BOOK에는 ITEM_ID 4, 5 가 있을 때 ITEM_ID = 2인 상품을 찾고 싶으면 ALBUM, MOVIE, BOOK을 UNION 해서 통합한 후 where조건문으로 조회해야 한다.

// 슈퍼타입
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
	...
}

// 서브타입
@Entity
public class Album extends Item {
    private String artist;
}

장점

- 서브타입을 구분해서 처리할 때 효과적이다.

- Not null 제약조건을 사용할 수 있다.

단점

- 여러 자식 테이블을 함께 조회할 때 성능이 느리다. UNION을 사용해야 한다.

- 자식 테이블을 통합해서 쿼리하기 어렵다.

 

@MappedSuperclass

여러 엔티티들의 공통 속성들을 모아서 BaseEntity 클래스를 만든 후 @MappedSuperclass 어노테이션을 붙이면 자식 엔티티들에는 공통 속성이 자동으로 적용된다.

// 슈퍼 타입
@MappedSuperclass
public abstract class BaseEntity {
    // 직접적으로 이 클래스를 활용하지는 않으므로 추상 클래스로 만든다.
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;

}

// 서브 타입
@Entity
@AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID")) // 선택사항, BaseEntity의 id를 Member에서 MEMBER_ID로 재정의
public class Member extends BaseEntity {
    private String email;
}

보통 등록일자, 수정일자, 등록자, 수정자 같은 공통 컬럼들을 슈퍼 타입에 넣어서 관리한다.

@Getter
@SuperBuilder
@MappedSuperclass
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
@ToString
public abstract class BaseEntity {
    /**
     * 저장일시
     */
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime registerDateTime;

    /**
     * 수정일시
     */
    @LastModifiedDate
    private LocalDateTime changeDateTime;
    
    /**
     * 등록자
     */
    @CreatedBy
    private String registerEmployeeNumber;
    
    /**
     * 수정자
     */
    @LastModifiedBy
    private String changeEmployeeNumber;
}

복합 키와 식별 관계 매핑

식별 관계 vs 비식별 관계

식별관계

식별 관계

부모 테이블의 기본키를 자식 테이블의 (기본키, 외래키)로 사용한다.

부모가 있어야 자식이 있을 수 있다.

 

비식별 관계

비식별 관계

부모 테이블의 기본키가 자식 테이블의 외래키로만 사용한다.

- 필수적 비식별 관계: 외래키에 null 허용 X

- 선택적 비식별 관계: 외래키에 null 허용 O

 

복합 키: 비식별 관계 매핑

@Id 어노테이션이 붙는 컬럼이 2개 이상이면 별도의 클래스를 만들어야 한다.

별도의 클래스를 @IdClass, @EmbeddedId 2가지 방법을 사용해서 만들 수 있다.

 

@IdClass

복합 키 테이블

// 식별자 클래스
public class ParentId implements Serializable {
    // id1, id2 속성명이 같아야 한다.
    private String id1; //Parent.id1 매핑
    private String id2; //Parent.id2  매핑
    
    ...
    // eqauls and hashCode 구현
}

// 엔티티
@Entity
@IdClass(ParentId.class)
public class Parent {

    @Id
    @Coulumn(name = "PARENT_ID1") // 식별자 클래스를 참조한다.
    private String id1; // ParentId.id1과 연결
    
    @Id
    @Coulumn(name = "PARENT_ID2")
    private String id2; // ParentId.id2 연결
    
    private String name;
    ...
}

 

@EmbeddedId

// 식별자 클래스
@Embeddable
public class ParentId implements Serializable {
	
    @Column(name = "PARENT_ID1")
    private String id1;
    @Column(name = "PARENT_ID2")
    private String id2;
    
    // eqauls and hashCode 구현

}

// 적용할 엔티티
@Entity
public class Parent {

    @EmbededId // 식별자 클래스를 엔티티 내에 내장한다.
    private ParentId id;
    
    private String name;
    ...
}
// 자식 클래스를 사용한 복합 키 매핑
@Entity
public class Child {
    @Id
    private String id;
    
    @ManyToOne
    @JoinColumns({
    	@JoinColumn(name = "PARENT_ID1",
        	referencedColumnName = "PARENT_ID1"),
        @JoinColumn(name = "PARENT_ID2",
        	referencedColumnName = "PARENT_ID2")
    })
    private Parent parent;
}

복합 키: 식별 관계 매핑

@IdClass

// 자식 클래스를 사용한 복합 키 매핑
@Entity
public class Child {
    @Id
    @ManyToOne
    @JoinColumns({
    	@JoinColumn(name = "PARENT_ID1",
        	referencedColumnName = "PARENT_ID1"),
        @JoinColumn(name = "PARENT_ID2",
        	referencedColumnName = "PARENT_ID2")
    })
    private Parent parent;
    
    @Id
    @Column(name = "CHILD_ID")
    private String childId;
}

- 식별자 클래스의 속성명, 엔티티에서 사용하는 속성명이 같아야 한다.

- Serializable 인터페이스를 구현해야 한다.

- equals, hashCode를 구현해야 한다.

- 기본 생성자가 있어야 한다.

- 식별자 클래스는 public이어야 한다.

 

 

@EmbeddedId

// 자식 클래스를 사용한 복합 키 매핑
@Entity
public class Child {
    @MapsId("parentId")
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    private Parent parent;
    
    @EmbeddedId
    @Column(name = "CHILD_ID")
    private String childId;
}

- Serializable 인터페이스를 구현해야 한다.

- equals, hashCode를 구현해야 한다.

- 기본 생성자가 있어야 한다.

- 식별자 클래스는 public이어야 한다.

 

일대일 식별 관계

식별 관계 일대일

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

식별관계보다는 < 비식별관계를 사용하는 것이 좋다. 

식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다. (BOARD_ID 가 BoardDetail 테이블에서도 ID로 사용할 수 있는 경우는 별로 없다.)

복합키로 사용되는 속성은 시간의 흐름에 따라 바뀔 가능성이 있다. 그렇기 때문에 비식별 관계로 두고, 새로운 ID 컬럼을 1개만 만드는 것이 좋다. (Long 타입이 좋음)

그리고 비식별 관계를 Null허용으로 만드는 것보다, NotNull 속성을 사용해서 항상 관계가 있다는 것을 보장하는 것이 좋다.

 

활용 방법

현재 회사에서는 @IdClass를 활용해서 다음과 같이 사용하고 있다. 

// 식별자 클래스
@Data
public class ParentId implements Serializable {
    // id1, id2 속성명이 같아야 한다.
    private String id1; //Parent.id1 매핑
    private String id2; //Parent.id2  매핑
}


// 엔티티
@Entity
@IdClass(ParentId.class)
public class Parent {

    @Id
    @Coulumn(name = "PARENT_ID1") // 식별자 클래스를 참조한다.
    private String id1; // ParentId.id1과 연결
    
    @Id
    @Coulumn(name = "PARENT_ID2")
    private String id2; // ParentId.id2 연결
    
    private String name;
    ...
}

@Data 어노테이션은  @Getter, @Setter, @RequiredArgsConstructor, @ToString, @EqualsAndHashCode 를 합친 기능을 한다.

따라서 @IdClass 필수조건 중 2가지를 만족한다. (equals, hashCode를 구현해야 한다. 기본 생성자가 있어야 한다.)

 

복합키 클래스에는 왜 Serializable을 반드시 구현해야 할까?
@Data
public class ParentId {
    // id1, id2 속성명이 같아야 한다.
    private String id1; //Parent.id1 매핑
    private String id2; //Parent.id2  매핑
}

위와 같이 Serializable을 구현하지 않으면 아래와 같은 에러 메시지가 뜬다.

The session object needs to be serializable hence all objects referenced by it must be serializable as well. The id is used as a key to index loaded objects in the session. In case of CompositeId s the class itself is used as the id

세션에서 객체가 로드될 때 Id를 키로 사용하는데 복합키 클래스의 경우 클래스 자체를 Id로 사용한다.

따라서 복합키 클래스에는 Serializable을 구현해야 한다. 클래스를 바이트로 압축한다.

 

인프런 김영한님 답변에서는 영속성 컨텍스트 내부 1차 캐시에 엔티티를 저장할 때 바이트코드로 변환한 키를 이용한다고 나와있다. 

 

@IdClass, @EmbeddedId 차이가 뭘까?
em.createQuery("select p.id.id1, p.id.id2 from Parent p"); // @EmbeddedId
em.createQuery("select p.id1, p.id2 from Parent p"); // @IdClass

@EmbeddedId는 별도의 ParentId클래스를 통해 id1, id2에 접근하기 때문에 p.id.id1를 사용한다.

@IdClass는 id1, id2가 Parent entity의 속성이라 바로 접근할 수 있기 때문에 p.id1, p.id2를 사용한다. 

 

interface Repository 는 어떻게 만들까?
import org.springframework.data.jpa.repository.JpaRepository;

public interface ParentRepository extends JpaRepository<Parent, ParentId> {
    // JpaRepository 안에 <엔티티 클래스, 복합키 클래스> 를 써야 한다.
    // Additional custom methods if needed
}

조인 테이블

데이터베이스 테이블의 연관관계는 조인 컬럼 또는 조인 테이블을 사용해서 만들 수 있다.

조인 컬럼 vs 조인 테이블

단점

조인 컬럼은 외래 키에 null을 허용해야 한다는 점,

조인 테이블은 테이블 1개를 추가해야 한다는 점이 단점이다.

 

조인 컬럼을 주로 사용하고, 필요할 때 조인 테이블을 사용하자.

 

일대일 조인 테이블

일대다 조인 테이블

다대일 조인 테이블

다대다 조인 테이블

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

Board 엔티티에 BOARD 테이블, BOARD_DETAIL 테이블 2개를 매핑할 수 있다.

하지만, 엔티티를 각각 만드는 것을 권장한다.

 

정리

이번 장에서는

- 상속 관계 매핑 : 조인 전략, 단일 테이블 전략을 선호한다.

- @MappedSuperclass: 공통으로 사용하는 컬럼들을 1개의 클래스에 넣을 때 사용한다.

- 복합키와 식별 관계 매핑: 필수적 비식별 관계를 사용하는 것이 좋다.

- 조인 컬럼 vs 조인 테이블: 기본적으로 조인 컬럼을 사용하고 필요할 때 조인 테이블을 사용하자.

- 엔티티 하나에 여러 테이블 매핑: 왠만하면 테이블 1개당 엔티티 1개를 사용하는 것이 좋다.

에 대해 공부했다.

 


참고자료

- https://www.yes24.com/Product/Goods/19040233 "자바 ORM 표준 JPA 프로그래밍-김영한"

반응형
profile

개발하는 쿠키

@COOKIE_

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!