스터디 모음/Spring 스터디

[Spring JPA Basic] 스프링 JPA 기초 간단 요약

always-dev 2022. 9. 7.
반응형

1. 권장하는 식별자 전략

  • SEQUENCE, IDENTITY, 랜덤(복합)키를 사용하는게 좋습니다.
  • 권장 : Long형 + 대체키 + 키 생성 전략 사용




1-1. IDENTITY 생성 전략 특징

  • MySQL, PostgreSQL, SQL Server, DB2 에서 사용
  • 생성하면 영속성 컨텍스트의 PK값으로 쓰게 된다.
  • IDENTITY 전략은 em.persist() 시점에 즉시 INSERT SQL 실행하고 DB에서 식별자를 조회합니다.




1-2. SEQUENCE 생성 전략 특징

  • DB에서 값을 얻어와서 Member에 값을 넣어준 후에 commit한다.




1-3. allocationSize (성능 최적화)

  • 미리 메모리에 올려놓는다.
  • 시퀀스 한 번 호출에 증가하는 수





2. 연관관계의 주인

  • 객체의 양방향 관계는 참조가 2군데 있다. (A -> B, B -> A
    • 둘중 테이블의 외래 키를 관리할 곳을 지정해야 한다.
  • 연관관계의 주인 : 외래 키를 관리하는 참조
  • 주인의 반대편 : 외래 키에 영향을 주지 않음, 단순 조회
  • 주인은 mappedBy 사용 X
  • 주인이 아니면 mappedBy 속성으로 주인 지정




2-1. 누가 주인 ?

  • 외래 키가 있는 곳을 주인으로 정해라




2-2. 양방향 연관관계 고려

  • 객체 관점에서 양쪽 방향에 모두 값을 입력
public void setTeam(Team team) {
    this.team = team;
    team.getMembers().add(this);
}

 

  • 양방향 매핑시 무한 루프 주의




2-3. 양방향 매핑 정리

  • 단방향 매핑만으로 이미 연관관계 매핑 완료
  • 양방향 매핑은 반대 방향으로 조회 기능이 추가된 것 뿐
  • JPQL에서 역방향으로 탐색할 일이 많다.
  • 단방향 매핑을 잘하고 양방향은 필요할 때 추가해도 된다. (테이블에 영향을 주지 않는다.)





3. 일대다 [1:N]

  • 일(1)이 연관관계의 주인
  • 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있다.
  • @JoinColumn을 사용하지 않으면 조인 테이블 방식을 사용한다. (중간 테이블 하나 추가된다.)




3-1. 일대다 단방향 단점

  • 엔티티가 관리하는 외래 키가 다른 테이블에 있다.
  • 연관관계 관리를 위해 추가로 UPDATE SQL 실행

 

*일대다 단방향 매핑보다 다대일 양방향 매핑을 사용




3-2. 일대다 양방향 정리 []

  • 일대다 양방향 매핑은 존재하지 않는다.
  • 대신 다대일 양방향 매핑을 사용해야 한다.





4. 일대일 관계

  • 주 테이블이나 대상 테이블 중에 외래 키 선택 가능
  • 외래 키에 데이터베이스 유니크 제약조건 추가




4-1. 주 테이블에 외래 키

  • 주 객체가 대상 객체의 참조를 가지는 것 처럼
  • 주 테이블에 외래 키를 두고 대상 테이블을 찾는다.
  • 객체지향 개발자 선호, JPA 매핑 편리
  • 장점
    • 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
  • 단점
    • 값이 없으면 외래 키에 null 허용




4-2. 대상 테이블에 외래 키

  • 대상 테이블에 외래 키가 존재
  • 전통적인 데이터베이스 개발자 선호
  • 장점
    • 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
  • 단점
    • 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩된다.





5. 다대다 관계

  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
  • 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 한다.
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

@Entity
class Member {
}

@Entity
class Product {
}

@Entity
class MemberProduct {

  @Id @GeneratedValue
  private Long id;

  // @Id
  @ManyToOne
  @JoinColumn(name = "MEMBER_ID")
  private Member member;

  // @Id
  @ManyToOne
  @JoinColumn(name = "PRODUCT_ID")
  private Product product;
}





6. 상속 관계 매핑

 

6-1. 조인 전략

  • 엔티티 각각을 모두 테이블로 만든다.
  • 기본 키 + 외래 키 전략
  • 조회할 때 조인 자주 사용
  • 타입을 구분하는 컬럼을 추가해야 한다. (DTYPE)
    • @Inheritance(strategy = InheritanceType.JOINED)
    • @DiscriminatorColumn(name = "DTYPE")
      • 부모 클래스에 구분 컬럼을 지정한다.
    • @DiscriminatorValue("M")
      • 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다.
  • 장점
    • 테이블 정규화
    • 외래 키 참조 무결성 제약조건 활용
    • 저장공간 효율적
  • 단점
    • 조인 사용으로 인한 성능 저하
    • 조회 쿼리 복잡
    • 데이터를 등록할 INSERT SQL을 두 번 실행한다.

 

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public class Item {

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

    private String name;
    private int price;
}

@Entity
@DiscriminatorValue("M")
public class Movie extends Item {

  private String director;
  private String actor;
}




6-2. 단일 테이블 전략

  • 테이블을 하나만 사용한다.
  • 구분 컬럼 (DTYPE)으로 어떤 자식 데이터가 저장되었는지 구분한다.
  • 장점
    • 조회할 떄 조인을 사용하지 않으므로 일반적으로 가장 빠르다.
  • 단점
    • 자식 엔티티가 매핑한 컬럼은 모두 null을 허용하는 점을 주의해야 한다.
    • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다.
    • 상황에 따라서는 조회 성능이 오히려 느릴 수 있다.
  • 특징
    • 구분 컬럼을 꼭 사용해야 한다.
      • @DiscriminateColumn

 

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public class Item {

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

    private String name;
    private int price;
}

@Entity
@DiscriminatorValue("M")
public class Movie extends Item {

  private String director;
  private String actor;
}



Hibernate: 

    create table Item (
       DTYPE varchar(31) not null,
        ITEM_ID bigint not null,
        name varchar(255),
        price integer not null,
        artists varchar(255),
        author varchar(255),
        isbn varchar(255),
        actor varchar(255),
        director varchar(255),
        primary key (ITEM_ID)
    )




6-3. 구현 클래스마다 테이블 전략

  • 자식 엔티티마다 테이블을 만든다.
  • 자식 테이블 각각에 필요한 컬럼이 모두 있다.
  • 장점
    • 서브 타입을 구분해서 처리할 때 효과적이다.
    • not null 제약 조건을 사용할 수 있다.
  • 단점
    • 여러 자식 테이블을 함께 조회할 때 성능이 느리다. (UNION)
    • 자식 테이블을 통합해서 쿼리하기 어렵다.
  • 특징
    • 구분 컬럼을 사용하지 안흔ㄴ다.
    • 추천하지 않는 전략이다.

 

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Item {

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

  private String name;
  private int price;
}

@Entity
public class Movie extends Item {

  private String director;
  private String actor;
}





7. 매핑 정보 상속 (@MappedSuperclass)

  • 테이블과 매핑하지 않고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할
  • 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용
  • @Entity 클래스는 엔티티나 @MappedSuperclass로 지정한 클래스만 상속 가능

 

import javax.persistence.Entity;

@MappedSuperclass
public abstract class BaseEntity {

  private String createdBy;
  private LocalDateTime createdDate;
  private String lastModifiedBy;
  private LocalDateTime lastModifiedDate;
}

@Entity
public class Member extends BaseEntity {}





8. 프록시

  • 프록시 객체는 처음 사용할 때 한 번만 초기화
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다.
  • 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
  • 프록시 객체는 원본 엔티티를 상속 받는다. 따라서 타입 체크 시 instance of 사용한다.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 인티티를 반환한다.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다.





9. 프록시와 즉시로딩

  • 가급적 지연 로딩만 사용
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
  • 즉시 로딩은 JPQL에서 N + 1 문제를 일으킨다.
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY
  • @OneToMany, @ManyToMany는 기본이 지연 로딩


9-1. 즉시 로딩, 지연 로딩 정리

  • 지연 로딩 (LAZY)
    • 연관된 엔티티를 프록시로 조회한다.
    • 프록시를 실제 사용할 때 초기화하면서 데이터베이스를 조회한다.
  • 즉시 로딩 (EAGER)
    • 연관된 엔티티를 즉시 조회한다.
    • 하이버네이트는 가능하면 SQL 조인을 사용해서 한 번에 조회한다.




9-2. 프록시와 컬랙션 래퍼

  • org.hibernate.collection.internal.PersistentBag
    • 하이버네이트는 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고
    • 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는데 이것을 컬렉션 래퍼라고 한다.
  • 엔티티를 지연 로딩하면 프록시 객체를 사용해서 지연 로딩을 수행하지만 주문 내역 같은 컬렉션은 컬렉션 래퍼가 지연 로딩을 처리해준다.
  • 컬렉션 래퍼도 컬렉션에 대한 프록시 역할을 한다.




9-3. 컬렉션에 FetchType.EAGER 사용 시 주의점

  • 컬렉션을 하나 이상 즉시 로딩하는 것은 비권장
    • 컬렉션과 조인한다 -> [1:N] 조인
    • 일대다 조인 -> 결과 데이터가 [N] 쪽에 있는 수 만큼 증가
    • 서로 다른 컬렉션을 2개 이상 조인한다면 [N * M] 만큼 SQL 실행
      • 애플리케이션 성능 저하 원인
  • 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.
    • [N:1] 관계인 테이블에서 외래 키에 not null 제약 조건을 걸어두면 항상 내부 조인을 사용해도 된다.
    • 하지만 외래 키를 한번도 사용하지 않은 값이 있을 때 내부 조인을 하면 조회되지 않는 문제가 발생한다.
    • JPA는 [1:N]관계를 즉시 로딩할 때 항상 외부 조인을 사용한다.

 

  • @ManyToOne, @OneToOne (default : EAGER)
    • (optional = false) : 내부 조인
    • (optional = true) : 외부 조인
  • @OneToMany, @ManyToMany (default : LAZY)
    • (optional = false) : 외부 조인
    • (optional = true) : 외부 조인





10. 영속성 전이 : CASCADE

  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때




10-1. 영속성 전이 : 저장

 

@Entity
public static class Parent {

    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
    private List<Child> children = new ArrayList<>();
}

@Entity
public static class Child {

  @Id @GeneratedValue
  private Long id;

  @ManyToOne
  private Parent parent;
}

 

  • cascade = CascadeType.PERSIST
    • 부모를 영속화할 때 연관된 자식들도 함께 영속화한다.
  • 영속성 전이는 연관관계를 매핑하는 것과는 아무 관련이 없다.




10-2. 영속성 전이 : 삭제

 

@Entity
public static class Parent {

  @Id
  @GeneratedValue
  private Long id;

  @OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE)
  private List<Child> children = new ArrayList<>();
}

@Entity
public static class Child {

  @Id @GeneratedValue
  private Long id;

  @ManyToOne
  private Parent parent;
}

void remove() {
        Parent findParent=em.find(Parent.class,1L);
        em.remove(findParent);
}

 

  • cascade = CascadeType.REMOVE
    • 부모 엔티티만 삭제하면 연관된 자식 엔티티도 함께 삭제된다.




10-3. CASCADE의 종류

public enum CascadeType {

    /** Cascade all operations */
    ALL, 

    /** Cascade persist operation */
    PERSIST, 

    /** Cascade merge operation */
    MERGE, 

    /** Cascade remove operation */
    REMOVE,

    /** Cascade refresh operation */
    REFRESH,

    /**
     * Cascade detach operation
     *
     * @since 2.0
     * 
     */   
    DETACH
}





11. 고아 객체 (ORPHAN)

  • JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공한다. : 고아 객체 제거
  • 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제된다.

 

@Entity
public static class Parent {

    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Child> children = new ArrayList<>();
}

 

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다.
  • 이 기능은 참조하는 곳이 하나일 때만 사용해야 한다.
  • 만약 삭제한 엔티티를 다른 곳에서도 참조한다면 문제가 발생한다.
  • orphanRemoval은 @OneToOne, @OneToMany에서만 사용한다.




11-1. 영속성 전이 + 고아 객체, 생명주기

  • CascadeType.ALL + orphanRemoval = true를 동시에 사용
    • 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.

 

  • 자식을 저장하려면 부모에 등록만 하면 된다. (CASCADE)
Parent parent = em.find(Parent.class, parentId);
parent.addChild(child1);

 

  • 자식을 삭제하려면 부모에서 제거하면 된다 (orphanRemoval)
Parent parent = em.find(Parent.class, parentId);
parent.getChildren().remove(removeObject);





12. 값 타입

  • JPA의 데이터 타입은 크게 엔티티 타입, 값 타입이 있다.

 

12-1. 기본 값타입

  • 자바 기본 타입 (int, double, ...)
  • 래퍼 클래스 (Integer, ...)
  • String




12-2. 임베디드 타입 (복합 값 타입)

  • 새로운 값 타입을 직접 정의해서 사용 : 임베디드 타입
  • @Embedded
  • @Embeddable
  • @AttributeOverrides




12-3. 값 타입과 불변 객체

  • 값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념으로 값 타입은 단순하고 안전하게 다룰 수 있어야 한다.




12-3-1. 값 타입 공유 참조

  • 임베디드 타입과 같은 타입을 여러 엔티티에서 공유하면 위험하다.
  • 공유 참조로 인해 발생하는 버그를 막으려면 값을 복사해서 사용하면 된다.




12-3-2. 값 타입 복사

  • 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
  • 임베디드 타입처럼 직접 정의한 값 타입은 객체 타입이다.

 

member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();

Address newAddress = address.clone();

newAddress.setCity("NewCity");
member2.setHomeAddress(newAddress);




12-3-3. 불변 객체

  • 객체를 불변하게 만들면 값을 수정할 수 없으므로 부작용을 원천 차단할 수 있다.
  • 따라서 값 타입은 될 수 있으면 불변 객체로 설계한다.

 

@Embeddable
public class Address {

  @Column(name = "city")
  String city;
  String street;
  String state;
  @Embedded
  Zipcode zipcode;

  protected Address() {
  }

  public Address(String city) {
    this.city = city;
  }

  public String getCity() {
    return city;
  }
}




12-4. 값 타입 비교

자바가 제공하는 객체 비교는 2가지이다.

  • 동일성 비교 : ==
  • 동등성 비교 : equals()
    • 값 타입의 equals() 메서드를 재정의할 때는 보통 모든 필드의 값을 비교하도록 구현한다.





12-5. 값 타입 컬렉션

값 타입을 하나 이상 저장하려면 컬렉션에 보관한다.

  • @ElementCollection
  • @CollectionTable

 

@Entity
class Member {

  @Id @GeneratedValue
  // ...

  @ElementCollection
  @CollectionTable(name = "FAVORITE_FOODS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
  @Column(name = "FOOD_NAME")
  private Set<String> favoriteFoods = new HashSet<>();

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

  // ...
}

 

  • 값 타입 컬렉션을 사용하는 favoriteFoods, addressHistory에 @ElementCollection을 지정
  • 관계형 데이터베이스의 테이블은 컬럼 안에 컬렉션을 포함할 수 없다.
    • 따라서 별도의 테이블을 추가하고 @CollectionTable를 사용해서 추가한 테이블을 매핑한다.




12-6. 값 타입 컬렉션 저장

  • 마지막에 member 엔티티만 영속화한다.
  • 실제 INSERT SQL
    • member : 1
    • member.favoriteFoods : 3
    • member.addressHistory : 2
  • 값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다.

 

Member member = new Member();

// 임베디드 값 타입
member.setHomeAddress(new Address("서울", "노원구", "000-000"));

// 기본 값 타입 컬렉션
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("탕수육");

// 임베디드 값 타입 컬렉션
member.getAddressHistroy().add(new Address("서울", "노원구2", "222-222"));
member.getAddressHistroy().add(new Address("서울", "노원구3", "333-333"));

em.persist(member);




12-7. 값 타입 컬렉션 조회

  • 값 타입 컬렉션도 조회할 때 페치 전략을 선택할 수 있다.
  • LAZY가 기본




12-8. 값 타입 컬렉션 수정

  • 임베디드 값 타입 수정
  • 기본값 타입 컬렉션 수정
  • 임베디드 값 타입 컬렉션 수정
    • 값 타입은 불변해야 한다.
    • 따라서 컬렉션에서 삭제하고 새롭게 등록해야 한다.




12-9. 값 타입 컬렉션의 제약사항

  • 값 타입은 에티티와 다르게 식별자 개념이 없다.
  • 값은 변경하면 추적이 어렵다.
  • 값 타입은 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
  • 값 타입 컬렉션을 매핑하는 테이블을 모든 컬럼을 묶어서 기본키를 구성해야 한다. : null X, 중복 저장 X




12-10. 값 타입 컬렉션 대안

  • 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려한다.
  • 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용한다.
  • 영속성 전이 + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용한다.
반응형

댓글