JPA

  • 복잡한 검색 조건을 사용해서 엔티티 객체를 조회할 수 있는 다양한 쿼리 기술을 지원한다.
  • 애플리케이션과 JDBC 사이에서 동작한다.
  • JPA 구동 방식 : Persistence(설정 정보를 조회) -> EntityManagerFactory(생성) -> EntityManager(생성)
  • 앤티티 매니저 팩토리는 하나만 생성해서 애플리케이션 전체에서 공유된다.
  • 엔티티 매니저는 스레드 간에 공유가 되지 않는다.
  • JPA의 모든 데이터 변경은 트랜잭션 안에서 실행된다.
그래서 왜 쓰는 걸까?
  • 유사한 CRUD SQL 반복 작업에 많은 시간을 사용했다.
  • 객체를 단순히 데이터 전달 목적으로 사용할 뿐, 객체 지향적이지 못함
  • 그래서 객체와 테이블을 매핑시켜주는 ORM이 주목받기 시작했고, 자바 진영에서는 JPA라는 표준 스펙이 정의되었다.
  • JPA를 구현한 여러 프레임워크가 있지만 Hibernate가 JPA를 주도하고 있기 때문에 JPA를 Hibernate라 생각하면 편하지 않을까 싶다.
  • Hibernate는 SQL을 직접 사용하지 않고, 메소드 호출만으로 쿼리가 수행된다.
  • 즉, SQL 반복 작업을 하지 않으므로 생산성이 매우 높아진다.
  • 그래도 SQL은 알아야 한다.
  • 그 이유는 수행한 쿼리를 보면서 의도한 대로 쿼리가 짜였는지, 성능이 어떠한지에 대한 모니터링이 필요하기 때문에 SQL은 반드시 알아야 한다.

하지만 JPA에서 제공하는 메소드 호출만으로 섬세한 쿼리 작성이 어렵다.

 

그 결과, JPQL이 탄생했다.

 

그래서 보통 단순한 기능은 JPA를 사용하고, 좀 복잡한 쿼리가 있을 때 JPQL이나 Querydsl 둘 중 하나를 사용하게 된다.

 

JPQL(Java Persistence Query Language)

  • SQL과 비슷한 문법을 가진 객체 지향 쿼리이다.
  • 테이블 검색이 아니라 객체를 검색하는 객체지향 쿼리
  • JPA는 JPQL을 분석해서 SQL을 자동으로 생성해서 DB에서 쿼리를 만들어준다.

JPQL 과정

  1. JPQL을 실행하면 영속성 컨텍스트로 요청을 보낸다.
  2. 영속성 컨텍스트는 1차 캐시에 엔티티 존재 여부 상관없이 DB에 질의한다.
  3. DB 질의를 통해 조회된 데이터는 영속성 컨텍스트가 다시 받는다.
  4. 데이터를 전달받은 영속성 컨텍스트는 엔티티를 초기화하고 캐시에 저장한다.
  5. 캐시에 저장한 후 엔티티를 반환한다.
//JPA : EntityManager 객체의 find() 메소드를 호출할 수 있다.

em.find(Item.class, id);

--------------------------------------------------------
//JPQL : createQuery를 통해 사용할 수 있다.

em.createQuery(
    "select i from Item i where i.id = :id", Item.class)
    .getResultList();

--------------------------------------------------------
//파라미터 바인딩(이름, 순서 중에서 이름 사용 추천)

em.createQuery(
    "select i from Item i where i.id = :id", Item.class)
    .setParameter("id", itemId)
    .getResultList();

--------------------------------------------------------
//페이징 API

em.createQuery(...)
	.setFirstResult(5) //5부터 시작
    .setMaxResults(20) //20개 출력
    .getResultList();

 

JPQL의 형태를 보면 SQL과 비슷하지만 다른 점이 있다. 

  • 대소문자 구분
    • 엔티티와 속성은 대소문자를 구분한다.
    • 엔티티 이름과 속성은 대소문자를 구분한다.
    • JPQL 키워드는 대소문자 구분 없이 사용 가능하다.(SELECT == select)
  • 엔티티명
    • FROM 절 뒤에 사용하는 것은 클래스명이 아니라 엔티티명이다.
    • @Entity(name=xx) 혹은 클래스명과 동일하면 생략 가능
  • 별칭
    • SQL과 다르게 JPQL에서는 별칭이 필수적으로 명시해야 한다.
    • 생략도 가능하다.
    • from 절 뒤에 해당 엔티티 + 별칭 형태로 사용한다.
  • 파라미터 바인딩
    • 클래스에 정의된 프로퍼티 앞에  :  를 사용한다.
    • LIKE와 같은 형태를 사용할 때 주의할 점
      • JPQL에 직접 문자를 쓰면 SQL Injection을 당할 수 있다.
      • JPA에서 파라미터만 다를 뿐 같은 쿼리로 인식하므로, JPQL을 SQL로 파싱한 결과를 재사용할 수 있다.
  • 페이징 API
    • setFirstResult(int startPosition) : 조회 시작 위치
    • setMaxResults(int maxResult) : 조회할 데이터 수
  • DTO 사용
    • 데이터를 전송하기 위해서 엔티티 자체를 쓸 수 있지만 엔티티 자체를 사용하는 것은 바람직하지 않다.
    • DTO를 사용하면 약간의 불편한 점이 있다.
    • new 뒤에 해당하는 패키지명을 모두 작성해야 한다.
    • 그래서 Querydsl을 사용하면 조금 더 편하게 사용할 수 있다.
  • 조인
    • INNER JOIN : INNER은 생략 가능
    • LEFT OUTER JOIN : OUTER 생략 가능
    • FETCH JOIN
      • JPQL에서 성능 최적화를 위해 제공하는 기능
      • 연관된 엔티티나 컬렉션을 한 번에 같이 조회(JPQL은 결과를 반환할 때 연관까지 고려하지 않음)
      • SQL 호출 횟수를 줄여 성능 최적화
      • 쿼리 시점에 조회하므로 지연 로딩이 발생하지 않음
      • N + 1 문제를 해결하는 데 주로 사용되는 방법이다.
//INNER JOIN
em.createQuery(
    "select o From Order o join o.member m", Order.class)
    .getResultList();

// Order + Member 한 번에 출력

----------------------------------------------------------------
//LEFT JOIN
em.createQuery(
    "SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'", Item.class)
    .getResultList();

// 팀 이름이 A인 팀만 조인해서 출력

----------------------------------------------------------------
//FETCH JOIN

em.createQuery(
    "select o from Order o" +
    " join fetch o.member m" +
    " join fetch o.delivery d", Order.class)
    .getResultList();

// Order + Member + Delivery 한 번에 출력