말귀 알아듣기.zip

Spring Data JPA 쿼리 작성 완벽 가이드 | Query Methods, JPQL, QueryDSL, Native SQL 총정리

zippy 2026. 4. 19. 01:06
반응형

JPA 쿼리는 데이터베이스에서 데이터를 조회·저장·수정·삭제하기 위해 사용하는 명령어를, 객체(Entity)를 기준으로 다루도록 도와주는 도구입니다. SQL처럼 보이지만 테이블이 아닌 엔티티(User)를 사용한다는 점이 핵심입니다. 이 글에서는 Query Methods부터 Native SQL까지 5가지 쿼리 작성 방법을 실습 예제와 함께 완벽하게 정리합니다.

1. JPA 쿼리란 무엇인가? (기초 개념)

📌 핵심 개념

JPA 쿼리는 데이터베이스에서 데이터를 조회·저장·수정·삭제하기 위해 사용하는 명령어를, 객체(Entity)를 기준으로 다루도록 도와주는 도구입니다. SQL처럼 보이지만, 테이블이 아니라 엔티티(User)를 사용합니다.

💡 쉬운 비유 설명

  • SQL: user라는 테이블 이름을 직접 호출해서 일하는 느낌
  • JPA 쿼리: User라는 자바 객체(엔티티)를 부르면서 일하는 느낌

즉, "DB야, user 테이블에서 가져와"가 아니라, "JPA야, User 객체들 중에서 골라줘"라고 말하는 것과 같습니다.

🖥️ 실제 코드 예제 (User 엔티티 기준)

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;

    private LocalDateTime createdDate;

    // getters, setters 생략
}

✅ 언제 사용하나?

데이터베이스에서 User 목록을 조회하거나 저장/수정/삭제가 필요할 때 항상 사용합니다.

❌ 피해야 할 경우

  • JPA를 전혀 쓰지 않는 프로젝트(MyBatis만 사용 등)에서는 사용하지 않습니다.
  • DB 특수 기능(예: 특정 DB만 지원하는 함수)만 써야 할 때는 Native SQL이 더 적합할 수 있습니다.

2. 5가지 쿼리 방법 개요 및 언제 사용하나?

Spring Data JPA에서 쿼리를 작성하는 대표적인 5가지 방법이 있습니다. 각 방법은 난이도와 쓰임새가 다릅니다.

  1. Query Methods: 메서드 이름 기반 쿼리
  2. JPQL: Java Persistence Query Language
  3. Criteria API: 코드로 쿼리를 조립하는 방식
  4. QueryDSL: 타입 안전한 DSL 기반 쿼리
  5. Native SQL: 순수 SQL 직접 작성

💡 쉬운 비유 설명

  • Query Methods: "치킨 주문"할 때 메뉴판 번호만 말하는 것 → "1번 세트 주세요"처럼 간단한 주문
  • JPQL: 직접 레시피를 글로 쓰는 것 → "닭에 양념 바르고 180도에 20분 구워줘" 같은 문장 쿼리
  • Criteria API: 레고 블록으로 요리를 만드는 느낌 → 코드로 쿼리를 조합
  • QueryDSL: Criteria보다 똑똑한 레고 세트 → 타입 안전 + 자동완성 지원
  • Native SQL: 아예 주방에 들어가서 직접 요리를 하는 것 → 순수 SQL 작성

✅ 언제 사용하나? (요약)

  • Query Methods: 간단한 조건 검색, CRUD, 빠른 개발
  • JPQL: 조인, 그룹핑, 집계 등 중간 이상 복잡도 쿼리
  • Criteria API: 화면 검색 조건에 따라 동적으로 쿼리가 계속 바뀔 때
  • QueryDSL: 실무에서 복잡한 동적 쿼리를 많이 쓸 때 최강
  • Native SQL: JPA로 표현하기 어려운 특수한 SQL, 성능 최적화가 필요할 때

❌ 피해야 할 경우 (요약)

  • 간단한 쿼리를 Native SQL로 다 써버리기 → 유지보수 지옥
  • 동적 쿼리가 많은데 JPQL 문자 조합만 쓰기 → 코드 난장판

3. JPQL 완전 정복

📌 핵심 개념

JPQL(Java Persistence Query Language)은 엔티티를 기준으로 작성하는 쿼리 언어입니다. SQL과 문법이 비슷하지만, 테이블 이름이 아니라 엔티티 이름(User)을 사용합니다.

💡 쉬운 비유 설명

  • SQL: SELECT * FROM user WHERE name = '홍길동'
  • JPQL: SELECT u FROM User u WHERE u.name = '홍길동'

즉, user 테이블이 아니라 User 엔티티를 대상으로 합니다.

🖥️ 기본 JPQL 문법 예시

// Repository 예시
public interface UserRepository extends JpaRepository<User, Long> {

    // 이름이 정확히 일치하는 사용자 조회
    @Query("SELECT u FROM User u WHERE u.name = :name")
    List<User> findByNameUsingJpql(@Param("name") String name);
}
  • SELECT u FROM User u: User 엔티티를 u라는 별칭으로 조회
  • WHERE u.name = :name: u의 name 필드가 파라미터 :name과 같은 데이터

🖥️ 실습 예제 5개

예제 1: 이름으로 부분 검색 (LIKE)

@Query("SELECT u FROM User u WHERE u.name LIKE %:name%")
List<User> findByNameContainingJpql(@Param("name") String name);

예제 2: 날짜 범위 검색 (BETWEEN)

@Query("SELECT u FROM User u " +
       "WHERE u.createdDate BETWEEN :start AND :end")
List<User> findByCreatedDateBetweenJpql(
        @Param("start") LocalDateTime start,
        @Param("end") LocalDateTime end
);

예제 3: 여러 조건 (AND)

@Query("SELECT u FROM User u " +
       "WHERE u.name = :name AND u.email = :email")
Optional<User> findByNameAndEmailJpql(
        @Param("name") String name,
        @Param("email") String email
);

예제 4: 정렬해서 상위 N개 조회

@Query("SELECT u FROM User u ORDER BY u.createdDate DESC")
List<User> findRecentUsersJpql(Pageable pageable);

// 사용 예시
PageRequest.of(0, 10); // 최근 10명

예제 5: 개수 세기 (COUNT)

@Query("SELECT COUNT(u) FROM User u WHERE u.email IS NOT NULL")
Long countUsersHasEmail();

여기까지 이해했다면, SELECT / FROM / WHERE / ORDER BY 구조는 SQL과 거의 같고, 테이블 대신 엔티티, 컬럼 대신 필드명을 쓴다는 것만 기억하면 됩니다.

📌 장점

  • SQL과 비슷해서 입문 난이도가 낮음
  • 엔티티 기준이라 객체지향 코드와 잘 어울림
  • 여러 DB 벤더(MySQL, PostgreSQL 등)에 이식성이 좋음

📌 단점

  • 문자열로 작성하므로, 오타를 컴파일 타임에 잡기 어려움
  • 동적 쿼리(조건이 늘었다 줄었다)는 코드가 지저분해짐

✅ 언제 사용하나? 어느 정도 복잡한 고정 쿼리(조인, 집계, 그룹핑 등)가 필요할 때 적합합니다.

❌ 피해야 할 경우: 화면에 체크박스/검색 조건이 많아서 조건이 자주 바뀌는 검색 쿼리를 만들 때 (이때는 QueryDSL이 더 좋습니다).

4. Query Methods (메서드 이름 쿼리)

📌 핵심 개념

Query Methods는 메서드 이름만으로 쿼리를 자동 생성하는 기능입니다. findBy + 필드명 + 키워드 형태로 쓰면 Spring Data JPA가 알아서 SQL을 만들어 줍니다.

💡 쉬운 비유 설명

메서드 이름이 곧 주문서입니다. findByName이라고 쓰면, 스프링이 "아~ name으로 찾으라는 거구나"라고 알아듣고 쿼리를 만들어 줍니다.

🖥️ 대표 키워드 예시

  • 논리 연산: And, Or
  • 범위 비교: Between, LessThan, GreaterThanEqual
  • 문자열 검색: Like, Containing, StartsWith, EndsWith
  • NULL 체크: IsNull, IsNotNull
  • 목록 포함 여부: In, NotIn
  • 정렬: OrderBy필드Asc, OrderBy필드Desc

🖥️ 실습 예제 10개

public interface UserRepository extends JpaRepository<User, Long> {

    // 1. 이름이 정확히 같은 사용자 찾기
    List<User> findByName(String name);

    // 2. 이름에 특정 문자열이 포함된 사용자 (LIKE %keyword%)
    List<User> findByNameContaining(String keyword);

    // 3. 이름이 A 이고 이메일이 B 인 사용자
    Optional<User> findByNameAndEmail(String name, String email);

    // 4. 이름이 A 이거나 이메일이 B 인 사용자
    List<User> findByNameOrEmail(String name, String email);

    // 5. id가 특정 값보다 큰 사용자
    List<User> findByIdGreaterThan(Long id);

    // 6. 생성일이 특정 기간 사이인 사용자
    List<User> findByCreatedDateBetween(LocalDateTime start, LocalDateTime end);

    // 7. 이메일이 없는 사용자 (NULL)
    List<User> findByEmailIsNull();

    // 8. 이름으로 찾되, 생성일 내림차순 정렬
    List<User> findByNameOrderByCreatedDateDesc(String name);

    // 9. 이름으로 찾되, 상위 1명만 조회
    Optional<User> findFirstByNameOrderByCreatedDateDesc(String name);

    // 10. 중복 제거 후 이메일로 찾기 (DISTINCT)
    List<User> findDistinctByEmail(String email);
}

✅ 언제 사용하나?

  • 단순 검색 조건(1~3개 정도)일 때
  • 빠르게 CRUD 기능을 만들고 싶을 때
  • 쿼리 문법이 아직 익숙하지 않은 초보자 단계에서 스타트용으로 아주 좋음

❌ 피해야 할 경우

  • 메서드 이름이 너무 길어질 정도로 조건이 복잡할 때 (예: findByNameAndEmailAndCreatedDateBetweenAndStatus...처럼 이름이 괴물이 될 때)
  • 조인, 서브쿼리, 복잡한 그룹핑이 필요할 때

📌 한계 정리: 복잡한 SQL을 표현하기 어렵고, 조건이 조금만 복잡해져도 메서드 이름이 기괴해집니다. 간단한 것은 Query Methods, 조금만 복잡해지면 JPQL 또는 QueryDSL로 넘어가는 것이 좋습니다.

5. Criteria API

📌 핵심 개념

Criteria API는 코드(자바 문법)로 쿼리를 조립하는 방식입니다. 문자열이 아니라, CriteriaBuilder, CriteriaQuery, Root 등을 사용해 쿼리를 만듭니다.

💡 쉬운 비유 설명

JPQL이 "문장으로 설명"이라면, Criteria API는 "레고 블럭으로 조립"입니다. if 문으로 조건을 추가/삭제하기 쉬워서 동적 쿼리에 적합하지만, 코드가 길고 장황해집니다.

🖥️ 단계별 실습 예제

아래 예제는 이름과 이메일을 조건으로, 필요한 것만 동적으로 붙이는 예시입니다.

import jakarta.persistence.*;
import jakarta.persistence.criteria.*;

@Repository
public class UserCriteriaRepository {

    @PersistenceContext
    private EntityManager em;

    public List<User> search(String name, String email) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<User> cq = cb.createQuery(User.class);

        Root<User> user = cq.from(User.class);

        List<Predicate> predicates = new ArrayList<>();

        // 이름 조건이 있을 때만 추가
        if (name != null && !name.isBlank()) {
            predicates.add(cb.like(user.get("name"), "%" + name + "%"));
        }

        // 이메일 조건이 있을 때만 추가
        if (email != null && !email.isBlank()) {
            predicates.add(cb.equal(user.get("email"), email));
        }

        // 조건 합치기
        cq.where(predicates.toArray(new Predicate[0]));

        return em.createQuery(cq).getResultList();
    }
}

단계별 설명

  1. CriteriaBuilder cb = em.getCriteriaBuilder();: 빌더 꺼내기
  2. CriteriaQuery<User> cq = cb.createQuery(User.class);: User를 조회하는 쿼리 객체 생성
  3. Root<User> user = cq.from(User.class);: FROM User user와 같은 느낌
  4. Predicate 리스트에 조건을 하나씩 추가
  5. 마지막에 cq.where(...)로 조건들을 한 번에 적용

✅ 언제 사용하나?

  • 검색 화면에서 체크박스, 드롭다운 등으로 조건이 유동적으로 변할 때
  • 런타임에 조건이 바뀌는 경우(예: 검색 옵션 여러 개)

❌ 피해야 할 경우

  • 쿼리가 단순한데 Criteria를 쓰면 코드가 너무 길어져서 손해
  • 실무에서는 대부분 Criteria 대신 QueryDSL을 더 선호

📌 장황함 해결법: 메서드 분리로 조건별로 작은 메서드로 쪼개거나, Spring Data JPA의 Specification 인터페이스를 활용해 재사용 가능한 조건을 만들 수 있습니다. 하지만 가장 실무적인 해결책은 QueryDSL로 넘어가는 것입니다.

6. QueryDSL (실무 최강자)

📌 핵심 개념

QueryDSL은 타입 안전한(컴파일 시점에 검증되는) 쿼리 DSL(Domain Specific Language)입니다. 엔티티를 기준으로 QUser 같은 클래스를 자동 생성해서, 메서드 체인(Fluent API)으로 쿼리를 작성합니다.

💡 쉬운 비유 설명

Criteria API가 레고 블럭이라면, QueryDSL은 "자동완성 되는 스마트 레고"입니다. user.name.eq("홍길동")처럼, 필드명을 타이핑할 때 IDE가 자동완성을 도와줍니다.

🖥️ Gradle 설정 예시

dependencies {
    implementation "com.querydsl:querydsl-jpa:5.0.0"
    annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jpa"
}

QueryDSL을 설정하면 User 엔티티 기준으로 다음과 같은 클래스가 자동 생성됩니다.

// 자동 생성 (수동 작성 금지)
public class QUser extends EntityPathBase<User> {
    public static final QUser user = new QUser("user");

    public final StringPath name = createString("name");
    public final StringPath email = createString("email");
    public final DateTimePath<LocalDateTime> createdDate = createDateTime("createdDate", LocalDateTime.class);
}

🖥️ Fluent API 실습 예제

예제 1: 이름으로 검색

import com.querydsl.jpa.impl.JPAQueryFactory;
import static com.example.demo.domain.QUser.user;

@Repository
public class UserQuerydslRepository {

    private final JPAQueryFactory queryFactory;

    public UserQuerydslRepository(JPAQueryFactory queryFactory) {
        this.queryFactory = queryFactory;
    }

    public List<User> findByName(String name) {
        return queryFactory
                .selectFrom(user)
                .where(user.name.eq(name))
                .fetch();
    }
}

예제 2: 동적 검색 (이름, 이메일 옵션)

public List<User> search(String name, String email) {
    BooleanBuilder builder = new BooleanBuilder();

    if (name != null && !name.isBlank()) {
        builder.and(user.name.contains(name));
    }

    if (email != null && !email.isBlank()) {
        builder.and(user.email.eq(email));
    }

    return queryFactory
            .selectFrom(user)
            .where(builder)
            .fetch();
}

예제 3: 정렬 + 페이징

public List<User> findRecentUsers(int page, int size) {
    return queryFactory
            .selectFrom(user)
            .orderBy(user.createdDate.desc())
            .offset((long) page * size)
            .limit(size)
            .fetch();
}

✅ 언제 사용하나?

  • 동적 검색 조건이 많은 페이지(검색 화면, 필터링이 많은 리스트 화면 등)
  • 협업이 많고, 장기 프로젝트에서 가독성 + 유지보수성이 중요한 경우

❌ 피해야 할 경우: 아주 작은 프로젝트나 간단한 CRUD만 필요한 토이 프로젝트(설정 비용이 아까울 수 있음)

📌 Criteria API vs QueryDSL 비교

항목 Criteria API QueryDSL
코드 길이 매우 김 상대적으로 짧음
타입 안전성 있음 있음
가독성 낮음 높음
IDE 자동완성 제한적 매우 좋음
실무 사용 빈도 낮음 높음

실무에서는 Criteria API 대신 QueryDSL을 사용하는 경우가 훨씬 많습니다.

7. Native SQL

📌 핵심 개념

Native SQL은 JPA가 제공하는 추상화 계층을 넘어서, DB에 직접 SQL을 날리는 방식입니다. @Query(nativeQuery = true)를 사용해 순수 SQL을 그대로 작성합니다.

💡 쉬운 비유 설명

지금까지는 번역가(JPA)가 대신 말해줬다면, Native SQL은 번역가 없이 직접 외국어(DB 언어)를 쓰는 것입니다.

✅ 언제 사용하나?

  • JPA/JPQL로 표현하기 어려운 복잡한 SQL이 필요할 때
  • DB 전용 함수, 윈도우 함수, FULL TEXT SEARCH 등 특수 기능을 사용할 때

❌ 피해야 할 경우: 단순 조회/저장까지 전부 Native로 처리하면 JPA의 장점을 거의 버리는 것과 같습니다.

🖥️ @Query 작성법

public interface UserRepository extends JpaRepository<User, Long> {

    // 1. 이름으로 검색 (Native SQL)
    @Query(value = "SELECT * FROM user WHERE name = :name", nativeQuery = true)
    List<User> findByNameNative(@Param("name") String name);

    // 2. 날짜 범위 + 정렬
    @Query(value = "SELECT * FROM user " +
                   "WHERE created_date BETWEEN :start AND :end " +
                   "ORDER BY created_date DESC",
           nativeQuery = true)
    List<User> findByCreatedDateBetweenNative(
            @Param("start") LocalDateTime start,
            @Param("end") LocalDateTime end
    );

    // 3. 개수 세기
    @Query(value = "SELECT COUNT(*) FROM user WHERE email IS NOT NULL", nativeQuery = true)
    Long countUsersHasEmailNative();
}

📌 주의사항

  • 테이블 이름, 컬럼 이름을 직접 써야 하므로, 엔티티 변경 시 SQL도 같이 고쳐야 합니다.
  • DB 종류를 바꾸면 SQL 문법 차이로 쿼리가 그대로 동작하지 않을 수 있습니다.
  • 프로젝트 전체의 10~20% 정도, 꼭 필요할 때만 제한적으로 사용하는 것이 좋습니다.

8. 5가지 방법 완벽 비교표

방법 난이도 동적쿼리 지원 성능 실무 사용도 특징 요약
Query Methods 매우 쉬움 거의 없음 좋음 매우 높음 메서드 이름만으로 쿼리 자동 생성
JPQL 중간 문자열 조합으로 가능(불편) 좋음 높음 엔티티 기준의 객체지향 쿼리
Criteria API 어려움 좋음 좋음 낮음 동적 쿼리 가능하지만 코드 장황
QueryDSL 중간~조금 어려움 매우 좋음 좋음~최상 매우 높음 실무 동적 쿼리의 사실상 표준
Native SQL 중간 SQL로 직접 처리 최상 (튜닝 여지 큼) 중간 특수 기능/최적화용 보조 수단

9. 실무 선택 가이드

📌 핵심 개념

실무에서는 보통 하나만 쓰지 않고, 여러 가지를 섞어서 사용합니다. 하지만 기본 축은 Query Methods + JPQL + QueryDSL 조합인 경우가 많습니다.

💡 추천 조합 (단계별)

  • 기본 CRUD, 단순 조회: Query Methods
  • 고정된 복잡 쿼리: JPQL (@Query)
  • 동적 검색, 조건이 많은 화면: QueryDSL
  • 성능 최적화/특수 기능: Native SQL (필요할 때만)

🖥️ 예시 시나리오: 회원 관리 페이지

  • findByEmail → Query Method
  • 회원 + 주문 + 결제 상태 복잡 조인 → JPQL 또는 QueryDSL
  • 검색 필터(이름, 기간, 상태, 등급 등) → QueryDSL

✅ 실무에서의 좋은 전략 (새 프로젝트 시작 시)

  1. 무조건 Query Methods 먼저로 간단히 기능 구현
  2. 복잡해지는 지점부터 JPQL 또는 QueryDSL로 분리
  3. 성능이 중요한 핵심 쿼리는 필요 시 Native SQL 도입

❌ 피해야 할 선택

  • 처음부터 모든 쿼리를 Native SQL로 작성
  • 단순 CRUD까지 QueryDSL로만 하겠다며 과도하게 복잡하게 설계

10. 흔한 실수와 해결법

📌 실수 1: N+1 문제를 무시

연관관계가 있는 엔티티를 조회할 때 쿼리가 여러 번 나가는 문제입니다. 해결: fetch join, @EntityGraph 등을 사용해 한 번에 함께 로딩합니다.

📌 실수 2: Query Methods에 모든 걸 우겨 넣기

메서드 이름이 너무 길어져서 가독성이 떨어집니다. 해결: 일정 수준 이상 복잡해지면 JPQL/QueryDSL로 과감히 분리합니다.

📌 실수 3: Native SQL에서 엔티티 변경을 반영 안 함

컬럼명/테이블명 변경 시 Native 쿼리가 깨집니다. 해결: Native SQL은 최소화하고, 사용 시 반드시 테스트 코드로 커버합니다.

📌 실수 4: 쿼리 디버깅을 안 켜놓음

실제로 DB에 어떤 SQL이 나가는지 모르는 상태로 개발하게 됩니다. 해결: 개발 환경에서 다음 설정으로 SQL을 항상 눈으로 확인합니다.

spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

11. 다음 스텝 학습 로드맵

JPA 쿼리는 한 번에 다 이해하기 어렵습니다. 단계별로 조금씩 올라가는 것이 좋습니다.

💡 추천 학습 순서 (왕초보 기준)

1단계: Query Methods 완전 정복

User 엔티티로 findByName, findByEmail, findByCreatedDateBetween 등 10개 이상 직접 만들어보기

2단계: JPQL 맛보기

@Query로 간단한 SELECT, WHERE, ORDER BY, COUNT 등을 직접 작성하고, SQL과 JPQL의 차이(테이블 vs 엔티티)를 몸으로 익히기

3단계: QueryDSL 도입

QUser 클래스가 어떻게 생성되는지 확인하고, selectFrom(user).where(...).fetch() 패턴에 익숙해지기. 동적 검색 화면 하나를 QueryDSL로 구현해보기

4단계: 성능 및 고급 주제

N+1 문제, 페이징, 캐시, fetch join, batch size 등

5단계: Native SQL 적절히 활용

정말 JPA로 표현하기 어려운 쿼리만 Native로 작성하고, 항상 테스트 코드로 검증

마지막 체크리스트 ("이해했어요!" 자기 점검)

  • User 엔티티 기준으로 Query Methods 10개 이상 직접 만들 수 있다.
  • JPQL로 SELECT / WHERE / ORDER BY / COUNT를 직접 작성해 볼 수 있다.
  • Criteria API가 왜 장황한지, 언제 쓰면 좋은지 설명할 수 있다.
  • QueryDSL 코드(selectFrom(user).where(...))를 보고 대략 어떤 쿼리인지 이해할 수 있다.
  • Native SQL이 언제 필요한지, 왜 전부 Native로 쓰면 안 되는지 말할 수 있다.

위 체크리스트에 모두 체크할 수 있다면, 이제 "이해했어요!"라고 말해도 충분한 수준입니다. 직접 작은 Spring Boot 프로젝트를 만들어, 이 가이드에 나온 예제들을 하나씩 따라 치면서 몸에 익혀보세요.

반응형