본문 바로가기
Book

자바 ORM 표준 JPA 프로그래밍 - JPA 정리하기 (7-1) 고급매핑

by devLog by Ronnie's 2022. 8. 17.

들어가며


7강은 1편과 2편으로 나눠 진행하였다. 

 

 

7.1 상속 관계 매핑

  • 관계형 디비에는 객체지향 언어에서 다루는 상속 개념이 없다
  • 대신 슈퍼타입 서브타입 관계라는 모델링 기법이 객체의 상속 개념과 가장 유사
  • ORM에서 이야기하는 상속 관계 매핑은 객체의 상속 구조와 디비의 슈퍼타입 서브타입 관계를 매핑하는 것
  • 슈퍼타입 서브타입 논리 모델을 실제 물리 모델인 테이블로 구현 시 3가지 방법 존재
    • 각각의 테이블로 변환 : 각각을 모두 테이블로 만들고 조회할 때 조인을 사용 (JPA에서는 조인 전략이라 함)
    • 통합 테이블로 변환 : 테이블을 하나만 사용해서 통합 (JPA에서는 단일 테이블 전략이라 함)
    • 서브타입 테이블로 변환 : 서브 타입마다 하나의 테이블을 만든다 (JPA에서는 구현 클래스마다 테이블 전략이라 함)

7.1.1 조인 전략

  • 조인 전략은 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략이다.
  • 따라서 조회할 때 조인을 자주 사용
  • 주의점은 객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없으므로 타입을 구분하는 컬럼을 추가해야함
  • DTYPE 컬럼을 구분 컬럼으로 추가하여 사용
  • 매핑 정보 분석
    • @Inheritance(starategy = InheritanceType.JOINED) : 상속 매핑은 부모 클래스에 @Inheritance 를 사용하며 조인 전략을 사용하므로 JOINED 사용
    • @DiscriminatorColumn(name = “DTYPE”) : 부모 클래스에 구분 컬럼을 지정하고 이 컬럼으로 저장된 자식 테이블을 구분 가능, DTYPE이 기본값이다.
    • @DiscriminatorValue(“M”) : 엔티티를 저장할 때 구분 컬럼에 입력할 값 지정, 해당 어노테이션이 적용된 엔티티 저장 시 구분 컬럼이 DTYPE에 값 M이 저장
  • 기본 값으로 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용하는데 만약 자식 테이블의 기본 키 값 변경하고 싶을땐 @PrimaryKeyJoinColumn을 사용
  • 조인 전략의 장단점
    • 장점
      • 테이블이 정규화된다
      • 외래 키 참조 무결성 제약조건을 활용할 수 있다.
      • 저장공간을 효율적으로 사용한다
    • 단점
      • 조회할 때 조인이 많이 사용되므로 성능 저하
      • 조회 쿼리 복잡
      • 데이터를 등록할 INSERT SQL을 두 번 실행 -> 두번이라는게?
    • 특징
      • JPA 표준 명세는 구분 컬럼을 사용하도록 하지만 하이버네이트를 포함한 몇 몇 구현체는 구분 컬럼 (@DiscriminatorColumn) 없이도 동작함

7.1.2 단일 테이블 전략

  • 테이블을 하나만 사용
  • 구분 컬럼(DTYPE)으로 어떤 자식 데이터가 저장되었는지 구분
  • 조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠름
  • 해당 전략 사용 시 주의점은 자식 엔티티가 매핑한 컬럼을 모두 null을 허용해야 함
  • @Inheritance(strategy = InheritanceType.SINGLE_TABLE) 로 지정하면 단일 테이블 전략 사용
  • 테이블 하나에 모든 것을 통합하므로 구분 컬럼을 필수로 사용해야함
  • 장점
    • 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다
    • 조회 쿼리가 단순하다
  • 단점
    • 자식 엔티티가 매핑한 컬럼을 모두 null을 허용해야 한다
    • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있어 상황에 따라서는 조회 성능이 오히려 느려질 수 있다.
  • 특징
    • 구분 컬럼을 꼭 사용해야한다.
    • 따라서 @DiscriminatorColumn을 꼭 설정해줘야 함
    • @DiscriminatorValue를 지정하지 않으면 기본적으로 엔티티 이름을 사용

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

  • 자식 엔티티마다 테이블을 만듦
  • 자식 테이블 각각에 필요한 컬럼이 모두 있음
  • @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) 를 사용하면 구현 클래스마다 테이블 전략을 사용한다.
  • 일반적으로 추천하지 않는 전략
  • 장점
    • 서브 타입을 구분해서 처리할 때 효과적
    • not null 제약조건을 사용할 수 있음
  • 단점
    • 여러 자식 테이블을 함께 조회할 때 성능이 느리다 (SQL UNION을 사용해야 함)
    • 자식 테이블을 통합해서 쿼리하기 어려움
  • 특징
    • 구분 컬럼을 사용하지 않음

7.2 @MappedSuperclass

  • 7.1에서 알아본 내용인 상속 관계 매핑은 부모 클래스와 자식 클래스를 모두 디비 테이블과 매핑한 것
  • 부모클래스는 테이블과 매핑하지 않고 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공하고 싶으면 @MappedSuperclass를 사용한다
  • 서로 관계가 없는 멤버와 판매자 엔티티가 있을 때 두 객체 모델의 공통 속성인 id, name을 부모 클래스로 모으고 객체 상속 관계를 만들고 해당 부모 클래스인 BaseEntity는 테이블과 매핑할 필요가 없고 자식 엔티티에게 공통으로 사용되는 매핑 정보만 제공하면 되므로 이럴때 @MappedSuperclass를 사용
  • 이때 부모로부터 물려받은 매핑 정보를 재정의하려면 @AttributeOverrides(둘 이상 재정의 시)나 @AttributeOverride를 사용한다.
  • 연관관계를 재정의하려면 @AssociationOverrides나 @AssociationOverride를 사용
  • @MappedSuperclass 특징
    • 테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용
    • @MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용 불가능
    • 이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것을 권장
    • 즉 @MappedSuperclass는 테이블과는 관계가 없고 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모아주는 역할을 할 뿐
    • @MappedSuperclass를 사용하면 등록일자, 수정일자, 등록자, 수정자 같은 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리 가능
    • 참고 : 엔티티는 엔티티이거나 @MappedSuperclass로 지정한 클래스만 상속받을 수 있음

7.3 복합 키와 식별 관계 매핑

7.3.1 식별관계 vs 비식별 관계

  • 디비 테이블 사이에 관계는 외래 키가 기본 키에 포함되는지 여부에 따라 식별 관계와 비식별 관계로 구분
  • 식별 관계
    • 식별 관계는 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래키로 사용하는 관계
  • 비식별 관계
    • 비식별 관계는 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계
    • 외래 키에 NULL 허용 여부에 따라 필수적 비식별 관계와 선택적 비식별 관계로 나눈다.
      • 필수적 비식별 관계 : 외래 키에 NULL을 허용하지 않고, 연관관계를 필수적으로 맺어야 함
      • 선택적 비식별 관계 : 외래 키에 NULL을 허용하고, 연관관계를 맺을지 말지 선택 가능

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

  • JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자 클래스를 만들어야 함
  • JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용
  • 식별자를 구분하기 위해 equals와 hashCode를 사용해서 동등성 비교를 한다
  • 식별자가 하나일때는 보통 자바의 기본 타입을 사용하므로 문제가 없지만 식별자 필드가 2개 이상이면 별도의 식별자 클래스를 만들고 그곳에 equals와 hashCode를 구현해야 한다
  • JPA는 복합 키를 지원하기 위해 @IdClass와 @EmbeddedId 두가지 방법 제공
  • @IdClass
    • 관계형 데이터베이스에 가까운 방법
    • PARENT 테이블에 있는 기본키 두개를 CHILD 테이블이 내려받았을 때 복합 키를 매핑하기 위해 식별자 클래스를 별도로 만들어야 함
    • 각각의 기본 키 컬럼을 @Id로 매핑 후 @IdClass를 사용하여 ParentId 클래스를 식별자 클래스로 지정
    • @IdClass를 사용 시 식별자 클래스는 다음 조건 만족하여야 함
      • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야함
      • Serializable 인터페이스를 구현해야 함
      • equals, hashCode 구현
      • 기본 생성자 존재
      • 식별자 클래스는 public
    • 부모 테이블의 기본 키 컬럼이 복합 키면 자식 테이블의 외래 키도 복합 키므로 외래키 매핑 시 여러 컬럼을 매핑해야 하므로 @JoinColumns 어노테이션을 사용하고 각각을 @JoinColumn으로 매핑한다.
  • @EmbeddedId
    • 객체지향에 가까운 방법
    • Parent 엔티티에서 식별자 클래스를 직접 사용하고 @EmbeddedId 어노테이션을 적어주면 됨
    • @IdClass와는 다르게 @EmbeddedId를 적용한 식별자 클래스는 식별자 클래스에 기본 키를 직접 매핑한다.
    • @EmbeddedId를 적용한 식별자 클래스는 다음 조건을 만족
      • @Embeddable 어노테이션을 붙여줘야 함
      • Serializable 인터페이스 구현
      • equals, hashCode 구현
      • 기본 생성자 존재
      • 식별자 클래스 public
  • 복합 키와 equals(), hashCode()
    • 복합 키는 해당 두 메서드를 필수로 구현해야함
    • 기본 제공하는 equals()는 인스턴스 참조 값 비교를 하기 때문에 상황에 따라 적절히 오버라이딩 필요
    • 따라서 식별자 객체의 동등성이 지켜지지 않으면 예상과 다른 엔티티가 조회되거나 엔티티를 찾을 수 없는 등 영속성 컨텍스트가 엔티티를 관리하는 데 문제가 발생할 수 있음
  • @IdClass vs @EmbeddedId
    • 각각 장단점이 있으므로 취향에 맞게 사용
    • @EmbeddedId가 @IdClass와 비교해서 더 객체지향적이고 중복도 없어 좋아보이지만 특정 상황에 JPQL이 다음과 같이 길어질 수 있음

7.3.3 복합 키 : 식별 관계 매핑

  • @IdClass와 식별 관계
  • 식별 관계는 기본 키와 외래 키를 같이 매핑
  • 따라서 식별자 매핑인 @Id와 연관관계 매핑인 @ManyToOne과 @JoinColumn으로 외래 키를 같이 매핑
  • @EmbeddedId와 식별 관계
    • @EmbeddedId로 식별 관계를 구성할 때 @MapsId를 사용해야 함
    • @MapsId는 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻

7.3.4 비식별 관계로 구현

  • 식별 관계의 복합 키를 사용한 코드와 비교 시 매핑도 쉽고 코드도 단순
  • 복합 키가 없으므로 복합 키 클래스를 만들지 않아도 됨

7.3.5 일대일 식별 관계

  • 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만 사용
  • 부모 테이블의 기본 키가 복합 키가 아니면 자식 테이블의 기본 키는 복합 키로 구성하지 않아도 됨

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

  • 디비 설계 관점에서 보면 다음과 같은 이유로 식별 관계보다는 비식별 관계를 선호
    • 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어남 -> 부모 테이블 기본키 1개 / 자식 테이블은 기본키 컬럼 2개 / 손자 테이블은 기본키 3개로 점점 늘어나 조인시 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커짐
    • 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많음
    • 식별 관계를 사용할 때 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많음 -> 반면 비식별 관계의 기본 키는 비즈니스와 전혀 관계없는 대리 키를 주로 사용함. 즉, 비즈니스 요구사항은 시간이 지남에 따라 언젠가는 변하기 때문에 비식별관계 권장
    • 식별 관계는 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하므로 비식별 관계보다 테이블 구조가 유연하지 못함
  • 객체 관계 매핑의 관점에서의 비식별 관계 선호 이유
    • 일대일 관계를 제외하고 식별 관계는 2개 이상의 컬럼을 묶은 복합 기본 키를 사용하므로 JPA에서 복합 키는 별도의 복합 키 클래스를 만들어서 사용해야 하는데 컬럼이 하나인 기본 키를 매팡하는 것보다 많은 노력이 필요하게 됨
    • 비식별 관계의 기본 키는 주로 대리 키를 사용하는데 JPA는 @GenerateValue처럼 대리 키를 생성하기 위한 편리한 방법을 제공해줌
  • 식별 관계가 가지는 장점은?
    • 기본 키 인덱스를 활용하기 좋고, 상위 테이블들의 기본 키 컬럼을 자식, 손자 테이블들이 가지고 있어 특정 상황에 조인 없이 하위 테이블로만 검색을 완료 가능
  • ORM 신규 프로젝트 진행 시 추전 방법
    • 비식별 관계를 사용하고 기본 키는 Long 타입의 대리 키를 사용하는 것
    • 유연한 대처가 가능하다는 장점
    • Integer는 20억 정도면 끝나버리므로 많은 양의 데이터 저장 시 문제 생길 수 있지만 Long 타입은 약 920경 이라 안전
    • 그리고 선택적 비식별 관계보다는 필수적 비식별 관계를 사용하는 것이 좋음
    • 이유는 선택적인 비식별 관계는 NULL을 허용하므로 조인할 때에 외부 조인을 사용해야 하는데 필수적 관계는 NOT NULL로 항상 관계가 있다는 것을 보장하므로 내부 조인만 사용해도 되기 때문

댓글