데일리 CS 학습
더보기
- Q. HTTP와 HTTPS의 차이점을 설명해주세요.
- A. HTTP는 데이터를 평문으로 전송하고, HTTPS는 SSL/TLS 암호화를 적용하여 보안을 강화합니다. HTTPS는 도청,변조,위장을 방지하며 현대 웹 서비스의 기본입니다.
- Q. TCP와 UDP의 차이점을 설명해주세요
- A. TCP는 연결 지향형으로 3-way handshake를 통한 연결, 신뢰성 보장, 순서 보장을 제공합니다. UPD는 비연결형으로 빠르지만 신뢰성이 낮습니다.
- Q. 쿠키와 세션의 차이점을 설명해주세요.
- A. 쿠키는 클라이언트에 저장되며 만료 시간 설정 가능, 보안에 취약할 수 있습니다. 세션은 서버에 저장되어 상대적으로 안전하지만, 서버 메모리를 사용합니다.
- Q. JWP란 무엇이고 어떻게 사용되나요?
- A. Header.Paylode.Signature 구조의 토큰으로, 서버가 상태를 저장하지 않는 Stateless 인증에 사용됩니다. 로그인 성공 시 토큰 발급 후 클라이언트가 이후 요청마다 Authorization 헤더에 포함하여 전송합니다.
- Q. REST API 설계 시 고려해야 할 보안 사항은 무엇인가요?
- A. HTTPS 사용, 인증 인가 처리, 입력값 검증으로 SQL Injection.XSS 방지, 민감 정보를URL에 노출하지 않기, 적절한 HTTP 상태 코드 반환 등이 있습니다.
- Q. Git에서 브랜치 전략을 간략히 설명해주세요.
- A. main(운영), develop(개발 통합), feature(기능 개발), release(배포 준비), hotfix(긴급 수정) 브랜치를 분리하여 관리합니다. 팀 개발 시 충돌을 중ㄹ이고 배포 관리를 체계화합니다.
- Q. 프로세스와 스레드의 차이를 설명해주세요.
- A. 프로세스는 독립적인 메모리 공간을 가진 실행 단위입니다. 스레드는 프로세스 내에서 메모리를 공유하며 동작하는 실행 단위로, 생성 비용이 낮고 통신이 쉽지만 동기화 문제에 주의해야 합니다.
- Q. RESTful API에서 인증과 인가의 차이는 무엇인가요?
- A. 인증은 '누구인지' 확인, 인가는 '무엇을 할 수 있는지' 확인입니다. 인증 후 인가가 이루어집니다.
Today I Learned · 2026 · Spring Backend
JPQL & QueryDSL
객체지향 쿼리부터 동적 쿼리까지
JPQL 기본 문법, N+1 주의사항, QueryDSL 설정 및 동적 쿼리 — 실무 패턴으로 완전 정리
Java Spring Boot JPA JPQL QueryDSL 동적쿼리
📋 목차
01 JPQL이란? — SQL vs JPQL
JPQL(Java Persistence Query Language)은 테이블이 아닌 엔티티 객체를 대상으로 쿼리를 작성하는 객체지향 쿼리 언어입니다. SQL과 문법이 매우 유사하지만 핵심 차이가 있습니다.
| 구분 | SQL | JPQL |
|---|---|---|
| 대상 | 테이블, 컬럼 | 엔티티 클래스, 필드 |
| DB 종속성 | 특정 DB에 종속 | DB 독립적 (방언으로 변환) |
| 별칭(alias) | 선택 사항 | 필수 (from Member m) |
| 예시 | SELECT * FROM member WHERE age > 20 | select m from Member m where m.age > 20 |
JPQL은 엔티티 이름, 필드 이름 모두 대소문자를 구분합니다.
테이블명
테이블명
member가 아닌 엔티티 클래스명 Member를 사용해야 합니다. 반면 SELECT, FROM, WHERE 같은 JPQL 키워드는 대소문자 구분 없습니다.02 JPQL 기본 문법
기본 SELECT / WHERE / ORDER BY
// 기본 전체 조회
"select m from Member m"
// WHERE 조건
"select m from Member m where m.age > 20"
// AND / OR / BETWEEN / IN / LIKE
"select m from Member m where m.age between 20 and 30"
"select m from Member m where m.name like '%현%'"
"select m from Member m where m.status in ('ACTIVE', 'PENDING')"
// ORDER BY / 페이징
"select m from Member m order by m.name asc, m.age desc"
// IS NULL / IS NOT NULL
"select m from Member m where m.email is not null"JPQL집합 / 그룹핑
// 집계 함수 — COUNT, SUM, AVG, MAX, MIN
"select count(m) from Member m"
"select avg(m.age) from Member m"
// GROUP BY / HAVING
"select t.name, count(m) from Member m join m.team t group by t.name"
"select t.name, count(m) from Member m join m.team t group by t.name having count(m) >= 2"JPQLJOIN — 묵시적 vs 명시적
// ❌ 묵시적 조인 — 경로 표현식으로 자동 JOIN (사용 지양)
"select m from Member m where m.team.name = '개발팀'"
// → 내부적으로 JOIN 발생하지만 쿼리가 복잡해질수록 예측 어려움
// ✅ 명시적 조인 — 항상 권장
"select m from Member m join m.team t where t.name = '개발팀'"
// LEFT OUTER JOIN
"select m from Member m left join m.team t"
// fetch join — 연관 엔티티 한 번에 로딩 (N+1 해결)
"select m from Member m join fetch m.team"
"select distinct m from Member m join fetch m.orders"JPQL파라미터 바인딩
// ✅ 이름 기반 파라미터 (권장)
em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", "현수")
.getResultList();
// ❌ 위치 기반 파라미터 (지양 — 순서 변경 시 버그 위험)
em.createQuery("select m from Member m where m.name = ?1", Member.class)
.setParameter(1, "현수")
.getResultList();Java결과 조회 메서드
TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
// getResultList() — 결과 없으면 빈 컬렉션 반환 (NPE 없음)
List<Member> members = query.getResultList();
// getSingleResult() — 정확히 1건. 없거나 2건 이상이면 예외 발생
// NoResultException / NonUniqueResultException
Member member = query.getSingleResult();
// 페이징
List<Member> paged = em.createQuery("select m from Member m order by m.id", Member.class)
.setFirstResult(0) // offset
.setMaxResults(10) // limit
.getResultList();Java03 Spring Data JPA — @Query
Spring Data JPA에서는 Repository 인터페이스에 @Query를 붙여 JPQL을 직접 작성할 수 있습니다. 메서드 이름 쿼리의 한계를 벗어나 복잡한 쿼리를 표현할 때 사용합니다.
public interface MemberRepository extends JpaRepository<Member, Long> {
// 기본 @Query — JPQL
@Query("select m from Member m where m.name = :name and m.age >= :age")
List<Member> findByNameAndAgeGoe(@Param("name") String name, @Param("age") int age);
// nativeQuery = true — 네이티브 SQL 직접 사용
@Query(value = "SELECT * FROM member WHERE name = :name", nativeQuery = true)
List<Member> findByNameNative(@Param("name") String name);
// 컬렉션 파라미터 — IN절
@Query("select m from Member m where m.id in :ids")
List<Member> findByIds(@Param("ids") List<Long> ids);
// 페이징 + countQuery 분리 (fetch join 시 필수)
@Query(
value = "select m from Member m join fetch m.team t where t.name = :teamName",
countQuery = "select count(m) from Member m join m.team t where t.name = :teamName"
)
Page<Member> findByTeamName(@Param("teamName") String teamName, Pageable pageable);
// 수정 쿼리 — @Modifying 필수
@Modifying(clearAutomatically = true) // 실행 후 영속성 컨텍스트 자동 초기화
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
}Java⚠ @Modifying 없이 UPDATE/DELETE를 @Query로 실행하면 예외 발생
또한 벌크 연산은 영속성 컨텍스트를 거치지 않고 DB에 직접 실행되므로, 실행 후 영속성 컨텍스트에 캐시된 엔티티와 DB 값이 달라집니다.
또한 벌크 연산은 영속성 컨텍스트를 거치지 않고 DB에 직접 실행되므로, 실행 후 영속성 컨텍스트에 캐시된 엔티티와 DB 값이 달라집니다.
clearAutomatically = true를 반드시 설정하거나, 벌크 연산 후 em.clear()를 호출해야 합니다.04 JPQL의 한계 — 동적 쿼리 문제
JPQL은 문자열로 쿼리를 작성하기 때문에 조건에 따라 쿼리를 동적으로 변경하기 매우 어렵습니다.
// ❌ JPQL로 동적 쿼리 작성 — 끔찍한 문자열 조합
public List<Member> search(String name, Integer age, String status) {
String jpql = "select m from Member m where 1=1";
if (name != null) jpql += " and m.name = :name";
if (age != null) jpql += " and m.age >= :age";
if (status != null) jpql += " and m.status = :status";
TypedQuery<Member> query = em.createQuery(jpql, Member.class);
if (name != null) query.setParameter("name", name);
if (age != null) query.setParameter("age", age);
if (status != null) query.setParameter("status", status);
return query.getResultList();
// → 문자열 오타가 런타임에서야 발견됨. 컴파일 타임 오류 감지 불가!
}JavaJPQL 한계
- 문자열이라 오타를 컴파일 타임에 못 잡음
- 동적 쿼리 작성 시 문자열 조합으로 가독성 최악
- IDE 자동완성 지원 없음
- 리팩토링 시 엔티티 필드명 변경 반영 어려움
QueryDSL 장점
- 자바 코드로 쿼리 작성 → 컴파일 타임 오류 감지
- BooleanExpression으로 동적 쿼리 깔끔하게 작성
- IDE 자동완성 완벽 지원
- 엔티티 필드 변경 시 Q클래스 재생성으로 자동 반영
05 QueryDSL — 설정과 Q클래스
QueryDSL은 자바 코드로 타입 안전한 쿼리를 작성할 수 있는 프레임워크입니다. 빌드 시점에 엔티티 기반으로 Q클래스를 자동 생성하고, 이를 통해 쿼리를 작성합니다.
의존성 설정 (Spring Boot 3.x / Gradle)
// build.gradle (Spring Boot 3.x — jakarta 기반)
dependencies {
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
}GradleJPAQueryFactory 빈 등록
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}JavaQ클래스란?
빌드 시 @Entity 클래스를 기반으로 자동 생성되는 메타 클래스입니다. 엔티티의 모든 필드를 타입 안전하게 참조할 수 있습니다.
// Member 엔티티에서 자동 생성된 QMember 클래스 (개념적 표현)
public class QMember extends EntityPathBase<Member> {
public static final QMember member = new QMember("member");
public final NumberPath<Long> id = createNumber("id", Long.class);
public final StringPath name = createString("name");
public final NumberPath<Integer> age = createNumber("age", Integer.class);
public final StringPath status = createString("status");
}
// 사용 시 static import로 편하게
import static com.example.entity.QMember.member;
import static com.example.entity.QOrder.order;Java06 QueryDSL 기본 사용법
Repository 구조 — Custom Repository 패턴
// 1단계 — Custom 인터페이스 정의
public interface MemberRepositoryCustom {
List<MemberDto> search(MemberSearchCondition condition);
Page<MemberDto> searchPage(MemberSearchCondition condition, Pageable pageable);
}
// 2단계 — QueryDSL 구현체
@Repository
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public List<MemberDto> search(MemberSearchCondition cond) {
return queryFactory
.select(Projections.constructor(
MemberDto.class,
member.id, member.name, member.age
))
.from(member)
.where(
nameEq(cond.getName()),
ageGoe(cond.getAgeGoe()),
statusEq(cond.getStatus())
)
.fetch();
}
}
// 3단계 — JpaRepository에 Custom 인터페이스 상속
public interface MemberRepository
extends JpaRepository<Member, Long>, MemberRepositoryCustom { }Java기본 CRUD 쿼리
// 단건 조회 — fetchOne() : 없으면 null, 2건 이상이면 예외
Member found = queryFactory
.selectFrom(member)
.where(member.name.eq("현수"))
.fetchOne();
// 단건 — fetchFirst() : 없으면 null (LIMIT 1)
Member first = queryFactory
.selectFrom(member)
.where(member.age.goe(20))
.fetchFirst();
// 목록 조회
List<Member> members = queryFactory
.selectFrom(member)
.where(member.status.eq("ACTIVE"))
.orderBy(member.name.asc(), member.age.desc())
.fetch();
// JOIN
List<Member> joined = queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("개발팀"))
.fetch();
// fetch join
List<Member> fetched = queryFactory
.selectFrom(member)
.join(member.team, team).fetchJoin()
.fetch();
// 벌크 수정
long count = queryFactory
.update(member)
.set(member.status, "INACTIVE")
.where(member.age.lt(18))
.execute();Java07 동적 쿼리 — BooleanExpression 패턴
QueryDSL의 핵심 강점입니다. BooleanExpression을 반환하는 메서드를 조합해 조건에 따른 동적 쿼리를 null-safe하게 작성할 수 있습니다.
핵심 원리
where()에 null이 전달되면 QueryDSL이 해당 조건을 자동으로 무시합니다. 따라서 조건 메서드가 null을 반환하면 그 조건은 쿼리에 포함되지 않습니다.@Repository
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final JPAQueryFactory queryFactory;
public List<MemberDto> search(MemberSearchCondition cond) {
return queryFactory
.select(Projections.constructor(MemberDto.class,
member.id, member.name, member.age, team.name))
.from(member)
.leftJoin(member.team, team)
.where(
nameEq(cond.getName()), // null이면 조건 무시
ageGoe(cond.getAgeGoe()), // null이면 조건 무시
ageLoe(cond.getAgeLoe()), // null이면 조건 무시
teamNameEq(cond.getTeamName()) // null이면 조건 무시
)
.fetch();
}
// BooleanExpression 메서드 — null 반환 시 조건 자동 제외
private BooleanExpression nameEq(String name) {
return name != null ? member.name.eq(name) : null;
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe != null ? member.age.goe(ageGoe) : null;
}
private BooleanExpression ageLoe(Integer ageLoe) {
return ageLoe != null ? member.age.loe(ageLoe) : null;
}
private BooleanExpression teamNameEq(String teamName) {
return teamName != null ? team.name.eq(teamName) : null;
}
// BooleanExpression 조합 — and() / or()로 재사용 가능
private BooleanExpression ageBetween(Integer goe, Integer loe) {
return ageGoe(goe).and(ageLoe(loe)); // 조건 메서드 재사용 조합
}
}Java✅ BooleanExpression의 진가 — 조합과 재사용
BooleanExpression은 .and(), .or()로 조합이 가능합니다. 각 조건 메서드를 단독으로도, 조합해서도 재사용할 수 있어 코드 중복을 크게 줄여줍니다. BooleanBuilder보다 가독성이 좋고 null-safe하여 실무에서 표준 패턴으로 사용됩니다.08 DTO 직접 조회 — Projections
엔티티 전체를 조회한 후 DTO로 변환하는 방식보다, 처음부터 필요한 필드만 DTO로 조회하면 불필요한 컬럼 조회와 dirty checking 오버헤드를 모두 없앨 수 있습니다.
// DTO 클래스
@Data
@AllArgsConstructor
public class MemberDto {
private Long id;
private String name;
private int age;
private String teamName;
}
// 방법 1 — Projections.constructor() : 생성자 기반
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class,
member.id,
member.name,
member.age,
team.name
))
.from(member)
.leftJoin(member.team, team)
.fetch();
// 방법 2 — @QueryProjection : 가장 타입 안전 (권장)
@Data
public class MemberDto {
private Long id;
private String name;
private int age;
@QueryProjection // 이 어노테이션으로 QMemberDto 자동 생성
public MemberDto(Long id, String name, int age) {
this.id = id; this.name = name; this.age = age;
}
}
// QMemberDto 사용 — 컴파일 타임에 타입 체크
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.id, member.name, member.age))
.from(member)
.fetch();Java| 방법 | 타입 안전 | 특징 |
|---|---|---|
Projections.bean() |
△ 보통 | setter 기반. 필드명이 일치해야 함 |
Projections.fields() |
△ 보통 | 필드 직접 접근. 필드명이 일치해야 함 |
Projections.constructor() |
△ 보통 | 생성자 기반. 타입은 맞춰야 함. 순서 오류는 런타임에서 발견 |
@QueryProjection |
✅ 완전 | 컴파일 타임 타입 체크. 단, DTO가 QueryDSL에 의존 |
09 페이징 처리
public Page<MemberDto> searchPage(MemberSearchCondition cond, Pageable pageable) {
// 콘텐츠 쿼리 — 실제 데이터
List<MemberDto> content = queryFactory
.select(new QMemberDto(member.id, member.name, member.age))
.from(member)
.leftJoin(member.team, team)
.where(nameEq(cond.getName()), ageGoe(cond.getAgeGoe()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
// 카운트 쿼리 — JOIN 없이 단순하게 (성능 최적화)
JPAQuery<Long> countQuery = queryFactory
.select(member.count())
.from(member)
.where(nameEq(cond.getName()), ageGoe(cond.getAgeGoe()));
// PageableExecutionUtils — 마지막 페이지면 카운트 쿼리 생략
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
}Java✅ PageableExecutionUtils 사용 이유
PageableExecutionUtils.getPage()는 마지막 페이지이거나 콘텐츠 수가 페이지 사이즈보다 작으면 카운트 쿼리를 실행하지 않습니다. 불필요한 COUNT 쿼리를 줄여 성능을 최적화할 수 있어 실무에서 표준으로 사용됩니다.10 면접 예상 질문
Q1. JPQL과 SQL의 차이점을 설명하세요.
SQL은 DB 테이블과 컬럼을 대상으로 쿼리를 작성하고 특정 DB에 종속됩니다. 반면 JPQL은 엔티티 클래스와 필드를 대상으로 쿼리를 작성하며 DB 독립적입니다. Hibernate가 방언(Dialect)을 통해 실행 시 각 DB에 맞는 SQL로 변환합니다. 테이블명 대신 엔티티 클래스명을, 컬럼명 대신 필드명을 사용하며 엔티티 별칭이 필수입니다.
Q2. JPQL의 단점과 QueryDSL을 사용하는 이유는?
JPQL은 문자열로 작성하기 때문에 오타가 런타임에서야 발견되고, 조건에 따라 쿼리를 동적으로 변경하려면 문자열을 직접 조합해야 해서 가독성이 나쁩니다. QueryDSL은 자바 코드로 쿼리를 작성하므로 컴파일 타임에 오류를 잡을 수 있고, IDE 자동완성을 지원합니다. 또한
BooleanExpression 패턴으로 동적 쿼리를 깔끔하게 작성할 수 있고, 조건 메서드를 재사용·조합할 수 있습니다.Q3. @Modifying을 사용할 때 주의할 점은?
벌크 연산(
UPDATE, DELETE)은 영속성 컨텍스트를 거치지 않고 DB에 직접 실행됩니다. 따라서 실행 후 영속성 컨텍스트에 캐시된 엔티티와 DB 값이 달라집니다. clearAutomatically = true를 설정하거나, 벌크 연산 직후 em.clear()를 호출해 영속성 컨텍스트를 초기화해야 이후 조회에서 정확한 데이터를 얻을 수 있습니다.Q4. QueryDSL에서 동적 쿼리를 어떻게 작성하나요?
BooleanExpression을 반환하는 메서드를 별도로 정의하고 where()에 조합합니다. 값이 null이면 null을 반환하고, QueryDSL은 where 조건에 null이 전달되면 해당 조건을 자동으로 무시합니다. 이 방식은 조건 메서드를 재사용·조합할 수 있고 가독성이 좋아 실무 표준 패턴입니다. BooleanBuilder를 사용하는 방법도 있지만 가독성이 떨어져 BooleanExpression을 권장합니다.Q5. DTO를 QueryDSL로 조회하는 방법과 각각의 차이점은?
세 가지 방법이 있습니다.
Projections.bean()은 setter 기반, Projections.constructor()는 생성자 기반으로 타입은 맞춰야 하지만 순서 오류는 런타임에서 발견됩니다. @QueryProjection은 DTO 생성자에 어노테이션을 붙여 QDto 클래스를 생성하는 방식으로 컴파일 타임에 완전한 타입 체크가 가능합니다. 단, DTO가 QueryDSL에 의존하게 된다는 단점이 있습니다.핵심 요약
- JPQL — 엔티티 대상 객체지향 쿼리. 문자열이라 오타 런타임 발견, 동적 쿼리 작성 불편
- @Query — JPQL 직접 작성. 수정 쿼리는 @Modifying 필수, clearAutomatically = true 설정
- 묵시적 JOIN 지양 — 예측하기 어려워 항상 명시적 join 사용
- QueryDSL — 자바 코드로 타입 안전 쿼리. 컴파일 타임 오류 감지, IDE 자동완성
- BooleanExpression — null 반환 시 조건 자동 제외. 동적 쿼리 실무 표준 패턴, 조합·재사용 가능
- DTO 직접 조회 — Projections.constructor() 또는 @QueryProjection. 불필요한 컬럼 조회 제거
- 페이징 — PageableExecutionUtils로 마지막 페이지 카운트 쿼리 생략 최적화
오늘의 핵심 — JPQL은 기본 조회에, QueryDSL은 동적 쿼리와 복잡한 조회에. BooleanExpression 패턴을 익히면 동적 쿼리 걱정은 끝.
728x90
'스프링' 카테고리의 다른 글
| Spring MVC 동작 원리 (0) | 2026.04.22 |
|---|---|
| 예외 처리 전략 @ControllerAdvice · @ExceptionHandler · 공통 에러 응답 설계 (1) | 2026.04.20 |
| @Transactional 동작 원리 / AOP 프록시 · self-invocation · readOnly 최적화 (0) | 2026.04.16 |
| @Transactional 동작 원리AOP 프록시 · self-invocation · readOnly 최적화 (0) | 2026.04.15 |
| JPA N+1 문제발생 원인과 3가지 해결 전략 (0) | 2026.04.13 |