객체지향 쿼리 언어 소개

2023. 9. 22. 16:12JPA

반응형

JPA는 복잡한 검색 조건을 사용해서 엔티티 객체를 조회할 수 있는 다양한 쿼리 기술을 지원한다. 

EntityManager.find() 메소드를 사용하면 식별자로 엔티티 하나를 조회할 수 있다. 이렇게 조회한 엔티티에 객체 그래프 탐색을 사용하면 연관된 엔티티들을 찾을 수 있다. 이 둘은 가장 단순한 검색 방법이다.

  • 식별자로 조회 EntityManager.find()
  • 객체 그래프 탐색 a.getB().getC()

이 기능만으로 애플리케이션을 개발하기는 어렵다. 예를 들어 나이가 24살 이상인 회원을 모두 검색하고 싶다면 좀 더 현실적이고 복잡한 검색 방법이 필요하다. 결국 데이터는 데이터베이스에 있으므로 SQL로 필요한 내용을 최대한 걸러서 조회해야 한다. 하지만 ORM을 사용하면 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발하므로 검색도 테이블이 아닌 엔티티 객체를 대상으로 하는 방법이 필요하다. JPQL은 이런 문제를 해결하기 위해 만들어졌는데 다음과 같은 특징이 있다.

  • 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리다.
  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.

JPQL을 사용하면 JPA는 이 JPQL을 분석한 다음 적절한 SQL을 만들어 데이터베이스를 조회한다. 그리고 조회한 결과로 엔티티 객체를 생성해서 반환한다.

JPA는 JPQL 뿐만 아니라 다양한 검색 방법을 제공한다. 다음은 JPA가 공식 지원하는 기능이다.

  • JPQL
  • Criteria Query: JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
  • 네이티브 SQL: JPA에서 JPQL 대신 직접 SQL을 사용할 수 있다.

다음은 JPA가 공식 지원하는 기능은 아니지만 알아둘 가치가 있다.

  • QueryDSL: Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스모음, 비표준 오픈소스 프레임워크다.
  • JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용

전체적으로 하나하나 아주 간단히 살펴보겠다.

JPQL 소개

JPQL은 엔티티 객체를 조회하는 객체지향 쿼리다. 문법은 SQL과 비슷하다. JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다. 그리고 데이터베이스 방언(Dialect)만 변경하면 JPQL을 수정하지 않아도 자연스럽게 데이터베이스를 변경할 수 있다. JPQL은 SQL보다 간결하다. 

회원 엔티티를 대상으로 JPQL을 사용하는 간단한 예시이다.

@Entity(name = "Member")
public class Member {
   
   @Column(name = "name")
   private String username;
   //...
}
//쿼리 생성
String jpql = "select m from Member as m where m.username = 'kim'";
List<Member> resultList = 
   em.createQuery(jpql, Member.class).getResultList();

 

Criteria 쿼리 소개

Criteria는 JPQL을 생성하는 빌더 클래스다. Criteria의 장점은 문자가 아닌 query.select(m).where(...)처럼 프로그래밍 코드로 JPQL을 작성할 수 있다는 점이다. 예를 들어 JPQL에서 select m from Memberrrr m 처럼 오타가 있다고 가정해보자. 그래도 컴파일은 성공하고 애플리케이션을 서버에 배포할 수 있다. 문제는 해당 쿼리가 실행되는 런타임 시점에 오류가 발생한다는 점이다. 이것이 문자기반 쿼리의 단점이다. 반면에 Criteria는 문자가 아닌 코드로 JPQL을 작성한다. 따라서 컴파일 시점에서 오류를 발견할 수 있다.

문자로 작성한 JPQL보다 코드로 작성한 Criteria의 장점은 다음과 같다.

  • 컴파일 시점에 오류를 발견할 수 있다.
  • IDE를 사용하면 코드 자동완성을 지원한다.
  • 동적 쿼리를 작성하기 편하다.

간단한 Criteria 사용 코드를 보자.

//Criteria 사용 준비
CriteriaBuilder cd = 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();

이 코드에서 아쉬운 점은 m.get("username")을 보면 필드 명을 문자로 작성했다. 이 부분을 문자가 아닌 코드로 작성하고 싶으면 메타모델을 사용하면 된다. JPA는 Member 엔티티 클래스로부터 Member_라는 Criteria 전용 클래스를 생성하는데 이것을 메타모델이라고 한다. 메타 모델을 사용하면 온전히 코드만 사용해서 쿼리를 작성할 수 있다.

// 메타 모델 사용 전 -> 사용 후
m.get("username") -> m.get(Member_.username)

Criteria는 코드로 쿼리를 작성할 수 있어서 동적 쿼리를 작성할 때 유용하다.

 

QueryDSL 소개

QueryDSL도 Criteria처럼 JPQL 빌더 역할을 한다. QueryDSL의 장점은 코드 기반이면서 단순하고 사용하기 쉽다. 그리고 작성한 코드도 JPQL과 비슷해서 한눈에 들어온다. 

QueryDSL로 작성한 코드이다.

//준비
JPAQuery query = new JPAQuery(em);
QMember member = QMember.member;

//쿼리, 결과 조회
List<Member> members = query.from(member)
                            .where(member.username.eq("kim"))
                            .list(member);

QMember는 Member 엔티티 클래스를 기반으로 생성한 QueryDSL 쿼리 전용 클래스다.

 

네이티브 SQL 소개

JPA는 SQL을 직접 사용할 수 있는 기능을 지원하는데 이것을 네이티브 SQL이라고 한다. JPQL을 사용해도 가끔은 특정 데이터베이스에 의존하는 기능을 사용해야 할 때가 있다. 네이티브의 단점은 특정 데이터베이스에 의존하는 SQL을 작성해야 한다는 것이다. 따라서 데이터베이스를 변경하면 네이티브 SQL도 수정해야한다.

String sql = "SELECT ID, TEAM_ID, NAME FROM MEMBER WHERE NAME = 'kim'";
List<Member> resultList = em.createNativeQuery(sql, member.class).getResultList();

실행하면 직접 작성한 SQL을 데이터베이스에 전달한다.

 

JDBC 직접 사용, 마이바티스 같은 SQL 매퍼 프레임워크 사용

이런 일은 드물겠지만, JDBC 커넥션에 직접 접근하고 싶으면 JPA는 JDBC 커넥션을 획득하는 API를 제공하지 않으므로 JPA 구현체가 제공하는 방법을 사용해야 한다. 하이버네이에서 직접 JDBC Connection을 획득하는 방법이 있다.

Session session = entityManager.unwrap(Session.class);
session.doWork(new Work(){
   @Override
   public void execute(Connection connection) throws SQLExeption {
      //work...
   }
});

JDBC를 직접 사용하든 마이바티스같은 SQL 매퍼와 사용하든 모두 JPA를 우회해서 데이터베이스에 접근한다. 문제는 JPA를 우회하는 SQL에 대해서는 JPA가 전혀 인식하지 못한다는 점이다. 최악의 시나리오는 같은 트랜잭션에서 영속성 컨텍스트에 있는 상품의 가격을 변경하고 아직 플러시를 하지 않았는데 JPA를 우회해서 데이터베이스에 직접 상품을 조회하면 변경되지 않은 상품 가격이 조회된다. 이런 이슈를 해결하는 방법은 JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해서 데이터베이스와 영속성 컨텍스트를 동기화하면 된다.

스프링 프레임워크를 사용하면 JPA와 마이바티스를 손쉽게 통합할 수 있다. 또한 스프링 프레임워크의 *AOP를 적절히 활용해서 JPA를 우회하여 데이터베이스에 접근하는 메소드를 호출할 때마다 영속성 컨텍스트를 플러시하면 위에서 언급한 문제도 깔끔하게 해결할 수 있다.

*AOP : 스프링에서 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화한다.

반응형

'JPA' 카테고리의 다른 글

JPQL 조인  (2) 2023.10.10
JPQL - (기본 문법, 쿼리 API, 페이징 API, 집합과 정렬)  (0) 2023.09.25
값 타입  (0) 2023.09.22
영속성 전이: CASCADE  (0) 2023.07.31
즉시 로딩과 지연 로딩  (0) 2023.07.31