JPA 스터디, #3

박주진·2021년 10월 19일
0

JPA 스터디

목록 보기
3/3

아래 내용은 김영한 님의 자바 ORM 표준 JPA 프로그래밍의 내용에 기반하여 정리한 글입니다.

고급매핑

상속 관계 매핑

  • 객체지향 언어의 상속이라는 개념은 RDB 슈퍼타입 서브타입 관계와 가장 유사하다.
  • 방법
    • 조인 전략
      • @Inheritance(strategy = InheritanceType.JOINED)
      • 객체와 다르게 DB는 타입으로 구분할 수가 없어 타입 컬럼이 필요하다.(@DiscriminatorColumn(name = "원하는 컬럼명"))
      • 엔티티를 저장할 때 타입 컬럼에 입력할 값을 지정할수 있다.(@DiscriminatorValue("원하는 값"))
      • JPA 표준명세에는 DiscriminatorColumn을 사용하지만 하이버네이트를 포함한 다른 구현체는 해당 애노테이션 없이도 동작한다. (해당 컬럼 없이도 정확하게 특정 자식 테이블과 조인해서 쿼리가 가능하다?)
      • 장점
        • 테이블 정규화
        • 외래 키 참조 무결성 제약 조건 활용 가능
        • 정규화로 저장공간이 효율적이다.
      • 단점
        • 조회시 조인이 많이 사용된다. (사실상 성능에 크게 영향이 없고 오히려 좋을때도 있다.)
        • 조회 쿼리가 복잡해 질 수 있다.
        • 데이터 저장할때 insert가 2번(부모+자식) 나간다.(성능에 크게 영향이 없음)
    • 단일 테이블
      • @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
      • DiscriminatorColumn을 꼭 사용해야 한다.
      • 기본 DiscriminatorValue를 지정안하면 기본값은 엔티티 이름이다.
      • 장점
        • 조인이 필요없어 일반적으로 조회가 빠름
        • 조회 쿼리 단순
      • 단점
        • 자식 엔티티에서 매핑한 모든 컬럼은 null을 허용해야 함
        • 단일 테이블에 모든 것을 저장함으로 테이블이 켜져 조회 성능에 영향을 줄 수 있다.(임계점을 넘어야 하는데 극히 드물다.)
    • 구현 클래스마다 테이블 전략
      • @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
      • 구분 컬럼을 사용하지 않는다.
      • 가급적 쓰지말자
      • 장점
        • 서브 타입을 명확하게 구분해서 처리가 효과적이다.
        • not null 제약 조건을 줄 수 있다. (조인 테이블도 가능한거 아닌가?)
      • 단점
        • 부모 타입으로 조회를 하면 union all이 발생해서 성능이 느리다.(특정 자식타입으로 조회하면 union all 생기지 않음)
        • 자식 테이블 전체에 대한 통합 쿼리가 힘들다.
        • 자식이 추가될때 많은 수정이 필요할 수 있다.(예) 모든 자식의 가격을 통합하는 쿼리에서 추가된 테이블를 반영해야 한다.)

권장하는 방법

비지니스 적으로 중요하고 복잡한 데이터인 상황이면 조인전략을 사용한다. 하지만 데이터가 구조가 단순하고 타입 확장 가능성도 없다면 단일 테이블 전략을 선택

MappedSuperClass

  • 부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하고 싶을때 사용
  • 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모아주는 역할 (부모 타입으로는 조회검색x)
  • 직접 사용할 일이 없음으로 추상 클래스 권장
  • 상속: @MappedSuperclass, 조합: @Embeddable, @Embedded

복합 키와 식별 관계 매핑

  • 테이블 관계 종류
    • 식별 관계 (부모의 기본 키를 자식 테이블의 기본 키 + 외래키로 사용)
    • 비식별 관계 (부모의 기본 키를 자식 테이블의 외래키로만 사용)
      • 필수적 비식별 관계 (외래키는 NOT NULL)
      • 선택적 비식별 관계 (외래키 NULLABLE)
  • 복합키
    • @idClass,@EmbeddedId 두가지 방법이 있다. (EmbeddedId가 좀더 객체지향적이고 중복이 없으나 JPQL이 좀더 길어질 수 있음)
    • 둘다 equals()와 hashCode()를 필수
    • 각자 장단점이 있음으로 본인 취향에 맞게 일관성 있게 사용하면 된다.
  • 일대일 식별관계는 부모 테이블이 복합 키가 아니면 자식 테이블의 기본 키는 복합 키로 구성하지 않아도 된다.
  • 비식별을 선호하는 이유
    • 식별 관계는 부모 테이블의 기본 키가 자식 테이블에 전파되어 자식 테이블의 기본 키 컬럼이 점점 늘어난다.(조인시 복잡, 기본 키 인덱스가 커짐)
    • 일대일 관계를 제외하고는 종종 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 해서 번거롭다.
    • 자식 테이블이 부모 테이블의 기본 키를 자신의 기본키로 사용함으로 유연성이 떨어짐.
    • 식별 관계는 보통 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많다. 비즈니스 요구사항은 시간이 지나면서 변경될 수 있고 이때 식별 관계의 자연 키 컬럼은 자식,손자 까지 전파되어 있다면 변경하기 매우 힘들다.
    • 비식별 관계는의 기본 키는 주로 대리키를 사용함으로 키 생성시 @GenerateValue처럼 편리한 방법을 사용할 수 있으나 식별관계는 사용할 수 없다.
  • 식별관계의 장점
    • 상위 테이블들의 기본 키 컬럼을 자식,손자 테이블들이 가지고 있으므로 특정상황에서 조인 없이 하위 테이블만 검색 가능.
    • 기본 키 인덱스를 활용하기 좋음(무슨뜻일까?)

    권장 방식

    필수적 비식별 관계(내부 조이만 상용하면 됨으로), 기본키는 Long에 대리 키 사용

조인 테이블

  • 테이블의 연관관계를 맺는 방법
    • 조인컬럼(외래 키) 사용하는 방법 (@JoinColumn)
    • 조인 테이블 사용(테이블 사용) 하는 방법 ( @JoinTable)
  • 조인 테이블은 주로 다대다 관계를 일대다, 다대일 관계로 풀어내기 위해 주로 사용하지만 일대일, 일대다, 다대일 관계에서도 사용할 수 있다.
  • 조인 테이블에 관계에 필요한 키들을 제외한 다른 컬럼을 추가하면 @JoinTable 전략을 사용 불가하다. 다른 컬럼이 필요하다면 새로운 엔티티를 만든후 조인 테이블과 매핑해야 한다.

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

// Board 엔티티에 BOARD_DETAIL,BOARD 테이블 매핑

@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;
  • @SecondaryTables를 사용해 더 많은 테이블 매핑 가능
  • 이 방법은 항상 두테이블이 같이 조회되기 떄문에 최적화하기 어려워 일대일 매핑을 권장한다.

프록시와 연관관계 관리

프록시

  • JPA는 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연시키기 위해(지연로딩) 프록시를 사용한다.
  • 하이버네이트 지연로딩 구현방법은 바이트코드 수정과 프록시 사용 두 가지가 있다.
  • 프록시 객체는 실제 객체에 대한 참조를 내부적으로 보관하고 있는 실제 클래스를 상속 받아서 만들어진다. 이 후 특정 메서드가 호출되면 실제 객체의 메소드를 대신 호출해주는 위임을 역할을 수행한다.
  • 프록시 객체는 처음 사용할 때 한 번만 초기화 된다.
  • 프록시 객체를 초기화 한다고 프록시 객체가 실제 엔티티로 바뀌는게 아니라 프록시를 통해 실제 엔티티를 접근할 수 있게 된다.
  • 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입체크시 == 비교가 아닌 instace of를 사용해야 한다.
  • 영속성 컨텍스트에 찾는 엔티티가 있으면 em.getReference를 호출해도 데이터베이스 조회 없이 프록시가 아닌 실제 객체가 반환된다. 이는 jpa는 같은 영속성 컨텍스트안에서는 같은 엔티티 끼리 동일성을 맞춰주기 위함이다. (반대로 em.getReference후 em.find를 해도 같은 프록시 객체 반환)
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일때 프록시를 초기화하면 예외 발생.(5.4.1 버젼부터 em.close()이후에는 예외는 발생하지 않고 em.detach후에만 예외가 발생한다.)

프록시와 식별자

  • 프록시 객체는 getId()를 호출해도 프록시를 초기화하지 않는다. 왜냐하면 식별자 값은 이미 가지고 있기 떄문이다. 하지만 엔티티 접근 방식을 필드@Access(AccessType.FIELD))로 설정한 경우에는 getId()가 id만 조회하는지 다른 필드를 활용하는지를 알 수 없기때문에 프록시 객체가 초기화 된다.
  • 연관관계를 설정시에는 식별자 값만 사용하므로 프록시를 사용하면 데이터베이스 접근횟수를 줄일 수있고 엔티티 접근 방식을 필드로 설정해도 초기화 되지 않는다.
Member member = em.find(Member.class, "member1");
Team team = em.getReference(Team.class, "team1"); //SQL을 실행하지 않음
member.setTeam(team);

즉시 로딩과 지연로딩

  • 지연 로딩은 프록시 객체가 로딩된다.
  • 즉시 로딩은 최적화를 위해 조인 쿼리로 한번에 가져온다.
  • 즉시 로딩 실행 쿼리는 @JoinColumn에 nullable 속성에 의해 내부조인 또는 외부조인이 사용된다. nullble = false로 세팅하면 외래키에 널값을 허용하지 않는다고 보장되어 내부조인을 사용해서 쿼리한다.(내부 조인이 성능과 최적화에서 더 유리하다.)
  • 처음 부터 모든 연관된 엔티티를 올려두는 것(즉시로딩)도 현실적이지 않고 필요할떄마다 쿼리를 실행해서(지연로딩)해서 가져오는 것도 최적화 관점에서는 꼭 좋지는 않으니 상황에 맞게 사용하자.
  • 컬렉션에 들어있는 엔티티는 하이버네이트에서 관리목적으로 org.hibernate.collection.internal.PersistentBag 라는 내장 컬렉션(컬렉션 래퍼)으로 변경해서 반환해준다. 지연로딩시에는 내장 컬렉션이 프록시 역활도 한다. 내장 컬렉션은 지연로딩시에는 특정 엔티티에 접근하기 전까지 초기화 되지 않는다. (member.getTeamList()를 호출해도 초기화 되지 않음 member.getTeamList().get(0)라고 호출해야 초기화 됨)

주의점

  • 컬렉션을 하나 이상 즉시 로딩하지 말자. 왜냐하면 너무 많은 데이터 반환되고 이 데이터들의 중복을 제거하기위해 메모리에서 필터링함으로 애플리케이션 성능이 저하될 수 있다.
  • 즉시 로딩은 모든 관계에서 항상 외부 조인을 사용한다.(다대일,일대일 관계에서 (optional = false) 값을 주는 경우 제외)
  • 즉시로딩은 jpql실행시 n+1 문제 발생 가능성이 있다.

권장하는 방법

  • 모든 연관관계에 지연로딩 사용후 꼭 필요한 곳에만 즉시 로딩 사용으로 최적화 하라.

영속성 전이와 고아 객체

영속성 전이

@Entity
public class Parent {
 ...
 @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
 private List<Child> children = new ArrayList<Child>();
 ...
  • 특정 엔티티를 영속화 할때 연관된 엔티티도 함께 영속 상태로 만들고 싶을때 사용한다.(편의제공을 위한 기능이지 연관관계 매핑과는 전혀 관계가 없다.)
  • 종류에는 ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH가 있다.
  • CascadeType.PERSIST, CascadeType.REMOVE는 em.persist(), em.remove()를 실행할 때 바로 전이가 발생하지 않고 플러시를 호출시 전이가 발생됨.
  • 자식이 부모에 완전 종속되어 있고 다른 소유주가 없을때 사용하라.

고아 객체

@Entity
public class Parent {
 @Id @GeneratedValue
 private Long id;
 @OneToMany(mappedBy = "parent", orphanRemoval = true)
 private List<Child> children = new ArrayList<Child>();
 ...
}
  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능.
  • 참조하는 곳이 하나일때만 사용해야 한다.
  • 영속성 전이 + 고아 객체를 둘다 사용하면 부모를 통해서 자식의 생명주기를 관리한다는 뜻이다. (ddd의 aggregate Root개념을 구현할때 유용하다.)

값 타입

JPA 데이터 타입

  • 엔티티
  • 값 타입
    • 기본값 타입 (int, Integer, String)
    • 임베디드 타입 (복합 값 타입)
    • 컬렉션 값 타입 (하나 이상의 값타입)

기본 값 타입

  • String, int와 같은 값 타입
  • 엔티티와 달리 식별자도 없고 생명주기 또한 속해 있는 엔티티에 의존하다.즉 속해있는 엔티티 제거 시 값도 같이 제거된다.
  • 절대 공유하면 안된다. 자바의 기본 타입은 절대 공유되지 않는다. 래퍼 클래스(Integer)나 String은 공유는 되나 변경 불가인 특수한 클래스여서 기본 타입처럼 사용할 수 있다.

임베디드 타입

  • 주로 기본 값 타입을 모아서 만든 복합 값 타입.
// 여기서 Period가 임베디드 타입이다.
@Entity
public class Member {
 
 @Id @GeneratedValue
 private Long id;
 
 private String name;
 
 @Embedded Period workPeriod;
 }
 
 @Embeddable
public class Period {
 
 @Temporal(TemporalType.DATE) java.util.Date startDate;
 @Temporal(TemporalType.DATE) java.util.Date endDate;


}
  • 응집력 있는 엔티티를 설계할 수 있다. (내부적으로 값타입을 사용하면 응집력이 높아지는 것인가? 값타입이 아니라 그냥 값들이 나열되어 있어도 엔티티 자체가 수행하는 기능에는 차이가 없는게 아닌가?)
  • 객체 모델링을 더 깔끔하게 할 수있고 재사용이 가능한 객체를 추출 가능하다.
  • 임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능하다.(무슨 뜻일까?)
  • @AttributeOverride 속성을 통해 같은 임베디드 타입을 중복으로 한 엔티티에서 사용할 수 있다.
  • 임베디드 타입은 다른 임베디드 타입 또는 다른 엔티티를 가질 수 있다.
  • 임베디드 타입이 null 이면 매핑된 컬럼의 값은 모두 null 이다.

값 타입과 불변 객체

  • 값 타입은 복잡한 객체 세상을 단순화하기 위한 개념
  • 임베디드 값 타입을 공유하면 side effect 우려가 있다. 왜냐하면 하나의 임베디드 값타입을 공유하고 있으면 한쪽에서 변경해도 다른쪽도 같이 값이 변경되기 때문이다.
  • 값이 공유가 목적이 었다면 값 타입이 아닌 엔티티를 사용해야 한다.
  • 항상 값을 복사해서 사용하면 되지만 임베디드 타입의 경우 객체타입 이기때문에 복사하지 않고 원본의 참조 값을 직접 넘기는걸 막을 방법이 없다. (기본 타입의 경우 값이 항상 복사되기 떄문에 걱정하지 않아도 된다.)
Address a = new Address("Old"); 
Address b =a;
  • 임베디드 타입의 경우 불변 객체로 설계하면 부작용을 원천 차단할 수 있다.

값타입 비교

  • 인스턴스가 달라도 그 안에 값이 같으면 같은 걸로 봐야 한다.
  • 동등성 비교를 위해 equals 메서드를 재정의 해야하고 hashCode도 재정의하는 것이 안전하다.

값 타입 컬렉션

  • 한 필드에 하나 이상의 값 타입을 저장하려면 컬렉션에 보관하고 @ElementCollection, @CollectionTable 어노테이션 을 사용해야 한다.
@Entity
public class Member {

 @Id @GeneratedValue
 private Long id;
 
 @Embedded
 private Address homeAddress;

 
 @ElementCollection
 @CollectionTable(name = "ADDRESS", joinColumns
 = @JoinColumn(name = "MEMBER_ID"))
 private List<Address> addressHistory = new ArrayList<Address>();
 //...
}

@Embeddable
public class Address {
 @Column
 private String city;
 private String street;
 private String zipcode;
 //...
}
  • 값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거(ORPHAN REMOVE) 기능을 필수로 가지고 있다.
  • 값 타입 컬렉션은 기본 페치 전략은 지연 로딩이다. (즉시 로딩 비추)
  • 값 타입은 그냥 갈아 끼워야 한다. 변경시 업데이트는 되나 부작용에 우려가 있다.

제약사항

  • 값 타입 컬렉션에 변경이 일어나면 변경된 내용만 반영하려고 노력하지만 여러 조건에 따라 기본 키를 식별하지 못해 값 타입 컬렉션에 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션에 있는 모든 값을 다시 디비에 저장하는 방식으로 동작된다.
  • 같은 값을 중복해서 저장할 수 없고 일부 컬럼 값을 NULL로 설정할 수 없다. 왜냐하면 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성하기 때문이다.

실무 권장사항

  • 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 일대다에 영속성 전이(Cascade) + 고아 객체 제거(ORPHAN REMOVE) 기능을 추가해서 사용하라.
  • 정말 단순한 상황에서만 값타입 컬렉션 사용 하라 예)체크박스에 내용. 대부분의 상황은 엔티티다. 예)주소이력

0개의 댓글