본문 바로가기

IT공부/JPA

JPA 8강 - Spring Data JPA와 QueryDSL 이해

JPA 8강 - Spring Data JPA와 QueryDSL 이해

JPA 기반 프로젝트

  • Spring Data JPA
  • QueryDSL

JPA와 스프링과 어떻게 얼개가 맞춰지는지를 알아보자. 보통 인터넷에 떠도는 예제를 통해 JPA를 배우면 영속성 컨텍스트, 매핑이론들에 대해서 못 배운다. 그런데 소스는 짧다. 그러면 드는 생각이 Spring JPA를 쓰면 다되나보다 생각을 한다. 그러고 현업에 들어가면 망한다. 매핑이 안된다던지, null값이 들어온다던지 하는 문제가 생길 것이다. 

Spring Data JPA

반복되는 CRUD

public class MemberRepository {

 

   public void save (Member member) { ... }

   public Member findOne(Long id) {...}

   public List<Member> findAll() { ... }

 

   public Member findByUsername (String username) { ... }

}

 

public class ItemRepository {

 

   public void save (Item item) { ... }

   public Member findOne(Long id) {...}

   public List<Member> findAll() { ... }

 

}

 

Repository(DAO)를 만든다고 하면 형식이 거의 비슷하다(CRUD가 거진 비슷하다. ).공통점이 있다는 뜻이다. JPA를 통해 많은 부분을 자동화했는데도 불구하고 DAO를 만들 때보니 비슷한 것들이 계속 생긴다. 그래서 이 것을 자동화한다. 

스프링 데이터 JPA 소개

  • 지루하게 반복되는 CRUD 문제를 세련된 방법으로 해결
  • 개발자는 인터페이스만 작성
  • 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입

스프링 데이터 JPA 적용전

public class MemberRepository {

 

   public void save (Member member) { ... }

   public Member findOne(Long id) {...}

   public List<Member> findAll() { ... }

 

   public Member findByUsername (String username) { ... }

}

 

public class ItemRepository {

 

   public void save (Item item) { ... }

   public Member findOne(Long id) {...}

   public List<Member> findAll() { ... }

 

}

 

위 코드를 보면 save, findOne, findAll과 같이 공통적인 메서드들인 보인다. save 메서드 안에 어떤 코드들이 들어갈까를 생각해보면 em.persist() 정도가 들어갈 것이고, findOne은 em.find(Member.class, memberId) 코드정도가 들어갈 것이다. 

스프링 데이터 JPA 적용 후 

public interface MemberRepository extends JpaRepository<Member, Long> {

   Member findByUsername(String username); //공통으로 할 수 없는 부분

}

 

public interface ItemRepository extends JpaRepository<Item, Long> {

   //비어있음

}

 

스프링 JPA에서는 JpaRepository라는 인터페이스를 제공한다. 

스프링 데이터 JPA 적용 후 클래스 다이어그램

JpaRepository 인터페이스에서 많은 공통메서드를 제공한다. 각 레포지토리에서 JpaRepository를 상속만하고 실제로 사용할때는 인터페이스를 호출하여 사용할 수 있다. 스프링이 어플리케이션 로딩 시점에 MemberRepository 구현체와 ItemRepository 구현체를 만들어서 상속관계를 만든다. 개발자는 ItemRepository 인터페이스에 인젝션만 받아서 쓰면 된다. 

스프링 데이터 JPA가 구현 클래스 생성

공통 인터페이스 기능

  • JpaRepository 인터페이스 : 공통 CRUD 제공
  • 제네릭은 <엔티티, 식별자>로 설정

public interface MemberRepository extends JpaRepository<Member, Long> {

   //비어있음

}

스프링은 스프링 데이타라는 프로젝트가 있고 스프링 데이타 JPA라는 프로젝트가 있다. 스프링 데이타 몽고 등등 DB CRUD 공통 인터페이스인 스프링 데이타가 있고 스프링 데이타 JPA에 특화된 기능들이 스프링 데이타 JPA에 있다고 보면 된다. 

쿼리 메서드 기능

  • 메서드 이름으로 쿼리 생성
  • @Query 어노테이션으로 쿼리 직접 정의 

메서드 이름으로 쿼리 생성

  • 메서드 이름만으로 JPQL 쿼리 생성

public interface MemberRepository extends JpaRepository<Member, Long>  {

 

   List<Member> findByName(String username);

}

 

Repository를 공통화하는데 그 중에서 공통화를 못하는 쿼리가 있을 수 있다. 위에서 보듯이 findByName 메서드이다. JPA는 findByName 메서드 명을 보고 JPQL을 짜서 쿼리를 날려준다. 즉 개발자가 JPQL을 안짜도 된다는 말이다. 

메서드 이름으로 쿼리 생성 - 사용코드

List<Member> member = memberRepository.findByName("hello")

 

실행된 SQL

SELECT * FROM MEMBER M WHERE M.NAME = 'hello'

이름으로 검색 + 정렬

public interface MemberRepository extends JpaRepository<Member, Long> {

  

   List<Member> findByName(String username, Sort sort);

}

 

실행된 SQL

SELECT * FROM MEMBER M WHERE M.NAME = 'hello' ORDER BY AGE DESC

 

정렬을 하고 싶다면 인터페이스 메서드 매개변수로 Sort sort를 넣어주면 된다. 

이름으로 검색 + 정렬 + 페이징

public interface MemberRepository extends JpaRepository<Member, Long> {

 

   List<Member> findByName(String username, Pageable pageable);

}

 

실행된 SQL 2가지

SELECT * //데이터 조회

FROM

  (SELECT ROW_.*, ROWNUM ROWNUM_

   FROM 

      (SELECT M.*

       FROM MEMBER M WHERE M.NAME = 'hello'

       ORDER BY M.NAME

       ) ROW_

    WHERE ROWNUM <= ?

   )

WHERE ROWNUM_ > ?

 

//전체 수 조회

SELECT COUNT(1)

FROM MEMBER M WHERE M.NAME = 'hello'

 

페이징 처리를 하고 싶다면 findByName 매개변수로 Pageable pageable을 매개변수로 정의하면 된다. 

이름으로 검색 + 정렬 + 페이징, 사용코드

Pageable page = new PageRequest(1, 20, new Sort ...);

Page<Member> result = memberRepository.findByName("hello", page);

 

int total = result.getTotalElements();     //전체 수

List<Member> members = result.getContent(); //데이터

 

전체 페이지 수, 다음 페이지 및 페이징을 위한 API가 다 구현되어 있음. 

@Query, JPQL 정의

  • @Query를 사용해서 직접 JPQL 지정

public interface MemberRepository extends JpaRepository<Member, Long> {

 

   @Query("select m from Member m where m.username = ?1")

   Member findByUsername(String username, Pageable pageable);

}

메서드 이름으로 JPQL을 자동으로 생성하는 기능은 메서드이름에 And를 추가하여 조건을 추가할 수 있는데 그 조건이 많아지면 메서드 이름도 끊임없이 길어진다. 이 때 메서드이름으로 쿼리를 만들지 않고 직접 쿼리를 작성할 수도 있다. 위 코드와 같이 메서드 이름 위에 @Query 어노테이션을 붙이고 괄호 안에 JPQL을 작성하면 된다. 어플리케이션 로딩중 오타가 있다면 오류를 출력한다. 

반환 타입

List<Member> findByName (String name); //컬렉션

Member findByEmail(String email); //단건

 

반환타입도 지정할 수 있다. 

Web 페이징과 정렬 기능

  • 컨트롤러에서 페이징 처리 객체를 바로 받을 수 있음
  • page : 현재 페이지
  • size : 한 페이지에 노출할 데이터 건수
  • sort : 정렬조건

/members?page=0&size=20&sort=name,desc

 

@RequestMapping(value ="/members", method = RequestMethod.GET)

String list(Pageable pageable, Model model) { }

Web 도메인 클래스 컨버터 기능

  • 컨트롤러에서 식별자로 도메인 클래스 찾음

/members/100

 

@RequestMapping("/members/{memberId}")

Member member(@PathVariable("memberId") Member member) {

    return member;

}

PK값으로 memberId 식별자를 경로로 받으면 식별자로 Member 객체를 매개변수로 받을 수 있다. 사실 잘 쓰진 않는다.


QueryDSL

QueryDSL 소개

  • SQL, JPQL을 코드로 작성할 수 있도록 도와주는 빌더 API
  • JPA 크리테리아에 비해서 편리하고 실용적임
  • 오픈소스 

SQL, JPQL의 문제점

  • SQL, JPQL은 문자, Type-check 불가능
  • 해당 로직 실행 전까지 작동여부 확인 불가

QueryDSL 장점

  • 문자가 아닌 코드로 작성
  • 컴파일 시점에 문법 오류 발견
  • 코드 자동완성(IDE 도움)
  • 단순하고 쉬움 : 코드 모양이 JPQL과 거의 비슷
  • 동적 쿼리

QueryDSL - 동작원리 쿼리타입 생성

Member.java 파일을 가지고 QMember.java라고 하는 QueryDSL 전용 파일을 하나 생성한다. 

QueryDSL 사용

//JPQL

//select m from Member m where m.age > 18

 

JPAFactoryQuery query = new JPAQueryFactory(em); //엔티티 매니저를 매개변수로 넘겨준다. 

QMember m = QMember.member;

 

List<Member> list = 

  query.selectFrom(m)

         .where(m.age.gt(18))

         .orderBy(m.name.desc())

         .fetch();

 

위 코드를 이클립스에서 작성 시 m.namee 같이 필드명을 잘못적으면 컴파일 에러가 난다. 

QueryDSL 조인

JPAQueryFactory query = new JPAQueryFactory(em);

QMember m = QMember.member;

QTeam  t = QTeam.team;

 

List<Member> list = 

  query.selectFrom(m)

         .join(m.team, t)

         .where(t.name.eq("teamA")

         .fetch();

QueryDSL 동적쿼리

String name = "member";

int age = 9;

 

QMember m = QMember.member;

 

BooleanBuilder builder = new BooleanBuilder();

if(name != null) {

  builder.and(m.name.contains(name));

}

if(age != null) {

  builder.and(m.age.gt(age));

}

 

List<Member> list = 

     query.selectFrom(m)

            .where(builder)

            .fetch();

사실 QueryDSL을 쓰는 것은 동적쿼리때문에 쓴다고 보면 된다. QueryDSL 장점으로는 코드이기 때문에 원하는 필드만 DTO로 바꿀 수 있다. 

QueryDSL은 자바다!

예를 들어 쿠폰을 개발한다고 가정하자. 현재 쿠폰은 라이브 상태이다. 쿠폰의 마케팅개수(발행한 전체개수)가 넘지 않아야 쓸 수 있다. 항상 비슷한 쿼리가 많다. 

위 코드처럼 QueryDSL은 자바코드이기 때문에 메서드로 지정하여 제약조건을 공통메서드로 만들어 쓸 수 있다. 

실무경험

  • 테이블 중심에서 객체 중심으로 개발 패러다임이 변화
  • 유연한 데이터베이스 변경의 장점과 테스트
  • Junit 통합 테스트 시에 H2 DB 메모리 로드
  • 로컬 PC에는 H2 DB 서버모드로 실행(xml에서 방언만 바꾸면 된다.)
  • 개발 운영은 MySQL, Oracle (xml에서 방언만 바꾸면 된다.)
  • 데이터 베이스 변경 경험(개발 도중 MySQL -> Oracle 바뀐 적도 있다.)
  • 테스트, 통합 테스트 시에 CRUD는 믿고 간다. (JPA가 자동으로 쿼리를 작성하기 때문, 핵심 비지니스 로직 테스트를 더 테스트 할 수 있다.)
  • 빠른 오류 발견
  • 컴파일 시점!
  • 늦어도 애플리케이션 로딩 시점
  • (최소한 쿼리 문법 실수나 오류는 거의 발생하지 않는다.)
  • 대부분 비지니스 로직 오류

  성능

  • JPA 자체로 인한 성능 저하 이슈는 거의 없음
  • 성능 이슈 대부분은 JPA를 잘 이해하지 못해서 발생
  • 즉시 로딩 : 쿼리가 튐 -> 지연로딩으로 변경
  • N + 1 문제 -> 대부분 페치 조인으로 해결
  • 내부 파서 문제 : 2000줄 짜리 동적 쿼리 생성 1초
  • 정적 쿼리로 변경(하이버네이트는 파싱된 결과 재사용)

성능 테스트는 일단 빠르게 개발 후 실제 성능 테스트를 돌려보고 병목이 되는 구간들을 찾아서 그 부분들만 설정변경으로 코드를 수정하고 빠르게 다시 테스트를 할 수 있어야 한다. 

'IT공부 > JPA' 카테고리의 다른 글

JPA 7강 - JPA 객체지향쿼리  (0) 2020.05.22
JPA 6강 - JPA 내부구조  (0) 2020.05.22
JPA 5강 - 양방향 매핑  (0) 2020.05.21
JPA 4강 - 연관관계 매핑  (0) 2020.05.21
JPA 3강 - 필드와 컬럼 매핑  (0) 2020.05.21