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가지 방법이 있습니다. 각 방법은 난이도와 쓰임새가 다릅니다.
- Query Methods: 메서드 이름 기반 쿼리
- JPQL: Java Persistence Query Language
- Criteria API: 코드로 쿼리를 조립하는 방식
- QueryDSL: 타입 안전한 DSL 기반 쿼리
- 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();
}
}
단계별 설명
- CriteriaBuilder cb = em.getCriteriaBuilder();: 빌더 꺼내기
- CriteriaQuery<User> cq = cb.createQuery(User.class);: User를 조회하는 쿼리 객체 생성
- Root<User> user = cq.from(User.class);: FROM User user와 같은 느낌
- Predicate 리스트에 조건을 하나씩 추가
- 마지막에 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
✅ 실무에서의 좋은 전략 (새 프로젝트 시작 시)
- 무조건 Query Methods 먼저로 간단히 기능 구현
- 복잡해지는 지점부터 JPQL 또는 QueryDSL로 분리
- 성능이 중요한 핵심 쿼리는 필요 시 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 프로젝트를 만들어, 이 가이드에 나온 예제들을 하나씩 따라 치면서 몸에 익혀보세요.
'말귀 알아듣기.zip' 카테고리의 다른 글
| Spring Data JPA Query Methods 키워드 완전 정리 (메서드 이름으로 쿼리 만들기) (0) | 2026.04.19 |
|---|---|
| [JPA] JPQL 문법 완전정복 - 실전 예제 30가지 총정리 (1) | 2026.04.19 |
| Spring Boot 소스코드 분석 방법 총정리 | GitHub 저장소 구조, AutoConfiguration, 학습 로드맵 (0) | 2026.04.19 |
| Spring Boot GitHub 저장소 완벽 가이드: 초보자도 5단계로 끝내는 코드 읽기 (0) | 2026.04.19 |
| PRD(제품 요구사항 문서) 작성법과 템플릿 총정리 (0) | 2026.04.03 |