JPA를 사용하면 EntityManager.find()로 단건 조회를 하거나, getTeam().getMembers() 같은 객체 그래프 탐색으로 연관 엔티티를 가져올 수 있다.
하지만 실무에서는 이것만으로 부족하다.
18세 이상의 회원만 조회를 하거나, 특정 조건을 만족하는 주문 목록 같은 복잡한 검색 조건이 필요한 경우가 대부분이기 때문이다.
실제로 실무에서 검색 탭만 10개가 넘은 적이 있었다.
모든 데이터를 메모리로 가져와서 필터링 하는 것은 불가능하다.
성능 문제도 있고, 애플리케이션이 메모리에 올릴 수 있는 데이터 양도 한계가 있다.
결국 데이터베이스 레벨에서 필터링이 필요하다. 그래서 JPA는 여러 쿼리 방법을 제공한다.
JPA는 다음 과 같은 쿼리 방법을을 제공한다.
실무에서는 대부분 JPQL이나 QueryDSL로 해결되고, 가끔 해결이 되지 않는 케이스에서 네이티브 SQL이나 다른 기술을 같이 사용한다.
JPA가 100% 문제를 해결하기 위해 만들어진 건 아니기 때문이다.
JPQL은 JPA가 제공하는 객체 지향 쿼리 언어이다.
SQL과 문법이 유사하지만 (SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 등 ANSI 표준 지원) 가장 큰 차이는 대상이 테이블이 아닌 엔티티 객체라는 점이다.
// 18세 이상 회원 검색
String jpql = "select m from Member m where m.age > 18";
List<Member> result = em.createQuery(jpql, Member.class)
.getResultList();
여기서 Member는 테이블이 아니라 엔티티 클래스를 가리킨다.
JPA가 이 JPQL을 각 데이터베이스에 맞는 SQL로 반환해준다.
위의 JPQL이 실제로 어떤 SQL로 변환되는지 보자
select
m.id as id,
m.age as age,
m.USERNAME as USERNAME,
m.TEAM_ID as TEAM_ID
from
Member m
where
m.age > 18
JPQL은 데이터베이스에 독립적이다.
MySQL을 쓰든 Oracl을 쓰든 JPA가 알아서 해당 데이터베이스의 SQL로 변환해준다.
JPQL의 가장 큰 문제는 단순 문자열이라는 점이다. 복잡한 동적 쿼리를 만들 때 문자열을 이어붙이다 보면 띄어쓰기를 빼먹거나, 오타가 나도 컴파일 시점에 잡을 수 없다.
// Criteria 사용 예시
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
// 루트 클래스 (조회를 시작할 클래스)
Root<Member> m = query.from(Member.class);
// 쿼리 생성
CriteriaQuery<Member> cq = query.select(m)
.where(cb.equal(m.get("username"), "kim"));
List<Member> resultList = em.createQuery(cq).getResultList();
하지만 실무에서는 거의 안 쓴다.코드가 너무 복잡하고 직관적이지 않아서 실용성이 떨어진다. 대신 QueryDSL을 사용한다.
QueryDSL은 Criteria의 장점은 살리면서 코드 가독성을 크게 개선한 오픈소스 라이브러리다.
// JPQL: select m from Member m where m.age > 18
JPAQueryFactory query = new JPAQueryFactory(em);
QMember m = QMember.member;
List<Member> list = query.selectFrom(m)
.where(m.age.gt(18))
.orderBy(m.name.desc())
.fetch();
코드만 봐도 SQL과 거의 똑같다. 메서드 체이닝 방식이라 읽기도 쉽다.
JPQL을 잘 이해하면 QueryDSL은 금방 배울 수 있다.
QueryDSL 공식 사이트에서 매뉴얼을 참고하면 된다.
실무에서는 JPQL + QueryDSL 조합을 권장한다.
QueryDSL로 작성하면 오타가 거의 생기지 않고, IDE의 자동완성 지원도 받을 수 있어서 생산성이 훨씬 높다.
JPQL로 해결할 수 없는 특정 데이터베이스의 고유 기능을 사용해야 할 때가 있다.
Oracle의 CONNECT BY (계층 쿼리), 특정 DB의 SQL 힌트 같은 경우다.
String sql = "SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = 'kim'";
List<Member> resultList = em.createNativeQuery(sql, Member.class)
.getResultList();
Native SQL은 순수 SQL을 그대로 사용하는 것이기 때문에 데이터베이스에 종속된다. 꼭 필요한 경우에만 사용하는 게 좋다.
JPA를 사용하면서 JDBC 커넥션을 직접 쓰거나, Spring JdbcTemplate, MyBatis 같은 기술을 함께 사용할 수도 있다. 하지만 반드시 영속성 컨텍스트를 수동으로 flush 해야 한다.
영속성 컨텍스트는 보통 트랜잭션 커밋 시점이나 JPA 쿼리 실행 전에 자동으로 flush된다.
하지만 JDBC, MyBatis 같은 기술은 JPA와 무관하게 동작한다.
코드로 예시를 확인 해 보자
// 1. JPA로 Member 저장 (아직 DB에 반영 안됨, 영속성 컨텍스트에만 존재)
Member member = new Member("kim", 20);
em.persist(member);
// 2. JDBC로 직접 쿼리 실행
Connection conn = dataSource.getConnection();
String sql = "SELECT * FROM MEMBER WHERE NAME = 'kim'";
ResultSet rs = conn.executeQuery(sql); // Member를 찾을 수 없음!
위 코드에서 Member는 아직 DB에 INSERT되지 않았기 때문에 JDBC 쿼리로는 조회되지 않는다.
이럴 때는 JPA를 우회하는 쿼리를 실행하기 직전에 em.flush()를 호출해야 한다.
Member member = new Member("kim", 20);
em.persist(member);
// JPA 우회 쿼리 실행 전 수동 flush
em.flush();
// 이제 JDBC 쿼리로도 조회 가능
Connection conn = dataSource.getConnection();
ResultSet rs = conn.executeQuery("SELECT * FROM MEMBER WHERE NAME = 'kim'");
JPA와 다른 기술을 혼용할 때는 영속성 컨텍스트의 변경 사항이 DB에 반영되도록
flush()를 명시적으로 호출해야 한다.
JPQL의 기본을 탄탄히 익혀두면, QueryDSL은 그 위에 얹는 도구일 뿐이라 금방 익힐 수 있다. 실무에서는 거의 QueryDSL을 사용하기 때문에 두 가지를 모두 학습해두는 게 좋다.