Language/Java

JPA에 대해 깊이 알아보자

hu6r1s 2024. 3. 7. 16:28

JPA에 대해 깊이 알아보기 위해 체크리스트

  • JDBC 와 Persistence Framework 의 차이점에 대해 설명할 수 있다.
  • JDBC Template 과 MyBatis 의 차이점에 대해 설명할 수 있다.
  • SQL Mapper 와 ORM 의 차이점에 대해 설명할 수 있다.
  • MyBatis 가 쿼리를 생성하는 2가지 방법에 대해 설명할 수 있다.
  • 영속성 컨텍스트와 쓰기지연의 연관성에 대해서 설명할 수 있다.
  • 다대다 관계를 현업에서 주로 어떻게 구현하여 사용하는지 설명할 수 있다.
  • 부모가 자식의 영속성을 완전히 관리하기 위해서 사용하는 cascade 옵션을 설명할 수 있다.
  • 현업에서 주로 사용하는 Fetch 전략에 대해서 설명할 수 있다.
  • QueryDSL 의 버전별 연동방법을 설명할 수 있다.
  • QueryDSL 로 페이징 및 정렬 쿼리를 작성할 수 있다.
  • JPA 쿼리가 응답해주는 Page<T>, Slice<T>, Iterable<T> 의 차이점을 설명할 수 있다.
  • Entity 의 일부필드만으로 생성 또는 업데이트 하는 방법을 설명할 수 있다.
  • Transcation 의 Isolation 과 propagation 종류 및 차이점에 대해 설명할 수 있다.
  • 현업에서 사용하는 Transaction 전파전략을 설명할 수 있다.
  • SpringDataJpa 에서 제공하는 여러 고급 기능들의 사용방법을 설명할 수 있다.

JDBC와 Persistence Framework의 차이점

JDBC

JDBC는 Java DataBase Connectivity의 약자로 문장 그대로 자바와 데이터베이스를 연결시켜주기 위해 만들어졌다.

  • JDBC Driver Manager 는 런타임 시점에
    • Connection(연결) 을 생성하여 쿼리를 요청할 수 있는 상태를 만들어주고
    • Statement(상태) 를 생성하여 쿼리를 요청하게 해주고
    • ResultSet(결과셋) 을 생성해 쿼리 결과를 받아올 수 있게 해준다.

Connection을 생성하여 실행할 쿼리를 준비하여 Statement 준비와 실행시키면 결과를 ResultSet에 담아준다.

사용을 하고나서 Connection, Statement, ResultSet 닫아 리소스가 낭비되는 것을 막아줘야 한다.

Persistence Framework

  • SQL 쿼리 요청시 중복 코드 발생
  • DB별 예외에 대한 구분 없이 Checked Exception (SQL Exception) 처리
  • Connection, Statement, ResultSet 닫기

이러한 문제점을 해결하기 위해서 Persistence Framwork가 생겨났다.

  • SQL Mapper : JDBC Template, MyBatis 👈 요게 먼저나옴
  • ORM : JPA, Hibernate

모든 Persistence Framework는 내부적으 JDBC를 사용한다.

JDBC Template 과 MyBatis 의 차이점

JDBC Template

위에서 언급한 JDBC의 반복 작업에 대해 대신 처리해준다.

대신 쿼리를 작성해줘야 한다.

JDBC Template에서는 DataSource에서 Connection을 대신 연결해준다.

MyBatis

JDBC Template와 마찬가지로 JDBC의 반복 작업을 대신 처리해준다.

하지만 MyBatis는 쿼리를 문자열로 직접 입력하지 않고

SQL 쿼리들을 XML 파일에 작성하여 코드와 SQL을 분리하여 사용한다.

 

이렇게 하여 JDBC Template보다 동적 쿼리를 훨씬 더 간편하게 사용할 수 있다.

SQL Mapper 와 ORM 의 차이점

SQL Mapper

Object와 SQL의 필드을 매핑하여 데이터를 객체화하는 기술.

객체와 테이블간의 관계를 매핑하는 것이 아니라, SQL문을 직접 작성하고 쿼리 수행결과를 어떠한 객체에 매핑하여 줄 지 바인딩하는 방법. 즉 SQL 의존적인 방법이다.

ORM

Object와 DB테이블을 매핑하여 데이터를 객체화하는 기술.

CRUD 관련 메소드를 사용하면 자동으로 SQL이 만들어져 개발자가 반복적인 SQL을 직접 작성하지 않아도 되고, DBMS에 종속적이지 않다.

MyBatis 가 쿼리를 생성하는 2가지 방법

MyBatis에서 쿼리를 생성하는 방법에는 2가지가 있다.

1. xml을 이용

xml 파일에서 쿼리를 작성하여 매핑한다.

2. 어노테이션을 이용

구현할 함수 위에 어노테이션을 달아서 구현한다.

영속성 컨텍스트와 쓰기지연의 연관성

영속성이란?

영속성이 데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터의 특성이다.

영속성을 갖지 않으면 데이터는 메모리에서만 존재하게 되고, 프로그램이 종료되면 해당 데이터는 모두 사라지게 된다.

그래서 우리는 데이터를 파일이나 DB에 영구 저장함으로써 데이터에 영속성을 부여한다.

  1.  비영속(new/transient) - 엔티티 객체가 만들어져서 아직 저장되지 않은 상태로, 영속성컨텍스트와 전혀 관계가 없는 상태
  2. 영속(managed) - 엔티티가 영속성 컨텍스트에 저장되어, 영속성 컨텍스트가 관리할 수 있는 상태
  3. 준영속(detached) - 엔티티가 영속성 컨텍스트에 저장되어 있다가 분리된 상태로, 영속성컨텍스트가 더 이상 관리하지 않는 상태
  4. 삭제(removed) - 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제하겠다고 표시한 상태

JPA의 관점에서 정리해보자면

  • persist(),merge() > `(영속성 컨텍스트에 저장된 상태)` > flush() > `(DB에 쿼리가 전송된 상태)` > commit() > `(DB에 쿼리가 반영된 상태)`
Item item = new Item();		
item.setItemNm("테스트 상품");	// 1

EntityManager em = entityManagerFactory.createEntityManager();
EntityTransaction transaction = em.getTransaction();
	
transaction.begin();		
em.persist(item);		// 2
em.flush(item).     
transaction.commit();		

em.close();			// 3

1️⃣  비영속
2️⃣  영속
3️⃣  준영속

쓰기 지연

쓰기 지연이 발생하는 시점

  • flush() 동작이 발생하기 전까지 최적화한다.
  • flush() 동작으로 전송된 쿼리는 더이상 쿼리 최적화는 되지 않고, 이후 commit()으로 반영만 가능하다.

쓰기 지연 효과

  • 여러개의 객체를 생성할 경우 모아서 한번에 쿼리를 전송한다.
  • 영속성 상태의 객체가 생성 및 수정이 여러번 일어나더라도 해당 트랜잭션 종료시 쿼리는 1번만 전송될 수 있다.
  • 영속성 상태에서 객체가 생성되었다 삭제되었다면 실제 DB에는 아무 동작이 전송되지 않을 수 있다.
  • 즉, 여러가지 동작이 많이 발생하더라도 쿼리는 트랜잭션당 최적화 되어 최소쿼리만 날라가게된다.

다대다 관계를 현업에서 주로 어떻게 구현하는가?

@ManyToMany

다대다 관계를 나타내는 매핑 정보이다.(N:M)

다대다 설정을 하게되면 중간 매핑테이블(JoinTable)이 자동으로 생성된다.

중간 매핑 테이블은 JPA상에서 숨겨져서(Entity 정의 없이) 관리된다.

@Entity
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToMany(mappedBy = "parents")
    private List<Child> childs;
}

@Entity
public class Child {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToMany
    @JoinTable(
        name = "parent_child",
        joinColumns = @JoinColumn(name = "parent_id"),
        inverseJoinColumns = @JoinColumn(name = "child_id")
    )
    private List<Parent> parents;
}

 

하지만 이러한 방식은 매핑 테이블 관리가 불가능하여서 실무에서는 잘 사용하지 않는 기능이다.

실무에서는 매핑 테이블을 아래와 같은 형태로 직접 정의한다.

TableA(`@OneToMany`) > MappingTable(`@ManyToOne`, `@ManyToOne`) > TableB(`@OneToMany`)
@Entity
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @OneToMany(mappedBy = "parent")
    private List<ParentChild> parentChilds;
}

@Entity
public class ParentChild {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne
    @JoinColumn("parent_id")
    private Parent parent;

    @ManyToOne
    @JoinColumn("child_id")
    private Child child;
}

@Entity
public class Child {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @OneToMany(mappedBy = "child")
    private List<ParentChild> parentChilds;
}

 

중간 테이블을 직접 엔티티를 구성하여 다대다 관계를 나타낸다.

부모가 자식의 영속성을 완전히 관리하기 위해서 사용하는 cascade 옵션

Cascade

Cascade는 영속성 전이로,  영속성 전이는 JPA에서 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용한다.

 

사용 위치

  • 연관관계의 주인 반대편 - 부모 엔티티(다대일에서 )
  • 즉, @OneToMany 가 있는 쪽 또는 @OneToOne 도 가능
  • 예를들어, 게시글과 첨부파일이라면 에 해당하는 게시글에 설정한다.

사용 조건

  • 양쪽 엔티티의 라이프사이클이 동일하거나 비슷해야한다.
    • 예를들어, 게시글이 삭제되면 첨부파일도 같이 삭제 되어야 한다.
  • 대상 엔티티로의 영속성 전이는 현재 엔티티에서만 전이 되어야 한다. (다른곳에서 또 걸면 안됨)
    • 예를들어, 첨부파일을 게시글이 아닌 다른곳에서 영속성 전이를 하면 안된다.

옵션 종류

  • ALL : 전체 상태 전이
  • PERSIST : 저장 상태 전이
  • REMOVE : 삭제 상태 전이
  • MERGE : 업데이트 상태 전이
  • REFERESH : 갱신 상태 전이
  • DETACH : 비영속성 상태 전이

orphanRemoval (고아 객체 제거)

사용 위치

  • `@OneToMany` 또는 `@OneToOne`에서 사용 - 부모 엔티티

사용법

  • Cascade.REMOVE 와 비슷한 용도로 삭제를 전파하는데 쓰인다.
  • 부모 객체에서 리스트 요소삭제를 했을경우 해당 자식 객체는 매핑정보가 없어지므로 대신 삭제해준다.
    • 요건 DB 에서는 절대 알 수 없는 행동이다. (부모가 자식의 손을 놓고 버리고 간 고아 객체)

 Cascade.REMOVE  orphanRemoval 차이점은 무엇인가?

Cascade.REMOVE의 경우 에 해당하는 부모 엔티티를 em.remove를 통해 직접 삭제할 때,그 아래에 있는 에 해당하는 자식 엔티티들이 삭제되는 것입니다.

orphanRemoval=true는 위 케이스도 포함하며,에 해당하는 부모 엔티티의 리스트에서 요소를 삭제하기만 해도 해당 에 해당하는 자식 엔티티가 delete되는 기능까지 포함하고 있다고 이해하시면 됩니다.

즉, orphanRemoval=true 는 리스트 요소로써의 영속성 전이도 해준다는 뜻

  • 부모 엔티티 삭제
    • CascadeType.REMOVE와 orphanRemoval = true 옵션 모두 부모 엔티티를 삭제하면, 자식 엔티티도 삭제된다.
  • 부모 엔티티와 자식 엔티티 사이의 연관관계 제거
    • CascadeType.REMOVE 옵션은 자식 엔티티가 DB에 삭제되지 않고 남아있으며, 외래키 값만 변경된다.
    • orphanRemoval = true 옵션은 자식 엔티티가 고아 객체로 취급되어 DB에서 삭제된다.
  • 부모 엔티티와 자식 엔티티 사이의 연관관계 변경
    • CascadeType.REMOVE와 orphanRemoval = true 옵션 모두자식 엔티티가 DB에 삭제되지 않고 남아있으며, 외래키 값만 변경된다.

✅ 영속성 전이 최강 조합 : orphanRemoval=true + Cascade.ALL

위 2개를 함께 설정하면 자식 엔티티의 라이프 사이클이 부모 엔티티와 동일해지며, 직접 자식 엔티티의 생명주기를 관리할 수 있게 되므로 자식 엔티티의 Repository 조차 없어도 된다. (따라서, 매핑 테이블에서 많이 쓰임)

현업에서 주로 사용하는 Fetch 전략

사용 위치

  • Entity 에 FetchType 으로 설정할 수 있다.
    • `@ElementCollection`, `@ManyToMany`, `@OneToMany`, `@ManyToOne`, `@OneToOne`
  • Query 수행시 fetch Join 을 통해서 LAZY 인 경우도 즉시 불러올 수 있다.

사용법

  • 기본 LAZY를 설정한 뒤에 필요할때만 fetch Join 을 수행한다.
  • 항상 같이 쓰이는 연관관계 일 경우만 EAGER 를 설정한다.

옵션(FetchType)

  • EAGER : 즉시 로딩 (부모 조회 시 자식도 같이 조회)
  • LAZY : 지연 로딩 (자식은 필요할때 따로 조회)

실무에서는 기본적으로 지연 로딩을 사용하며, N+1 문제가 발생한다면 JPQL의 fetch join 등을 활용하여 해결한다.

사실상 실무에서는 연관관계를 잘 사용하지 않는다고 한다. 굳이 사용한다고 하면 다대일 관계, 일대일 관계 정도?