챕터 1-1 : QueryDSL 개념 및 적용
1. JPQL의 한계
기존 JPA에서 사용하는 JPQL은 문자열 기반 쿼리 언어

2. QueryDSL의 등장
QueryDSL은 JPQL을 타입 안전하게 작성하기 위한 라이브러리
핵심 개념
1.JPQL 빌더이자 도메인 전용 언어(DSL)
2.SQL이 아닌, JPA 위에서 동작하는 쿼리 빌더
장점 요약
1.IDE 자동완성 지원
2.컴파일 타임 검증
3.동적 쿼리 작성 간결화
4.코드 가독성 및 유지보수성 향상
3. QueryDSL 설정
build.gradle 설정
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"
QuerydslConfig 설정
@Configuration
public class QuerydslConfig {
@PersistenceContext
private EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}
4. Q-Type 이해하기
QueryDSL은 빌드 시 자동으로 Q타입 클래스 (querydsl 전용 클래스)를 생성
User 엔티티
@Entity
public class User {
@Id @GeneratedValue
private Long id;
private String username;
private int age;
}
생성된 QUser
@Generated("com.querydsl.codegen.EntitySerializer")
public class QUser extends EntityPathBase<User> {
public static final QUser user = new QUser("user");
public final StringPath username = createString("username");
public final NumberPath<Integer> age = createNumber("age", Integer.cla
ss);
}
build.gradle 설정과 QuerydslConfig 설정을 잘 했다면 Q타입 클래스는 자동으로 생성된다
Q클래스 생성 위치 확인
build → classes → java.main.Entity를 생성한 패키지 위치 → QClass
아래 작성은 공식문서에 나온데로 작성을 한거라 다 외울 필요는 없다.


Q클래스가 자동으로 생성 됨

4. 기본 쿼리 예제[실습]
https://github.com/MinWoo1995/spring-plus
GitHub - MinWoo1995/spring-plus
Contribute to MinWoo1995/spring-plus development by creating an account on GitHub.
github.com
챕터 1-2 : QueryDSL 검색 기능 구현
1. 개념 설명
기본 구조 이해
Querydsl 구조
queryFactory
.select(조회대상)
.from(대상엔티티)
.where(조건)
.orderBy(정렬)
.fetch()
타입(Q-Type) 활용법
QueryDSL은 컴파일 시점에 Q타입 클래스를 자동 생성
public class QUser extends EntityPathBase<User> {
public static final QUser user = new QUser("user");
public final StringPath username = createString("username");
public final StringPath email = createString("email");
public final EnumPath<UserRoleEnum> roleEnum = createEnum("roleEnu
m", UserRoleEnum.class);
}
QUser.user 를 통해 username , email , roleEnum 등 필드에 안전하게 접근 가능
IDE 자동완성 + 컴파일 시점 검증으로 런타임 오류 예방
기본 검색 쿼리 작성 예시
목표: NORMAL 사용자 중 gmail.com 이메일을 가진 사용자 조회
List<User> result = queryFactory
.selectFrom(user)
.where(
user.roleEnum.eq(UserRoleEnum.NORMAL),
user.email.endsWith("gmail.com")
)
.fetch();

fetch() 는 결과 리스트를 반환하며,
fetchOne() , fetchFirst() 로 단건 조회도 가능(단, 결과가 둘 이상이면 예외 발생)
정렬
목표: 사용자 이름 오름차순, ID 내림차순 정렬 후 3명만 조회
List<User> result = queryFactory
.selectFro
.orderBy(user.username.asc(), user.id.desc())
.limit(3)
.fetch
결과 예시
관리자
밥
앨리스
offset() 은 시작 위치, limit() 은 개수
Spring Data의 Pageable 과도 함께 사용 가능
https://github.com/MinWoo1995/spring-plus
GitHub - MinWoo1995/spring-plus
Contribute to MinWoo1995/spring-plus development by creating an account on GitHub.
github.com
2. 다양한 검색 예제 (실제 데이터 기반)
문자열 검색
목표: “여행” 키워드가 포함된 게시글(Post) 조회
List<Post> result = queryFactory
.selectFrom(post)
.where(post.content.contains("여행"))// content 내용에 "여행"이 포함
.fetch();
contains() 는 SQL의 LIKE '%keyword%' 과 동일한 동작
논리 조합
목표: ADMIN 사용자 또는 이름에 “밥”이 포함된 사용자 조회
List<User> result = queryFactory
.selectFrom(user)
.where(
user.roleEnum.eq(UserRoleEnum.ADMIN)
.or(user.username.contains("밥"))
)
.fetch();
조인 검색
목표: “앨리스”가 작성한 게시글(Post) 조회
List<Post> result = queryFactory
.selectFrom(post)
.join(post.user, user)
.where(user.username.eq("앨리스"))
.fetch();
join(post.user, user) 는 실제 SQL의 INNER JOIN과 동일하게 동작
Fetch Join
목표: 게시글과 작성자 정보를 한 번에 로딩
List<Post> result = queryFactory
.selectFrom(post)
.join(post.user, user).fetchJoin()
.fetch();
fetchJoin()은 N+1 문제를 방지하며, Post 조회 시 User까지 한 번에 조회
댓글(Comment) 검색
목표: "리제로 3기 감상평" 게시글의 모든 댓글 조회
List<Comment> comments = queryFactory
.selectFrom(comment)
.join(comment.post, post)
.where(post.content.eq("리제로 3기 감상평"))
.fetch();
페이징 실습
목표: 게시글 목록을 5개씩 조회 (2페이지: 6~10번 게시글)
List<Post> page2 = queryFactory
.selectFrom(post)
.orderBy(post.id.asc())
.offset(5)
.limit(5)
.fetc
챕터 1-3 : QueryDSL 검색 기능 구현 (고도화)
1. 동적 쿼리
동적 쿼리(Dynamic Query)란?
사용자가 입력한 검색 조건에 따라 쿼리를 유연하게 생성하는 방식
예시)
이름만 입력하면 이름으로 검색
이메일만 입력하면 이메일로 검색
둘 다 입력하면 둘 다 조건으로 검색
이런 형태를 지원하기 위해 QueryDSL에서는 BooleanBuilder 나 BooleanExpression 조건을 활용
2. 동적 쿼리가 필요한 이유
기존 방식의 한계
Spring Data JPA의 기본 메서드만으로는 아래 같은 검색을 표현하기 어렵다
예시)
사용자 이름(username)이 “앨리스”인 사람
또는 이메일(email)에 “gmail”이 포함된 사람
그리고 역할(role)이 NORMAL인 경우
기존 방식 (메서드 이름 기반)
List<User> findByUsernameAndEmailContainsAndRoleEnum(
String username, String email, UserRoleEnum roleEnum
);
조건이 조금만 바뀌어도 메서드가 폭발적으로 늘어남
→ findByUsernameAndRoleEnum ,
→ findByEmailContains ,
→ findByUsernameOrEmailContainsAndRoleEnum
유지보수가 불가능
기존 방식 (if문 조합)
if (username != null && email != null) {
return userRepository.findByUsernameAndEmail(username, email);
} else if (username != null) {
return userRepository.findByUsername(username);
} else if (email != null) {
return userRepository.findByEmail(email);
}
코드가 중복되고, 분기문이 늘어남.
조건이 늘어날수록 복잡도가 기하급수적으로 증가.
수정할 때마다 쿼리 로직을 직접 손대야 함.
3. 단일 책임 원칙과 동적 쿼리
동적 쿼리는 여러 조건을 처리하니까 SRP(단일 책임 원칙)에 어긋나는 거 아닌가
->단일 책임 원칙”이란 ‘하나의 클래스(또는 함수)가 한 가지 이유로만 변경되어야 한다’는 의미
동적 쿼리는 ‘검색 조건이 다양하다’는 하나의 책임(검색 기능 제공) 을 수행하는 것이지,서로 다른 비즈니스 로직을 섞는 게 아니
회원 검색 이라는 하나의 목적 안에서
다양한 입력 조건을 처리하는 것일 뿐
4. BooleanBuilder vs BooleanExpression
QueryDSL에서 동적 쿼리를 작성할 때 사용하는 대표적인 두 가지 방법입니다.
둘 다 조건(where 절)을 표현하지만 역할과 사용 시점이 다르다
BooleanBuilder란?
여러 조건을 동적으로 추가하거나 제거할 수 있는 “가변 조건 컨테이너” 입니다.
-> 조건을 하나씩 쌓아가며 만드는 방식으로, 복잡한 검색 조건에 유용
BooleanBuilder builder = new BooleanBuilder();
if (username != null) {
builder.and(user.username.contains(username));
}
if (email != null) {
builder.and(user.email.contains(email));
}
if (role != null) {
builder.and(user.roleEnum.eq(role));
}
List<User> result = queryFactory
.selectFrom(user)
.where(builder)
.fetch();

BooleanExpression이란?
하나의 조건(표현식)을 메서드로 정의해 조합하는 불변 표현식입니다.
-> null 조건은 QueryDSL이 자동으로 무시하므로 코드가 깔끔
private BooleanExpression usernameContains(String username) {
return username != null ? user.username.contains(username) : null;
}
private BooleanExpression emailContains(String email) {
return email != null ? user.email.contains(email) : null;
}
private BooleanExpression roleEq(UserRoleEnum role) {
return role != null ? user.roleEnum.eq(role) : null;
}
List<User> result = queryFactory
.selectFrom(user)
.where(
usernameContains(cond.getUsername()),
emailContains(cond.getEmail()),
roleEq(cond.getRole())
)
.fetch();


5. 동적쿼리 생성 실습
https://github.com/MinWoo1995/nbcam-plus-1-3
GitHub - MinWoo1995/nbcam-plus-1-3
Contribute to MinWoo1995/nbcam-plus-1-3 development by creating an account on GitHub.
github.com
챕터 1-4 : QueryDSL과 엔티티 연관관계 관리
1. 동적 쿼리 JPA에서 연관관계를 사용하는 이유
JPA는 “객체와 테이블을 1:1로 매핑”하는 ORM
->즉, 객체 그래프 탐색이 가능하도록 엔티티 간 연결이 필요
@Entity
public class Post {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
}
이렇게 하면 아래 코드가 가능
post.getUser().getUsername();
개발자가 SQL을 직접 작성하지 않아도,
객체 참조( post.user.username )만으로 관계를 탐색
연관관계는 쿼리를 자동으로 만들어주는 편리함 때문에 필요
하지만, 이 편리함이 바로 JPA의 가장 큰 단점
2. JPA 연관관계가 만들어내는 문제들

List<Post> posts = postRepository.findAll();
for (Post post : posts) {
System.out.println(post.getUser().getUsername());
}
실제 실행 SQL:
SELECT * FROM post;
SELECT * FROM user WHERE id = 1;
SELECT * FROM user WHERE id = 2;
SELECT * FROM user WHERE id = 3;
한 번의 조회(1) + 연관 데이터 N개 → N+1 문제 발생
3. JPA의 “불편하지만 써야 하는” 이유
그럼 왜 이런 불편함에도 연관관계를 쓸까?
JPA의 장점(객체 그래프 관리)”을 쓰기 위해 연관관계라는 불편한 장치를 감수해야 했던 거다

4. QueryDSL의 등장 — 쿼리를 다시 제어하자
QueryDSL은 “타입 안전한 SQL Builder”이다
즉, ORM의 내부 쿼리를 신뢰하지 않고 개발자가 직접 SQL을 조립 가능
List<Post> result = queryFactory
.selectFrom(post)
.join(post.user, user)
.where(user.username.eq("김동현"))
.fetch();
JPA가 자동으로 만들어주던 쿼리를 개발자가 명시적으로 정의할 수 있게되었다
5. ID 기반 설계 — 연관관계 없는 엔티티
이제 실무에서는 객체 간의 관계를 굳이 매핑하지 않는다.
대신 단순한 외래키 필드(ID) 만 저장
기존 방식
@Entity
public class Post {
@ManyToOne(fetch = FetchType.LAZY)
private User user;
}
실무형 방식
@Entity
public class Post {
private Long userId; // 연관관계 대신 ID만 저장
}
연관관계 제거 후, QueryDSL로 조인 시점에만 연결
6. QueryDSL로 명시적 조인
이제 Post와 User를 이렇게 조인
List<PostResponse> result = queryFactory
.select(Projections.constructor(PostResponse.class,
post.content,
user.username))
.from(post)
.leftJoin(user).on(post.userId.eq(user.id)) // ID 기반 Join
.fetch();
“관계는 코드가 아니라 쿼리로 관리”하는 구조로 바뀐 것
7. 이렇게 바꾸면 생기는 변화

8. 결과적으로, 실무의 흐름은 이렇게 변했다
[JPA 초기]
객체 중심 설계 → 연관관계 사용 (@OneToMany 등)
↓
[N+1, Lazy 문제 발생]
fetch join, batch size로 임시 해결
↓
[QueryDSL 도입]
개발자가 쿼리 직접 제어 가능
↓
[현재 실무 트렌드]
엔티티 간 연관관계 제거 → ID 기반 + 명시적 join(QueryDSL)
연관이 필요 없는것은 JPA가, 조인이 필요한 것은 QueryDSL
9. 한 줄 요약
JPA는 객체 그래프 탐색을 위해 연관관계를 사용했지만,
QueryDSL이 등장하면서
개발자가 직접 Join을 제어할 수 있게 되었다.
따라서 실무에서는 연관관계를 제거하고
ID 기반 설계 + 명시적 Join(QueryDSL) 으로 전환
불편한 것이 생기고 이를 개선하기 위해서 새로운 기술이 나오는 자연적인 흐름으로 진화한것
| 구분 | 연관 관계 매핑 (Entity) | ID 직접 참조 (Long) |
| 핵심 철학 | 객체지향 (Object Graph) | 데이터 독립성 (Decoupling) |
| 조회 방식 | review.getMember() | memberRepo.findById(id) |
| 성능 이슈 | Fetch 전략(Lazy/Eager), N+1 | 잦은 SELECT 호출 |
| 적합한 규모 | 단일 DB 기반의 모놀리식 아키텍처 | 대규모 시스템, MSA, 분산 DB |
ID 직접 참조를 사용하게 된다면 null값에 대한 대비에 신경써야 한다.
10. 연관관계 끊기 실습
https://github.com/MinWoo1995/plus-chapter1-4
GitHub - MinWoo1995/plus-chapter1-4
Contribute to MinWoo1995/plus-chapter1-4 development by creating an account on GitHub.
github.com
'spring_2기[본캠프] > 과제' 카테고리의 다른 글
| [과제] Spring 플러스 프로젝트 Day 1 (0) | 2026.03.05 |
|---|---|
| [과제]CH 5 플러스 Spring 과제 (0) | 2026.03.03 |
| [과제] Standard Spring Task 5 (0) | 2026.02.24 |
| [과제] 결제 시스템 Day9 (0) | 2026.02.20 |
| [과제] 결제 시스템 Day8 (0) | 2026.02.19 |