[스프링] JPA에서 제공하는 쿼리 방법

2025. 7. 26. 19:50·스프링

애플리케이션에서 데이터를 다룰 때마다 SQL을 직접 작성하는 일은 생각보다 번거롭다. 쿼리가 길어질수록 가독성은 떨어지고, 테이블 구조가 바뀔 때마다 일일이 SQL을 고쳐야 한다. 여기에 객체와 테이블 사이의 변환까지 신경 써야 한다면, 코드보다 쿼리에 더 많은 시간을 쓰게 된다.

 

JPA는 이런 상황을 줄이기 위해 등장한 기술이다. 데이터베이스와의 상호작용을 객체 중심으로 바꿔주고, 쿼리 역시 테이블이 아닌 엔티티 중심으로 작성할 수 있게 한다. JPA는 다양한 쿼리 방법을 지원하지만, 실무에서 가장 많이 사용되는 것은 JPQL이다. 그리고 성능 최적화 측면에서 자주 등장하는 Fetch Join도 함께 이해할 필요가 있다.

 

JPA에서 제공하는 쿼리 방법

JPA는 다음과 같은 방식으로 쿼리를 작성할 수 있다.

 

1. JPQL (Java Persistence Query Language)

JPA의 기본 쿼리 언어이다. SQL과 문법은 비슷하지만, 테이블이 아닌 엔티티 객체를 대상으로 쿼리한다는 점이 다르다.

String jpql = "select m from Member m where m.age > 18";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();

이 쿼리는 실행 시 다음과 같은 SQL로 변환된다:

SELECT m.id, m.age, m.username, m.team_id
FROM member m
WHERE m.age > 18

 

장점

  • JPA에서 가장 표준적이고 널리 쓰이는 방식
  • SQL처럼 익숙한 문법이지만, 객체지향적으로 표현 가능
  • 페이징 API(setFirstResult, setMaxResults)와 함께 사용하기 쉬움

단점

  • 문자열 기반이라 오타나 문법 오류를 런타임에야 확인할 수 있음
  • 복잡한 동적 쿼리에는 적합하지 않음

 

2. Criteria API

자바 코드로 JPQL을 조립하는 방식이다. JPA에서 공식적으로 지원하며, 특히 복잡한 조건을 코드로 구성할 때 유리하다.

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
Root<Member> m = query.from(Member.class);
query.select(m).where(cb.equal(m.get("username"), "kim"));
List<Member> result = em.createQuery(query).getResultList();

장점

  • 자바 코드로 작성되기 때문에 컴파일 시 문법 오류를 잡을 수 있음
  • 조건 분기나 동적 쿼리 조합이 가능함

단점

  • 문법이 너무 장황하고 가독성이 떨어짐
  • 실제로 쓰기 불편해서 대부분의 실무 현장에서는 거의 사용하지 않음

 

3. QueryDSL

QueryDSL은 Criteria의 복잡함을 해결하기 위해 만들어진 대안이다. 자바 코드로 쿼리를 작성하면서도 간결하고 직관적인 문법을 제공한다.

JPAQueryFactory query = new JPAQueryFactory(em);
QMember m = QMember.member;

List<Member> result = query
    .selectFrom(m)
    .where(m.age.gt(18))
    .orderBy(m.name.desc())
    .fetch();

장점

  • 타입 안정성과 가독성을 모두 확보
  • 동적 쿼리 작성이 간편
  • 실무에서 가장 널리 사용되는 JPQL 대체제

단점

  • 빌드 시점에 Q클래스 생성이 필요함 (APT 설정)
  • 프로젝트 세팅 시 약간의 추가 설정 필요

 

4. 네이티브 SQL

JPA를 사용하면서도 필요하다면 SQL을 직접 쓸 수 있다. 특정 DB에 종속적인 기능이나 복잡한 쿼리를 다뤄야 할 때 선택한다.

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

장점

  • 복잡한 SQL을 그대로 사용할 수 있음 (JOIN, 서브쿼리, DB별 특수 기능 등)
  • DB 튜닝이나 프로시저 연동도 가능

단점

  • 객체 매핑을 수동으로 해야 하므로 생산성이 떨어짐
  • 데이터베이스 독립성이 깨짐 (JPA 철학과는 어긋남)
  • 결과가 엔티티가 아닌 Object[]나 DTO로 나옴

 

Fetch Join – 성능 최적화의 핵심

JPA를 쓰면 연관된 엔티티는 기본적으로 지연 로딩(LAZY)된다. 예를 들어 회원을 조회했는데, 회원이 속한 팀 정보는 실제로 접근하기 전까지 로딩되지 않는다. 이런 구조는 불필요한 데이터를 줄이는 데 효과적이지만, 반복 조회에서는 N+1 문제(이전 장에서 설명)가 발생할 수 있다.

이를 해결하기 위한 방식이 바로 Fetch Join이다. join fetch 키워드를 사용해, 연관된 엔티티를 한 번에 함께 가져오는 방식이다.

 

예시: 회원과 팀을 함께 조회

String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class).getResultList();

 

JPQL은 객체 기준으로 작성되지만, 내부적으로는 다음과 같은 SQL이 만들어진다.

SELECT m.*, t.*
FROM member m
JOIN team t ON m.team_id = t.id

이렇게 하면 member.getTeam()을 호출해도 추가 쿼리가 발생하지 않는다.

 

예를 들어 하나의 팀에 멤버가 10명 있다고 하자.
fetch join을 사용하지 않으면, 팀을 한 번 조회하고 나서 그 안에 있는 멤버들을 지연 로딩 방식으로 하나씩 불러오게 된다. 이 경우, 팀 조회 쿼리 1번 + 멤버 조회 쿼리 10번으로 총 11번의 쿼리가 실행된다.

 

반면 fetch join을 사용하면, 팀과 멤버를 한 번의 SQL로 조인해서 가져오므로 쿼리가 1번만 실행된다.
실제 SQL에서는 TEAM과 MEMBER 테이블이 조인되며, 한 번의 쿼리로 팀과 그에 속한 모든 멤버를 모두 불러올 수 있다.

 

결과적으로 fetch join은 불필요한 반복 쿼리를 줄이고, 연관 데이터를 한 번에 조회할 수 있게 해준다.

 

컬렉션 Fetch Join (1:N)

select t from Team t join fetch t.members where t.name = '팀A'

팀 하나와 그 팀에 속한 모든 회원을 한 번의 쿼리로 가져온다. 단, 컬렉션 페치 조인은 결과가 중복될 수 있고, 페이징이 불가능하다. 하이버네이트 6부터는 distinct 없이도 중복 제거가 시도되지만, 여전히 주의가 필요하다.

 

Fetch Join 정리

  • 연관 엔티티를 SQL 한 번에 조회할 수 있다.
  • 지연 로딩으로 인한 N+1 문제를 막는다.
  • 컬렉션(1:N) fetch join은 페이징 불가.
  • 꼭 필요한 경우에만 신중하게 사용하는 것이 좋다.

 

마무리 하며

JPA에서 데이터를 효율적으로 다루려면 단순히 쿼리를 작성하는 것만으로는 부족하다.


어떤 방식으로 데이터를 조회할지, 연관된 엔티티는 언제 로딩할지, 반복 호출을 어떻게 줄일지 등을 함께 고려해야 한다.

 

이번 글에서 살펴본 JPQL과 fetch join은 JPA에서 가장 기본이면서도 중요한 기능이다.


특히 fetch join은 N+1 문제를 해결하고 성능을 안정적으로 유지하기 위한 핵심 도구다. 단순히 기능을 아는 것을 넘어, 실제 어떤 상황에서 적용할지 판단하는 감각이 중요하다.

 

다음에는 JPA 위에서 동작하는 Spring Data JPA에 대해 다룰 예정이다.
쿼리를 직접 작성하지 않고도 데이터를 조회하고, 복잡한 조건을 쉽게 구성할 수 있는 방법들을 소개할 계획이다.

 

감사합니다.

'스프링' 카테고리의 다른 글

[스프링] 지연 로딩과 조회 성능 최적화  (3) 2025.07.27
[스프링] DTO가 필요한 이유  (3) 2025.07.27
[스프링] JPA 상속관계 매핑  (7) 2025.07.25
[스프링] JPA 연관관계 매핑  (2) 2025.07.25
[스프링] JPA 기본 매핑 어노테이션  (4) 2025.07.24
'스프링' 카테고리의 다른 글
  • [스프링] 지연 로딩과 조회 성능 최적화
  • [스프링] DTO가 필요한 이유
  • [스프링] JPA 상속관계 매핑
  • [스프링] JPA 연관관계 매핑
0kingki_
0kingki_
자바 + 스프링 웹 개발
  • 0kingki_
    0kingki_
    0kingki_
  • 전체
    오늘
    어제
    • 분류 전체보기 (134)
      • 코딩 테스트 (54)
      • 자바 (21)
      • 스프링 (27)
      • 타임리프 (16)
      • 스프링 데이터 JPA (8)
      • 최적화 (2)
      • QueryDSL (4)
      • AWS (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    예외 처리
    JPA
    스프링 컨테이너
    mvc
    SpringDataJpa
    fetch join
    백준
    thymeleaf
    컬렉션
    재귀
    Java
    자바
    쿼리dsl
    LocalDateTime
    dfs
    코딩 테스트
    쿼리
    코딩테스트
    SOLID
    최적화
    QueryDSL
    spring
    BFS
    스프링 데이터 JPA
    타임리프
    불변객체
    스프링
    예외처리
    객체지향
    다형성
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
0kingki_
[스프링] JPA에서 제공하는 쿼리 방법
상단으로

티스토리툴바