development/spring

JPA 정리본

bokshiri 2023. 9. 15. 05:34

*** JPA ***

 

   ※ JPA의 핵심 내용

      1. Persistence Context - 영속성 컨텍스트

      2. ORM - 객체와 관계형 DB의 매핑 (Object Relational Mapping) -> JPA는 JAVA의 ORM 표준 명세이다.

   

1. JPA는 인터페이스, Hibernate는 구현체이다. (Spring-data-JPA는 Spring에서 JPA를 사용할 수 있도록 프레임워크에서 제공하는 JPA)

 1-1. JAVA Persistence API - 자바 진영에서 제공하는 표준 ORM 기술이다. (객체와 RDB의 릴레이션 매핑)

 1-2. JPA는 스레드가 생성될 때마다 EntityManagerFactory에서 EntityManager를 생성한다. 

     EntityManager는 내부적으로 DB커넥션 풀을 활용하여 DB에 커넥션한다.

     

2. JPA의 영속성 컨텍스트란 무엇인가?

 - 자바 어플리케이션과 DB의 중간단계 역할을 하는 컨텍스트

 JPA는 기본적으로 스레드가 생성될 때마다 EntityManagerFactory에서 EntityManager를 생성하며,

 영속성 컨텍스트는 트랜잭션 단위로 생성된다. (트랜잭션 종료시 함께 종료 - 라이프 사이클이 동일함)

 

 여기서, EntityManager를 통해 Persistence Context를 제어한다. (em.persist(), em.find() 등)

 ※ 영속성 컨텍스트의 특징

   1. 1차 캐싱 기능을 제공한다. em.find()를 통해 객체를 조회할 경우 이미 영속화된 객체가 있으면 해당 객체를 반환함으로써 쿼리 실행x

                     만일 객체가 없을 경우 select 쿼리를 날려 대상을 조회한 후 영속성 객체로 등록한다.

                     (여기서, 1차 캐싱을 하면서 영속성 컨텍스트에 스냅샷을 남겨둠 -> 추후 트랜잭션 롤백이나, 더티체킹시에 변경사항을 해당 스냅샷을 활용하여 진행한다.)

   2. 영속성 컨텍스트의 객체는 반드시 해당 엔티티의 primary key(식별자)로 찾아야 한다.

   3. 위에서 언급한 캐싱은 말 그대로 영속성 컨텍스트 단위로 적용되는 사항이다. (캐싱 객체를 반환)

      만일 100명의 사용자가 request를 날릴경우 톰캣은 100개의 요청을 어플리케이션으로 보낼 것이고 EntityManagerFactory는 각 스레드별로 EntityManager를 생성할 것이다.(100개)

      각 요청들은 모두 트랜잭션이 분리될 것이므로 영속성 컨텍스트는 각각 적용되어 총 100개가 생성되므로 해당 캐시는 글로벌 캐시가 아니다.

   4. em.find()로 두번 조회를 하더라도 영속성 객체 자체가 반환되므로 두 변수의 주소값이 같다. 즉 같은 영속성 객체를 바라본다.

   5. 영속성 컨텍스트에서 분리를 하게되면(비영속 상태) 영속 객체의 상태가 변하더라도 flush() 시점에 업데이트 쿼리가 실행되지 않는다. (영속성 컨텍스트의 객체만을 대상으로 함)

   6. 트랜잭션 커밋 시점에 flush()가 실행됨으로써, Dirty Checking을 하여 스냅샷과 현재 영속성 객체의 상태를 비교하여 변경사항 발생시 쓰기지연 SQL 저장소에 SQL을 저장해놓은 후 쿼리를 실행한다. 그 후 트랜잭션 commit -> DB반영

   

3. 매핑 관계

   - @Entity : JPA Entity 선언

   - @Table : RDB Table 선언

   

   기본키 생성 전략

   IDENTITY - SEQUENCE 전략의 차이

   IDENTITY는 DBMS의 생성전략을 따른다. (e.g. MYSQL - AUTO_INCREMENT) 즉 DBMS에 먼저 데이터를 밀어넣고 해당 값을 가져와 엔티티에 동기화 (따라서 em.persist 시점에 flush가 이루어짐)

   SEQUENCE의 경우 반대로 시퀀스값을 먼저 조회해와서 엔티티에 담은 후 커밋 시점에 flush처리

   

4. 객체 연관관계 - https://jeong-pro.tistory.com/m/231 / https://velog.io/@yuseogi0218/JPA-%EC%9D%BC%EB%8C%80%EB%8B%A4-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84

   - 고려사항은 총 3가지이다.

   

   1. 방향 : 단방향 / 양방향

   2. 연관 관계의 주인 : Master/Slave 구조로 생각하자. 두 객체의 연관관계에서 누가 Master역할을 할지를 결정하는 개념

   3. 다중성 : 1:1, 1:N, N:1, N:M 관계

   

   양방향 관계의 경우 주인이 되는 객체가 무엇인지 정의하는 것이 반드시 필요함.(JPA가 무엇을 기준으로 수정/삭제 처리를 할지, 어떤 것을 읽기전용으로 사용할지 결정해야 하기 때문)

   기본적으로 외래키가 존재하는 객체가 주인(Master)이 될 것이고, 보조(Slave)객체에는 필드에 @OneToMany(mappedBy = "board") 위와 같이 mappedBy를 붙여주어야 한다.

   mappedBy가 붙지 않은 것이 Master가 됨.

   

   Board(게시판)과 Post(게시글)의 관계는 1:N 이다.

   RBD에서는 릴레이션간의 관계가 항상 양방향이지만 JPA에서는 객체간의 관계로 해석해야 하기 때문에 단방향과 양방향 모두를 고려하여야 함.

   Board 기준에서 보면 Post와의 관계는 1:N(일대 다), Post 기준에서 보면 Board와의 관계는 N:1(다대 일)

   

   기준이 되는 클래스(객체)의 입장에서 @ManyToOne 혹은 @OneToMany 를 결정하면 된다. (+ @OneToOne)

   다대 일 단방향일 경우 Board 기준에서 Post에 외래키가 들어갈 것이고, 외래키 Board 필드에 @JoinColumn과 @ManyToOne 어노테이션이 사용되어야 한다.

   다대 일 양방향의 경우 양쪽 모두 참조가 필요하므로 Board 객체에도 Post의 List를 필드로 받는다. (List<Post> postList = new ArrayList<>(); 형태로 작성 후

   어노테이션 @OneToMany (Board 기준에서는 일대 다 이므로) 작성, 주인은 참조 외래키가 있는 객체이므로 mappedBy = "board" 추가 

   

   일대 다 단방향의 경우 Board : Post 에서 Board에 @OneToMany @JoinColumn 어노테이션으로 List<Post>를 참조가능

   하지만 실무에서는 다대 일로 해소하여 사용한다. (일대 다 양방향의 경우도 다대 일 양방향으로 해소)

   

   N:M 관계는 RDB 기준으로 보면 두 테이블 사이에 관계 해소를 위한 중간 테이블이 필요한데, JPA에서는 자동으로 해당 테이블을 만들어준다.

   하지만 위 경우 쿼리나 조인이 복잡해지므로 실무에서는 사용하지 않음.

   설계 단계에서 중간 테이블을 엔티티 형태로 따로 설계하여 1:N, N:1 관계로 해소한다.

   e.g. ) 주문 - 상품 관계의 경우 주문상품이라는 매핑 릴레이션이 필요. (각각 별도의 엔티티로 분리하여 작성한다.)

   

   1:1 관계 - @OneToOne

   단방향은 지원x 양방향으로 상호 참조하되, 주 테이블(참조 외래키를 갖는 테이블)을 설정해야 함. (그 기준은?.. 애매) -> 보조(slave) 엔티티에는 mappedBy 붙여준다.

   

   ※ @JoinColumn은 참조 외래키 필드에 정의한다...

   

5. 고급 매핑 

   (참고 주소: https://ict-nroo.tistory.com/128  /  https://velog.io/@shininghyunho/JPA%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-7.%EA%B3%A0%EA%B8%89-%EB%A7%A4%ED%95%91

           https://velog.io/@mindddi/DB%EB%AA%A8%EB%8D%B8%EB%A7%81-%EA%B4%80%EA%B3%84%EC%8A%88%ED%8D%BC-%EC%84%9C%EB%B8%8C%ED%83%80%EC%9E%85

   

   - 자바의 상속과 관계형 데이터베이스의 슈퍼타입-서브타입 관계의 매핑이라고 보면 됨.

   ※ 매핑의 3가지 방법

      1. 각각의 테이블로 변환 (조인 활용)

      2. 통합 테이블로 변환 (테이블 하나만 활용)

      3. 서브타입 테이블로 변환 (서브타입마다 공통속성을 갖도록 테이블 각각 구성)

      

      ===> 실무에서는 3번 케이스를 사용하지 않는다.(정규화 원칙에 위배)

           조인 방식(각각 별도의 테이블로 분리)과 통합 테이블(하나의 테이블) 방식 중 데이터의 양을 고려하여 적절한 전략을 선택하는 것이 필요.

           1,2의 경우 부모 클래스를 일반 클래스로 선언, 3번 케이스는 추상 클래스로 선언한다.

           why? ) 혹시라도 인스턴스화가 될 수 있으므로 미연에 방지하는 차원

      

   #짚고 넘어갈 지식

   추상 클래스는 생성자 선언이 가능하다. 단, 일반 클래스처럼 new 를 사용해 생성자 호출x 자식 클래스에서 super()를 통해 간접 호출만 가능.

   

   1. 부모-자식 각각의 테이블로 변환 (조인 활용)

      - @Inheritance(strategy = InheritanceType.JOINED) 

      - @DiscriminatorColumn(name = "DTYPE") => 테이블의 관점에서 보면, 해당 데이터가 어떤 자식 타입인지 표현하기 위한 컬럼(부모테이블에 생성)

        자식 클래스에서는 @DiscriminatorValue("A") 형태로 value를 지정한다. - 데이터가 insert될 때 지정된 value로 DTYPE이 저장됨

      - 자식 엔티티에는 Id가 없는데 부모 테이블에 Id를 자식 테이블에서 그대로 사용한다. 만약 자식 테이블 기본키 컬럼명을 따로 설정하려면 @PrimaryKeyJoinColumn을 사용한다.

        e.g.) @PrimaryKeyJoinColumn(name = "BOOK_ID")

        

   2. 통합 테이블로 변환 (부모 테이블 하나만 사용)

      - @Inheritance(strategy = InheritanceType.SINGLE_TABLE)

      - @DisciminatorColumn(name = "DTYPE") => 어떤 타입인지 나타내기 위한 구분자

      - 마찬가지로 자식 클래스는 @DiscriminatorValue("A")로 DTYPE에 들어갈 value 설정 필요.

      

   3. 서브타입 테이블 각각 변환 (상속개념x)

      - @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)

      - 부모 클래스를 추상 클래스로 선언한다.

      - 실무에서는 일반적으로 사용하지 않는다.

      

   @MappedSuperclass

      - 참조: https://velog.io/@sa1341/JPA-Auditing

      - JPA Auditing (공통 필드 분리)와 연관됨.

      - 단건일 경우 @AttributeOverride, 복수 건일 경우 @AttributeOverrides 어노테이션을 사용하여 부모 클래스의 필드를 재정의할 수 있음.

      - @AssociationOverrides를 활용하여 연관관계 재정의 가능.

      

      

   ※ 식별관계와 비식별관계

      - 식별관계: 부모의 기본키를 기본키+외래키로 사용

      - 비식별관계: 부모의 기본키를 외래키로 사용

      비식별관계를 주로 사용 (식별관계는 꼭 필요한 경우에만...)

   

   1. 복합키 비식별관계 매핑

   

   JPA에서는 식별자가 2개 이상, 즉 복합키일 경우 별도의 클래스를 만들어 분리한 후 equals() hashCode() 메서드를 구현해야 한다.

   * 복합키 구현 지원을 위한 어노테이션

      - @IdClass : 관계형 DB에 가까운 방식

      - @EmbeddedId : 객체 지향에 가까운 방식

   

   1. 엔티티 클래스에 @Id를 다중으로 선언한 후 해당 필드들을 별도의 클래스로 분리하여 처리 => 필드의 이름은 두 클래스가 상호 일치해야 한다.

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

   3. equals(), hashCode() 구현해야 한다.

   4. 기본 생성자가 있어야 한다. (생성자 없이 하면 될듯... 자바 컴파일러 자동 생성)

   5. 식별자 클래스는 public

   

   @IdClass

   영속성 컨텍스트에 저장할 때는 동일한 방식으로 엔티티 객체를 만들어 키 세팅하면 됨

   e.g. ) Parent parent = new Parent();

         parent.setId1("1");

         parent.setId2("2");

         em.persist(parent);

         

   조회를 할 때에는 키 매핑 클래스를 활용하여 처리

   e.g. ) ParentId parentId = new ParentId("1","2");

         Parent parent = em.find(Parent.class, parentId);

   

   ※ 자식 클래스 선언방법

   

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

   }

   

   @EmbeddedId

      - @IdClass와 마찬가지로 키 매핑 클래스를 만들기는 하나, Parent 클래스에는 ParentId 객체를 필드로 받고 @EmbeddedId 어노테이션을 붙여준다.

      - 키 매핑 클래스에는 클래스단에 @Embeddable 사용

   

   Parent parent = new Parent();

   ParentId parentId = new ParentId("1","2");

   parent.setId(parentId);

   em.persist(parent);

   

   조회할 때에는 ParentId 객체로 조회 - @IdClass와 동일.

   

   2. 복합키 식별관계 매핑

      - 자식 엔티티는 부모 엔티티의 키를 받아 기본키로 사용하기 때문에 키 매핑 클래스를 별도로 선언하고 복합키 설정을 해주어야 함.

      

   조인 테이블(1:1, 1:N, N:1, N:M ...)은 블로그 글 참조..

   

6. 프록시와 연관관계 관리

   - 프록시는 JPA의 지연로딩과 관계가 있음.

  • 연관관계에 있는 엔티티를 조회할 때 이용할 수 있음.

   

JPA는 즉시로딩과 지연로딩을 모두 지원함. 지연로딩을 사용할 경우 JPA는 조회 시점에 db를 바로 조회하지 않고, 실제 객체에 대한 참조를 담는 프록시 객체를 반환함. 실제 값이 필요한 시점에 db를 조회하여 프록시 객체에 실 객체에 대한 참조를 담고 있는 target 에 참조를 전달함으로써 값을 조회할 수 있게 해준다.

 

  • 기본적인 영속성 엔티티 조회방법: em.find()
  • 지연 로딩에 사용하는 메서드: em.getReference();
  • 양자 모두 엔티티의 기본키를 사용하여 조회한다는 점에서 같다.

 

프록시 객체는 최초 한 번만 초기화된다.

  • 프록시 객체 초기화란?

사용자가 프록시 객체에 특정 값을 사용하였을 때 프록시 객체의 target에 실 객체에 대한 참조정보가 없을 경우 프록시 객체는 영속성 컨텍스트에 영속성 엔티티에 대한 정보를 요청하게 된다. 영속성 컨텍스트는 db를 조회하여 객체를 만든후 영속화 시키며, 해당 객체에 대한 참조를 프록시의 target에 담는다.

 

ersistenceUnitUtil.isLoaded(Object entity) 메서드를 사용하면 프록시 객체의 초기화 여부를 확인할 수 있다.

  • 초기화가 됐을 경우 : true
  • 초기화가 이루어지지 않은 경우 : false

 

연관관계 엔티티를 즉시 로딩: @ManyToOne(fetch = FetchType.EAGER)

연관관계 엔티티를 지연 로딩: @ManyToOne(fetch = FetchType.LAZY)

 

조회하는 대상이 이미 영속성 컨텍스트의 엔티티인 경우 프록시를 사용하지 않고 실제 객체(엔티티)를 조회한다. 즉 지연로딩을 사용할 수 없다.

 

추천하는 방법은 모든 연관관계에 지연 로딩을 사용하는 것이다. 그리고 애플리케이션 개발이 어느 정도 완료 단게에 왔을 때 실제 사용하는 상황을 보고 꼭 필요한 곳에만 즉시 로딩을 사용하도록 최적화하면 된다. (일대 다 조인 연관관계 - 컬랙션 을 즉시 로딩하는 것은 권장하지 않음. 왜? 대량의 데이터 발생으로 인해 메모리 과부하 문제 발생..)

 

영속성 전이 - CASCADE

 - 특정 엔티티를 영속화할 때 해당 엔티티와 연관관계가 있는 엔티티도 함께 영속화하기 위해 사용

 

@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)

private List<Child> children = new ArrayList<>();

 

CascadeType.REMOVE로 설정하면 부모 엔티티만 삭제하면 자식 엔티티도 함께 삭제한다.

  • RDB에서 제공하는 제약조건 옵션 - on delete cascade와 동일한 기능.

 

public enum CascadeType {

    ALL, // 모두 적용

    PERSIST, // 영속

    MERGE, // 병합

    REMOVE, // 삭제

    REFRESH, // REFRESH

    DETACH, // DETACH

}

 

 

참고로 PERSIST, REMOVE는 em.persist(), em.remove()를 실행할 때 바로 전이가 발생하지 않고, 플러시를 호출할 때 전이가 발생한다. (전이 시점은 flush()가 발생할 때.)

 

  • 고아 객체
  • 부모 객체와 연관관계가 끊어진 자식 객체를 고아객체라고 함.
  • JPA는 참조가 없는 자식객체(고아객체)를 자동으로 제거해주는데, 이것을 고아객체 제거라고 한다.
  • e.g.) @OneToMany(orphanRemoval = true)

 

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

 

또한 부모가 제거되면 자식은 참조가 없어지게 되므로 이 역시 제거대상에 포함된다. -> CascadeType.REMOVE와 동일.

 

  • 영속성 전이와 고아객체, 생명주기
  • CascadeType.ALL + orphanRemoval = true를 동시에 사용하게 되면 부모 엔티티를 통해 자식의 생명주기를 관리할 수 있다.
  • 자식을 저장하려면 부모에 등록하면 되고,(persist 전에 child객체 세팅) 자식을 삭제하려면 부모의 컬랙션에서 자식객체를 제거하면 된다.

 

참조: https://velog.io/@ayoung0073/JPA-%ED%94%84%EB%A1%9D%EC%8B%9C-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EA%B4%80%EB%A6%AC

 

  1. 값 타입 - 교재 320p

 

  • 기본값 타입 : 자바의 원시타입, 참조타입
  • 임베디드 타입 : 사용자 정의 클래스 타입 (회원과 주소 클래스 관계)
  • 컬랙션 값 타입

 

 

회원의 주소지에 개인 주소 외에 회사 주소가 추가적으로 필요한 경우, 같은 Address 클래스를 사용하되 @AttributeOverrides 어노테이션을 사용하여 중복 컬럼 네임을 별도로 지정해준다.  - p.327~328

(코드가 지저분해질 수 있는 문제가 있음. 다행히 한 클래스에서 같은 타입의 임베디드 클래스를 사용하는 경우는 많지 않음.)

 

임베디드 클래스 단 - @Embeddable

임베디드 사용 필드 단 - @Embedded

 

  • 값 타입과 불변 객체

  원시 타입은 스택영역에 변수명과 값이 저장되기 때문에, 값을 복사하여 전달하는 구조

e.g.) int a = 3;

int b = a; //a = 3 b = 3

         b = 10; //a = 3, b = 10

  반면 참조타입은 변수에 객체에 대한 참조 주소를 저장하기 때문에 같은 힙영역 객체를 바라보게 됨

 

e.g.) KylenClass a = new KylenClass();

KylenClass b = a; 

//변수명은 각각 a, b 로 다르나 해당 변수들은 스택 영역에 동일한 객체 주소값을 가지게 됨으로써 하나의 객체를 가리키게 됨. a변수로 객체의 상태를 변경하면 b도 함께 변경된다는 의미.

 

서로 다른 멤버의 주소를 설정할 때 같은 임베디드 객체를 사용하게 될 경우 한 회원의 주소를 변경하면 나머지 회원도 함께 변경되는 문제가 발생할 수 있음.

객체를 깊은 복사를 통해 분리한 후 저장하는 것이 필요. 

 

  • 위와 같은 문제를 근본적으로 해결하기 위해서는 객체를 불변객체로 선언하는 것이 필요하다. (상태값이 변경되지 않는 객체) - setter x
  • 값 타입의 비교 - 기본적으로 자바는 == 를 사용하여 객체의 참조주소를 비교하고, .equals() 메서드를 통해 객체의 상태를 비교한다. 일반적으로 비교하고자 하는 객체는 equals(), hashcode() 메서드를 재정의하여 사용한다.