본문 바로가기
Framework and Tool/JPA

JPA - 객체지향 쿼리 언어 - 개요

by ocwokocw 2021. 7. 28.

- 참조: 자바 ORM 표준 JPA 프로그래밍

- Query DSL 공식 홈페이지: https://querydsl.com/

- Query DSL Release note: https://querydsl.com/releases

- Query DSL 환경설정: https://www.baeldung.com/intro-to-querydsl

- 개요

JPA 는 엔티티 객체를 조회할 수 있는 다양한 쿼리기술을 지원한다. JPQL, Criteria, QueryDSL 등 JPA 를 배우지 못하다가 이렇게 책으로라도 배워야겠다고 마음먹은 사람들이라면 한번쯤 들어봤을 법한 단어들이 바로 엔티티 객체를 조회하는 쿼리기술들이다.

위와 같이 화려한 단어들 중 가장 중요한것은 JPQL 이다. Criteria 나 QueryDSL 도 결국 JPQL 을 편하게 사용하도록 도와주는 도구이기 때문에 결국 JPQL 을 알아야 한다.

여태까지 식별자를 조회할 때 엔티티 매니저의 find() 메소드를 이용하여 조회했다. 하지만 실전에서는 '오늘 일자에 생산된 모든 물품을 조회하라' 와 같은 요구사항이 있을 때 모든 엔티티를 메모리에 올려두고 날짜를 비교할 수 없으므로 엔티티를 검색하는 방법이 필요하다.

JPQL 은 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리이기 때문에 특정 SQL 에 종속되지 않는다. 이말은 곧 Project 의 데이터베이스 제품이 갑자기 변경되었을 때 JPQL 을 사용했다면 방언(Dialect)만 변경하면 된다는 얘기이다.

JPQL 은 SQL 과 문법이 상당히 유사해서 SQL 에 익숙하다면 사용하는데 크게 어려움이 없을것이다. 객체지향 쿼리는 JPA 의 공식 지원 여부를 기준으로 아래와 같이 나눌 수 있다.

  • 공식 지원: JPQL, Criteria Query, Native SQL
  • 비공식 지원: QueryDSL, JDBC 직접 사용, SQL 매퍼(Mybatis) 프레임워크 사용

- JPQL

JPQL 은 SQL 이 아닌 엔티티를 조회하는 쿼리이기 때문에 문법은 유사하지만 특정 데이터베이스에 의존하지 않는다. JPQL 을 실행하면 JPA 가 자동으로 SQL 로 변경하기 때문에 데이터베이스가 변경된다면 방언만 바꾸면 된다.

 

public static void save(EntityManager em) throws CloneNotSupportedException {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();
	
	Member member1 = new Member();
	member1.setName("abcd");
	em.persist(member1);
	
	Member member2 = new Member();
	member2.setName("efg");
	em.persist(member2);
	
	tx.commit();
	em.close();
}

 

만약 위와 같이 abcd 회원과 efg 회원을 영속화시켰다고 가정해보자. 이때 JPQL 을 이용해 이름이 efg 인 회원을 조회하고 싶다면 아래와 같이 사용하면 된다.

 

public static void find(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();
	
	String jpql = "select m from Member m where m.name = 'efg'";
	List<Member> members = em.createQuery(jpql, Member.class)
		.getResultList();
	
	members.forEach(System.out::println);

	tx.commit();
	em.close();
}

 

SQL 과 문법이 정말 비슷하지만 몇 가지 차이점이 존재한다. 우선 from 뒤에는 테이블이 아닌 엔티티 이름(Member) 를 적어 줘야 한다. 또한 where 절에서 m.name 에는 컬럼명이 아닌 엔티티의 속성명을 사용해야 한다. 위의 JPQL 을 실행하면 실제 수행되는 SQL 은 아래와 같다.

 

Hibernate: 
    /* select
        m 
    from
        Member m 
    where
        m.name = 'efg' */ select
            member0_.member_id as member_i1_4_,
            member0_.insert_datetime as insert_d2_4_,
            member0_.update_datetime as update_d3_4_,
            member0_.city as city4_4_,
            member0_.street as street5_4_,
            member0_.zipcode as zipcode6_4_,
            member0_.name as name7_4_ 
        from
            member member0_ 
        where
            member0_.name='efg'

 

별칭이 복잡하긴 하지만 우리가 잘 알고 있는 SQL 임에 틀림없다.


- Criteria 쿼리

Criteria 는 JPQL 을 생성하는 빌더 클래스이다. JPQL 의 경우 잘못 작성하였을 때 실행시점에 되어야 알 수 있지만 Criteria 의 경우 컴파일 시점에 오류를 발견할 수 있다.

 

위에서 efg 회원을 조회한 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("name"), "efg"));
List<Member> members = em.createQuery(cq).getResultList();

members.forEach(System.out::println);

 

m.get("name") 부분도 문자열로 작성하다보니 오타가 있을 수 있는데 메타 모델을 이용하면 코드로 변환할 수 있다. 자바의 어노테이션 프로세서를 이용하면 어노테이션을 분석해 클래스를 생성한다. JPA 는 이를 이용해서 Member_ 라는 Criteria 전용 클래스를 생성한다. 이 기능을 이용하면 m.get("name") 으로 작성한 코드를 m.get(Member_.name) 과 같이 작성할 수 있다.

 

Criteria 는 장점도 있지만 코드를 보고 느낄 수 있듯이 단순히 조회 한번 하는데 꽤나 장황한 코드를 작성해야 한다.


- QueryDSL

Criteria 처럼 JPQL 빌더 역할을 한다. Criteria 와 같은 코드기반 이지만 JPQL 과 구조도 비슷하여 Criteria 보다 파악이 쉽다.

 

QueryDSL 은 JPA 표준은 아니고 오픈소스이다. JPA 뿐만 아니라 JDO, 몽고DB 등을 다른 모듈도 지원하는데 https://querydsl.com/ 에서 확인할 수 있다.

 

우선 pom.xml 에 dependency 를 추가해줘야 한다.

 

<properties>
	<java.version>1.8</java.version>
	<querydsl.version>4.1.3</querydsl.version>
</properties>

......

<dependency>
  <groupId>com.querydsl</groupId>
  <artifactId>querydsl-apt</artifactId>
  <version>${querydsl.version}</version>
  <scope>provided</scope>
</dependency>

<dependency>
  <groupId>com.querydsl</groupId>
  <artifactId>querydsl-jpa</artifactId>
  <version>${querydsl.version}</version>
</dependency>

..............

<plugin>
    <groupId>com.mysema.maven</groupId>
    <artifactId>apt-maven-plugin</artifactId>
    <version>1.1.3</version>
    <executions>
        <execution>
            <goals>
                <goal>process</goal>
            </goals>
            <configuration>
                <outputDirectory>target/generated-sources/java</outputDirectory>
                <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
            </configuration>
        </execution>
    </executions>
</plugin>

 

querydsl-jpa 는 말그대로 querydsl 자체를 이용하기 위한 라이브러리이며, querydsl-apt 는 querydsl 관련하여 자바 소스파일의 어노테이션을 처리할 수 있는 annotation processing tool(APT) 이다. 이 tool 은 @Entity 어노테이션이 붙은 클래스들마다 Criteria 의 메타모델 처럼 Q-type 이라고 불리는 소스들을 생성해낸다.

mvn compile 을 수행하면 outputDirectory 에 소스가 생성된다.

위와 같이 생성된 폴더 경로를 빌드 패스로 잡아줘야 한다.

이제 QueryDSL 을 이용하여 efg 회원을 조회해보자.

 

JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QMember member = QMember.member;

List<Member> members = queryFactory.select(member)
		.from(member)
		.where(member.name.eq("efg"))
		.fetch();

 

책에서는 JPAQuery 와 .list() 메소드를 이용하지만 .list() 메소드가 .fetch() 로 이름이 변경되었고, JPAQueryFactory 를 이용한다. 자세한 내용은 글의 첫머리의 QueryDSL Release note 를 참조하길 바란다. 문법이 Criteria 에 비해서 훨씬 SQL 구조와 비슷하고 직관적이다. 


- Native SQL

Native SQL 이란 말 그대로 SQL 을 사용하는 기능을 제공하겠다는 것이다. 가끔 Oracle의 CONNECT BY 같이 JPQL 로 표현할 수 없는 기능을 사용해야 하는 경우가 있다.

 

어쩔 수 없이 사용해야할 때가 있지만 데이터베이스 제품군을 변경할 때 SQL 이 변경될 수도 있다는 점을 명확히 인지하고 사용해야 한다.

 

String sql = "select * from member where name='efg'";

List<Member> members = em.createNativeQuery(sql, Member.class)
		.getResultList();

members.forEach(System.out::println);

 

사용법은 JPQL 과 비슷하지만 문법이 SQL 이라는점이 다르다.


- JDBC 직접 사용 및 SQL 매퍼 프레임워크 사용

거의 사용할 일이 없지만 JDBC 커넥션에 직접 접근해야 한다면 JPA 구현체가 제공하는 방법을 사용해야 한다. JDBC 및 Mybatis 를 JPA 와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 flush 해야 한다. 이렇게 하지 않으면 어떤 트랜잭션 안에서 영속성 컨텍스트에서 값을 변경하고 플러시하기 전에 Mybatis 로 조회하면 수정 전 값이 나오는 사태가 벌어진다.

 

위의 이슈를 해결하기 위해서는 JPA 를 사용하지 않고 SQL 을 실행하기 직전 영속성 컨텍스트를 강제로 flush 해야 한다. 스프링을 이용하면 JPA 와 Mybatis 를 쉽게 통합할 수 있으며 AOP 을 활용해서 JPA 를 사용하지 않고 SQL 을 실행하는 메소드를 수행할 때 마다 flush 하여 문제를 해결할 수 있다.

댓글