고급 매핑

식빵·2022년 1월 4일
0

JPA 이론 및 실습

목록 보기
9/17

JPA 가 제공하는 고급매핑에 대해 알아보자.

순서

  • 상속 관계 매핑
  • @MappedSuperclass
  • 복합키와 식별 관계 매핑
  • 조인 테이블




🍀 상속 관계 매핑


데이터베이스에는 상속이라는 개념이 없다.
대신 슈퍼타입 서브타입 관계라는 모델링 기법이 그나마 객체의 상속과 비슷하다.

그렇다면 이러한 모델링을 객체 상속관계로 매핑하는 3가지 방법에 대해 알아보자.

  • 각각의 테이블로 변환
  • 통합테이블로 변환
  • 서브타입 테이블로 변환



🐛 조인 전략

엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본키를 받아서 기본 키+ 외래 키로 사용하는 전략이다.

객체에는 타입을 통해서 구분이 가능하지만, 테이블은 타입을 구분하는 컬럼(= DTYPE)을 사용해서 서로 구분한다.



예제 테이블 그림


엔티티 코드

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "emp_type")
public abstract class Employee {

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

    private String empName;

    private String address;
    
}

@Entity
@Setter
@Getter
@DiscriminatorValue("hour")
public class HourlyEmployee extends Employee {
    private String hourlyRate;
}

@Entity
@Getter
@Setter
@DiscriminatorValue("consult")
public class Consultant extends Employee {
    private String payRate;
}

@Entity
@Setter
@Getter
@DiscriminatorValue("salary")
public class SalariedEmployee extends Employee {
    private String annualSalary;
}


상세 설명

  • @Inheritance(strategy = InheritanceType.JOINED)
    • 상속 매핑은 부모 클래스에 @Inheritance 애노테이션 사용
    • 조인 전략이므로 JOINED 표기
  • @DiscriminatorColumn(name = "emp_type")
    • 부모 클래스에 구분 컬럼을 지정
    • 이 컬럼으로 자식 테이블을 구분
    • 기본 값은 "DTYPE"
  • @DiscriminatorValue("salary")
    • 엔티티를 저장할 때 사용할 구분 컬럼의 값 지정




🐛 단일 테이블 전략

구분 컬럼을 사용하되 각 테이블 별로 있던 컬럼을 그냥 한 테이블에 다 때려 박는다.

예제 테이블


엔티티 코드

위의 조인 전략 코드에서 부모 클래스인 Employee 의 애노테이션만 수정하면 된다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 얘만 변경
@DiscriminatorColumn(name = "emp_type")
public abstract class Employee {

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

    private String empName;

    private String address;

}

생성된 테이블 결과




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

이 방법은 데이터베이스 설계자와 ORM 전문가 모두가 추천하지 않으니, 될 수 있으면 사용하지 말자.


엔티티 코드

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) // 요것만 수정
@DiscriminatorColumn(name = "emp_type")
public abstract class Employee {...}

생성된 테이블




🍀 @MappedSuperclass

이전에 본 전략들은 모두 데이터베이스 테이블과 매핑이 됐는데,
이번엔 부모 클래스에 테이블이 매핑되지 않고, 오직 자식 클래스에게 매핑 정보만 넘겨주겠다.
@MappedSuperclass 가 있으면 테이블 매핑이 일어나지 않는다.

이전에 사용했던 코드를 그대로 쓰겠다.
다만 이전에 Employee 추상 클래스 위에 있는 애노테이션들을 아래처럼 수정하겠다.



공통 매핑 정보 추상 클래스

@MappedSuperclass // @Entity 가 사라지고 이게 대신 쓰인다.
@Getter @Setter
public abstract class Employee {

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

    private String empName;

    private String address;

}

이렇게하면 끝이다!
이러고 자식 클래스에서 extend를 하기만 하면 모든 매핑 정보를 다 물려받는다.
참고로 자식 클래스에서 부모로부터 받은 매핑 정보를 재정의하려면 아래처럼 한다.


엔티티 코드

@Entity
@Setter
@Getter
@AttributeOverride(name = "id", column = @Column(name = "employee_id"))
public class HourlyEmployee extends Employee {
    private String hourlyRate;
}

테이블 생성 결과



여러개의 매핑 정보를 수정하고 싶다면 아래처럼 하자.

@Entity
@Setter
@Getter
@AttributeOverrides({
        @AttributeOverride(name = "id", column = @Column(name = "employee_id")),
        @AttributeOverride(name = "empName", column = @Column(name = "employee_name"))
})
public class HourlyEmployee extends Employee {
    private String hourlyRate;
}

테이블 생성 결과

  • 필요한 매핑 정보를 자식 클래스들이 모두 잘 받은 걸 확인할 수 있다.
  • hourly_employee 테이블은 AttributeOverrides가 잘 적용된 것을 확인할 수 있다.

참고: @MappedSuperclasstable_per_class의 차이점
이 글을 참고하자. 나는 Vlad Mihalcea의 답변이 난 이해하기 편했다.

참고2:
엔티티(@Entity)는 엔티티(@Entity)이거나 @MappedSuperclass로 지정한 클래스만 상속 가능




🍀 복합 키와 식별 관계 매핑


🐛 식별 관계? 비식별 관계?

DB Table 에서 외래키가 기본키에 포함 되면 식별관계 아니면 비식별 관계이다.
아래 그림을 통해서 차이점을 보자.



식별 관계 그림


비식별 관계 그림


  • 필수적 비식별 관계: 외래키 NULL 비허용
  • 선택적 비식별 관계: 외래키 NULL 허용




🐛 복합 키 매핑 사전 지식


1. @Id를 단순히 2개 쓴다고 되지 않는다.

정말 단순하게 생각해서 복합키는 @Id 를 2개 쓰면 되지 않을까 싶다.
한번 아래 엔티티 코드 작성후 테스트를 돌려보자.


엔티티 코드

@Entity
public class TestEntity {

    @Id
    private String name;

    @Id
    private String phoneNumber;
    
}

콘솔 출력

😱


에러가 난다. JPA에서는 이런 방식으로 복합 키를 지원하지 않는다.
JPA에서는 @IdClassEmbeddedId 2가지 애노테이션을 제공하며 우리는 이를 사용해서 식별자 클래스를 작성하면 된다.



2. 식별자 클래스의 equals와 hashcode 는 필수 구현

잊었을까봐 말하지만 JPA의 영속성 컨텍스트는 엔티티를 보관할 때 식별자를 사용한다.
그런데 JPA 에서는 이 식별자를 실제 비교하는 방법은 식별자 객체의 equals 와 hashcode 메소드를 통해서 이루어진다. 그러므로 식별자 클래스에서 이 2개의 메소드는 필수적으로 구현해야 한다..



3. @GeneratedValue는 사용 못한다.

복합키로 구성시에 복합키 구성원 중 그 어떤 필드도 @GeneratedValue를 사용하지 못한다.




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


1. @IdClass : 비식별 관계

  • Main 테이블은 기본키가 복합키
  • 그런 복합키를 FK로 사용하는 Sub 테이블
  • 비식별 관계

이런 상황에서 어떻게 매핑을 하는지 코드를 보자.


식별자 클래스

@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode // 반드시 equals 와 hashcode 는 구현해야 한다!
public class MainId implements Serializable {
    private String id1; //MainEntity.id1 매핑
    private String id2; //MainEntity.id2 매핑
}

복합키를 갖는 엔티티

@Entity
@Table(name = "Main")
@IdClass(MainId.class)
public class MainEntity {

    @Id
    @Column(name = "main_id1")
    private String id1; // MainId.id1과 연결

    @Id
    @Column(name = "main_id2")
    private String id2; // MainId.id1과 연결

    private String name;
}

참고. 만약 id1, id2 라는 필드 명을 안 쓰면?
아래와 같은 예외가 터진다.
Property of @IdClass not found in entity me.dailycode.main.domain._05.MainClass: id1


식별자 클래스 작성법

  • 식별자 클래스의 속성명 == 엔티티가 사용하는 식별자의 속성명
  • 식별자 클래스는 Serializable 구현 필수
  • equals, hashcode 구현 필수
  • 기본 생성자 필수
  • public class

복합키를 외래키로 참조하는 엔티티

@Entity
public class SubEntity {

    @Id
    @Column(name = "sub_id")
    private String id;

    @ManyToOne
    @JoinColumns({
            @JoinColumn(name = "main_id1", referencedColumnName = "main_id1"),
            @JoinColumn(name = "main_id2", referencedColumnName = "main_id2")
    })
    private MainEntity main;

}


간단한 실습

- 엔티티 저장 코드

MainEntity main = new MainEntity();

// 아래 2줄 처럼 하면 JPA가 알아서 MainId 객체 생성 후, 영속성 컨텍스트의 키로 사용
main.setId1("mainId1");
main.setId2("mainId2"); 


main.setName("mainEntity");
em.persist(main);

- 엔티티 조회 코드

MainId mainId = new MainId("mainId1", "mainId2");
MainEntity mainEntity = em.find(MainEntity.class, mainId);

- 나가는 쿼리

Hibernate: 
    select
        mainentity0_.main_id1 as main_id1_7_0_,
        mainentity0_.main_id2 as main_id2_7_0_,
        mainentity0_.name as name3_7_0_ 
    from
        main mainentity0_ 
    where
        mainentity0_.main_id1=? 
        and mainentity0_.main_id2=?




2. @EmbeddedId : 비식별 관계

@IdClass가 데이터베이스에 맞춘 방법이라면, @EmbededId는 조금 더 객체지향적 방식이다.
코드를 보자.

복합키를 갖는 엔티티

@Entity
@Table(name = "Main")
@Setter @ToString
public class MainEntity {

    @EmbeddedId
    private MainEntityId id;

    private String name;
}

식별자 클래스

@Embeddable
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode // 반드시 구현해야함
public class MainEntityId implements Serializable {

    @Column(name = "main_id1")
    private String id1;

    @Column(name = "main_id2")
    private String id2;

}

식별자 클래스 작성법

  • @Embeddable 을 붙여준다.
  • Serializable을 구현한다.
  • equals, hashcode 메소드를 구현한다.
  • 기본 생성자 필요하다.
  • public class 여야 한다.

@IdClass을 사용하면 기본키 매핑(=@Id)을 엔티티 클래스에 직접 작성했지만,
@EmbeddedId을 사용하면 기본키 매핑이 @Id대신 @EmbeddedId를 통해서 이루어지며
그 식별자 클래스는 @Embeddable 애노테이션을 사용한다.



등록 및 조회 테스트

MainEntity main = new MainEntity();
MainEntityId mainEntityId = new MainEntityId("mainId1", "mainId2");
main.setId(mainEntityId);
main.setName("mainEntity");
em.persist(main);

em.flush();
em.clear();

MainEntity mainEntity = em.find(MainEntity.class, mainEntityId);
System.out.println("mainEntity = " + mainEntity);
// mainEntity = MainEntity(id=MainEntityId(id1=mainId1, id2=mainId2), name=mainEntity) 



@IdClass vs @EmbeddedId

그냥 본인 취향에 맞는 걸 쓰자 ^^;

참고 (중요!)
복합 키에는 @GeneratedValue를 사용하지 못함.




🐛 복합 키: 식별관계 매핑

이 구조는 대체 어떻게 매핑할까.


1. @IdClass : 식별 관계


@Entity @Table(name = "Super")
public class SuperEntity {

    @Id
    @Column(name = "super_id")
    private String id;
    
    private String name;

}

@EqualsAndHashCode
public class MiddleId implements Serializable {
    private String super_;
    private String middleId;
}
@Entity
@Table(name = "Middle")
@IdClass(MiddleId.class)
public class MiddleEntity {

    @Id // PK
    @ManyToOne// FK
    @JoinColumn(name = "super_id")
    private SuperEntity super_;

    // PK
    @Id
    private String middleId;

    private String name;

}

@EqualsAndHashCode
public class DerivedId implements Serializable {
    private MiddleId middle; // DerivedEntity.middle 매핑
    private String derivedId; // DerivedEntity.derivedId 매핑
}
@Entity @Table(name = "Derived")
@IdClass(DerivedId.class)
public class DerivedEntity {

    @Id
    @ManyToOne
    @JoinColumns({
            @JoinColumn(name = "super_id"),
            @JoinColumn(name = "middle_id")
    })
    private MiddleEntity middle;

    @Id
    private String derivedId;

    private String name;

}

어렵다😱!



2. @EmbeddedId : 식별 관계

@Embedded로 식별 관계를 구성할 때는 @MapsId를 사용해야 한다.


@Entity @Table(name = "Super")
public class SuperEntity {

    @Id
    @Column(name = "super_id")
    private String id;

    private String name;

}

@Entity
@Table(name = "Middle")
public class MiddleEntity {

    @EmbeddedId
    private MiddleId id;

    @MapsId("superId")
    @ManyToOne
    @JoinColumn(name = "super_id")
    public SuperEntity super_;

    private String name;

}
@Embeddable
@EqualsAndHashCode
public class MiddleId implements Serializable {

    private String superId; // MapsId("superId")로 매핑

    @Column(name = "middle_id")
    private String id; // 필드명을 MiddleEntity 의 PK와 매핑
}

@Entity @Table(name = "Derived")
public class DerivedEntity {

    @EmbeddedId
    private DerivedId id;

    @MapsId("middleId")
    @ManyToOne
    @JoinColumns({
            @JoinColumn(name = "super_id"),
            @JoinColumn(name = "middle_id")
    })
    private MiddleEntity middle;

    private String name;

}
@Embeddable
@EqualsAndHashCode
public class DerivedId implements Serializable {
    @Embedded
    private MiddleId middleId;
    private String derivedId;

    public MiddleId getMiddleId() {
        return middleId;
    }
}

테이블 생성 모습

결론: 어렵다😱!
그래도 쓰다보면 익숙해질지도?




🐛 단일 키: 비식별 관계로 전환

@Entity
public class SuperEntity {

    @Id
    @Column(name = "super_id")
    private String id;

    private String name;
}

@Entity
public class MiddleEntity {

    @Id
    private String middleId;

    @ManyToOne
    @JoinColumn(name = "super_id")
    private SuperEntity superEntity;

    private String name;
}

@Entity
public class DerivedEntity {

    @Id
    private String derivedId;

    @ManyToOne
    @JoinColumn(name = "middle_id")
    private MiddleEntity middleEntity;

    private String name;
}

이전에 비해서 너무나도 쉽다. 게다가 복합키도 없어서 더더욱 그렇다.



🐛 일대일 식별 관계

@Entity
@Getter @Setter
public class Board {

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

    private String title;

    @OneToOne(mappedBy = "board")
    private BoardDetail boardDetail;
}
@Entity
@Getter @Setter
public class BoardDetail {

    @Id
    private Long boardId;

    @MapsId // BoardDetail.boardId 매핑
    @OneToOne
    @JoinColumn(name = "board_id")
    private Board board;

    private String content;

}



🐛 추천사항

  • ORM 신규 프로젝트 진행시 비식별 관계를 사용하고 기본 키는 Long 타입의 대리 키를 사용하자.
  • 선택적 비식별 관계보다는 필수적 비식별 관계를 사용하는 것이 효율이 더 좋다(Join 시 내부 조인).




🍀 조인 테이블 (= 링크 테이블)

테이블이 서로 연관관계를 맺는 방법은 2가지다.

  • 조인 컬럼 사용(외래키)
  • 조인 테이블 사용(테이블 사용)

조인 컬럼 그림


조인 테이블 그림

  • 일반적인 조인 컬럼을 쓴다면 관계를 갖으면 다행이지만 갖지 않으면 NULL을 넣어야 하는 경우가 있다.
  • 반면 조인 테이블을 사용하면 양쪽의 연관관계를 갖더라도 서로가 외래키를 갖지 않는다.
  • 하지만 조인 테이블은 하나의 테이블을 새로 생성하고 관리해야하며, 두 테이블을 조인하려면 Member_Locker 까지 조인해야 한다.
  • 그러므로 처음에는 조인 컬럼 방식을 쓰고, 필요하면 그때 조인 테이블 방식을 쓰는 게 좋다.

참고:
책에서는 1:1, 1:N, N:1, N:M 순으로 조인테이블 매핑 방식을 알려주지만...
내가 보기에는 1:N 과 N:1 사실상 똑같고, N:M 은 이전에 했던 @JoinTable 내용의 반복이다.
그러니 1:1, 1:N 조인 테이블만 가볍게 보고 넘어가겠다.




🐛 일대일 조인 테이블


엔티티 코드

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

}
@Entity
public class Child {

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

    private String name;

}



🐛 일대다 조인 테이블

참고: 1:N 에서 N 쪽에 있는 PK 를 조인테이블이 참조하게 되는데, 이 값은 UNIQUE 제약 조건을 줘야한다.

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

}
@Entity
public class Child {

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

    private String name;

}




🍀 참고

자바 ORM 표준 JPA 프로그래밍
인프런 JPA 관련 로드맵

profile
백엔드를 계속 배우고 있는 개발자입니다 😊

0개의 댓글