[20250214] querydsl ๊ธฐ๋ณธ ์์๋ก ์ฌ์ฉ๋ฐฉ๋ฒ ์์๋ณด๊ธฐ!
Querydsl ์์ ์ ๋ณต ๊ฐ์ด๋: ์ด๋ณด์๋ ์ดํดํ๋ ํต์ฌ ๊ฐ๋ ๊ณผ ํ์ฉ๋ฒ
Querydsl์ ๋ณต์กํ SQL ์ฟผ๋ฆฌ๋ฅผ ์๋ฐ ์ฝ๋๋ก ์ฝ๊ณ ๊ฐ๊ฒฐํ๊ฒ ์์ฑํ ์ ์๋๋ก ๋์์ฃผ๋ ์คํ์์ค ํ๋ ์์ํฌ์ ๋๋ค. ๋ง์น SQL์ ์๋ฐ ์ธ์ด์ฒ๋ผ ๋ค๋ฃจ์ด ํ๋ก๊ทธ๋๋ฐ์ ์ธ ์ ๊ทผ์ด ๊ฐ๋ฅํ๊ฒ ํด์ค๋๋ค. ๋ณต์กํ ์ฟผ๋ฆฌ ์์ฑ์ ์ด๋ ค์์ ํด๊ฒฐํ๊ณ ์์ฐ์ฑ์ ๋์ฌ์ฃผ๊ธฐ ๋๋ฌธ์ ๋ง์ ๊ฐ๋ฐ์๋ค์ด ์ ์ฉํ๊ณ ์์ต๋๋ค.
1. Querydsl, ์ ์ฌ์ฉํ ๊น์?
- ๊ฐ๋ ์ฑ ํฅ์: SQL ์ฟผ๋ฆฌ๋ฅผ ๋ฌธ์์ด๋ก ์์ฑํ๋ ๋์ ์๋ฐ ์ฝ๋๋ก ํํํ์ฌ ๊ฐ๋ ์ฑ์ ๋์ด๊ณ ์ ์ง๋ณด์๋ฅผ ์ฉ์ดํ๊ฒ ํฉ๋๋ค.
- ์์ฐ์ฑ ํฅ์: ๋ณต์กํ ์ฟผ๋ฆฌ๋ฅผ ๊ฐ๋จํ๊ฒ ์์ฑํ ์ ์์ด ๊ฐ๋ฐ ์๊ฐ์ ๋จ์ถ์์ผ์ค๋๋ค.
- ํ์ ์์ ์ฑ: ์ปดํ์ผ ์์ ์ ์ค๋ฅ๋ฅผ ๋ฐ๊ฒฌํ์ฌ ๋ฐํ์ ์ค๋ฅ๋ฅผ ์ค์ฌ์ค๋๋ค.
- ์ ์ฐ์ฑ: ๋ค์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ORM ํ๋ ์์ํฌ๋ฅผ ์ง์ํ์ฌ ํ์ฉ ๋ฒ์๋ฅผ ๋ํ์ค๋๋ค.
2. Querydsl ํต์ฌ ๊ฐ๋ ํํค์น๊ธฐ
- Q-Type: Querydsl์ ์ํฐํฐ ํด๋์ค๋ฅผ ๊ธฐ๋ฐ์ผ๋ก Q-Type์ด๋ผ๋ ์ฟผ๋ฆฌ ํ์ ์ ์์ฑํฉ๋๋ค. ์ด Q-Type์ ์ฌ์ฉํ์ฌ ์ฟผ๋ฆฌ ์์ฑ ์ ํ๋๋ช ์ด๋ ์ํฐํฐ๋ช ์ ์ง์ ์ฐธ์กฐํ ์ ์์ต๋๋ค.
- JPA QL: Querydsl์ JPA QL์ด๋ผ๋ JPQL ๊ธฐ๋ฐ์ ์ฟผ๋ฆฌ ์ธ์ด๋ฅผ ์ฌ์ฉํฉ๋๋ค. JPA QL์ SQL๊ณผ ์ ์ฌํ๋ฉด์๋ ์ํฐํฐ ์ค์ฌ์ ์ฟผ๋ฆฌ ์์ฑ์ ์ง์ํฉ๋๋ค.
- Predicate: Querydsl์์ where ์ ์ ์กฐ๊ฑด์ ๋ํ๋ด๋ ์ธํฐํ์ด์ค์ ๋๋ค. Predicate๋ฅผ ์ฌ์ฉํ์ฌ ๋ค์ํ ์กฐ๊ฑด์ ์กฐํฉํ๊ณ ์ฟผ๋ฆฌ์ ์ ์ฉํ ์ ์์ต๋๋ค.
- Querydsl API: Querydsl์ ๋ค์ํ API๋ฅผ ์ ๊ณตํ์ฌ ์ฟผ๋ฆฌ ์์ฑ, ์คํ, ๊ฒฐ๊ณผ ์ฒ๋ฆฌ ๋ฑ์ ์ง์ํฉ๋๋ค.
3. Querydsl, ์ด๋ ๊ฒ ์ฌ์ฉํด์!
- Querydsl ์์กด์ฑ ์ถ๊ฐ: ํ๋ก์ ํธ์ Querydsl ์์กด์ฑ์ ์ถ๊ฐํฉ๋๋ค.
- Q-Type ์์ฑ: ์ํฐํฐ ํด๋์ค๋ฅผ ๊ธฐ๋ฐ์ผ๋ก Q-Type์ ์์ฑํฉ๋๋ค.
- Querydsl ์ธ์คํด์ค ์์ฑ: ์ฟผ๋ฆฌ ์คํ์ ์ํ Querydsl ์ธ์คํด์ค๋ฅผ ์์ฑํฉ๋๋ค.
- ์ฟผ๋ฆฌ ์์ฑ: Q-Type๊ณผ Querydsl API๋ฅผ ์ฌ์ฉํ์ฌ ์ํ๋ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํฉ๋๋ค.
- ์ฟผ๋ฆฌ ์คํ: ์์ฑ๋ ์ฟผ๋ฆฌ๋ฅผ ์คํํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํ๋ฐ์ต๋๋ค.
Querydsl ๊ธฐ๋ณธ ์์: ๊ฐ๋จํ ํ์ ๊ด๋ฆฌ ๊ธฐ๋ฅ
Querydsl์ ์ฌ์ฉํ์ฌ ๊ฐ๋จํ ํ์ ๊ด๋ฆฌ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ์์๋ฅผ ํตํด ๊ธฐ๋ณธ์ ์ธ ์ฌ์ฉ๋ฒ์ ์ตํ๋ณด๊ฒ ์ต๋๋ค.
1. ํ๋ก์ ํธ ์ค์
- Spring Boot ํ๋ก์ ํธ๋ฅผ ์์ฑํ๊ณ Querydsl ์์กด์ฑ์ ์ถ๊ฐํฉ๋๋ค.
- ํ์ ์ํฐํฐ(Member)๋ฅผ ์์ฑํฉ๋๋ค.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
// ... getter, setter ๋ฑ
}
2. Q-Type ์์ฑ
- ์ํฐํฐ ํด๋์ค๋ฅผ ๊ธฐ๋ฐ์ผ๋ก Q-Type์ ์์ฑํฉ๋๋ค. (IntelliJ IDEA ๋ฑ IDE์์ ์ง์)
3. Querydsl ์ธ์คํด์ค ์์ฑ
- JPAQueryFactory๋ฅผ ์ฌ์ฉํ์ฌ Querydsl ์ธ์คํด์ค๋ฅผ ์์ฑํฉ๋๋ค.
@PersistenceContext
private EntityManager entityManager;
private JPAQueryFactory queryFactory;
@BeforeEach
public void setUp() {
queryFactory = new JPAQueryFactory(entityManager);
}
4. ์ฟผ๋ฆฌ ์์ฑ ๋ฐ ์คํ
- Q-Type๊ณผ Querydsl API๋ฅผ ์ฌ์ฉํ์ฌ ์ํ๋ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ๊ณ ์คํํฉ๋๋ค.
QMember member = QMember.member;
// ์ด๋ฆ์ผ๋ก ํ์ ์กฐํ
Member findMember = queryFactory
.selectFrom(member)
.where(member.name.eq("ํ๊ธธ๋"))
.fetchOne();
// ๋์ด์์ผ๋ก ํ์ ์ ๋ ฌ ํ ์กฐํ
List<Member> members = queryFactory
.selectFrom(member)
.orderBy(member.age.asc())
.fetch();
5. ์์ ์ฝ๋ ์ ์ฒด
@SpringBootTest
public class MemberRepositoryTest {
@PersistenceContext
private EntityManager entityManager;
private JPAQueryFactory queryFactory;
@BeforeEach
public void setUp() {
queryFactory = new JPAQueryFactory(entityManager);
}
@Test
public void ํ์_์ด๋ฆ์ผ๋ก_์กฐํ() {
QMember member = QMember.member;
Member findMember = queryFactory
.selectFrom(member)
.where(member.name.eq("ํ๊ธธ๋"))
.fetchOne();
assertThat(findMember.getName()).isEqualTo("ํ๊ธธ๋");
}
@Test
public void ํ์_๋์ด์์ผ๋ก_์ ๋ ฌํ_์กฐํ() {
QMember member = QMember.member;
List<Member> members = queryFactory
.selectFrom(member)
.orderBy(member.age.asc())
.fetch();
assertThat(members.get(0).getAge()).isLessThanOrEqualTo(members.get(1).getAge());
}
}
6. ์คํ ๊ฒฐ๊ณผ
- ์ ์์ ์ฝ๋๋ฅผ ์คํํ๋ฉด ํ์ ์ด๋ฆ์ผ๋ก ์กฐํํ๊ฑฐ๋ ๋์ด์์ผ๋ก ์ ๋ ฌ๋ ํ์ ๋ชฉ๋ก์ ์กฐํํ ์ ์์ต๋๋ค.
Querydsl ๋์ ์ฟผ๋ฆฌ ์ฌ์ฉ: ๊ฒ์ ๊ธฐ๋ฅ ๊ตฌํ ์์
Querydsl์ ์ฌ์ฉํ์ฌ ๋์ ์ฟผ๋ฆฌ๋ฅผ ๊ตฌํํ๋ ๊ฒ์ ๊ฒ์ ๊ธฐ๋ฅ๊ณผ ๊ฐ์ด ๋ค์ํ ์กฐ๊ฑด์ ๋ฐ๋ผ ์ฟผ๋ฆฌ๊ฐ ๋ฌ๋ผ์ ธ์ผ ํ๋ ๊ฒฝ์ฐ์ ์ ์ฉํฉ๋๋ค. ๋ค์์ ๊ธฐ๋ณธ์ ์ธ ์์๋ฅผ ํตํด Querydsl ๋์ ์ฟผ๋ฆฌ ์ฌ์ฉ๋ฒ์ ์ค๋ช ํฉ๋๋ค.
1. ํ๋ก์ ํธ ์ค์ ๋ฐ ์ํฐํฐ ์์ฑ
- Spring Boot ํ๋ก์ ํธ๋ฅผ ์์ฑํ๊ณ Querydsl ์์กด์ฑ์ ์ถ๊ฐํฉ๋๋ค.
- ๊ฒ์ ๋์ ์ํฐํฐ(Product)๋ฅผ ์์ฑํฉ๋๋ค.
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String category;
private int price;
// ... getter, setter ๋ฑ
}
2. Q-Type ์์ฑ ๋ฐ Querydsl ์ธ์คํด์ค ์์ฑ
- ์ํฐํฐ ํด๋์ค๋ฅผ ๊ธฐ๋ฐ์ผ๋ก Q-Type์ ์์ฑํฉ๋๋ค.
- JPAQueryFactory๋ฅผ ์ฌ์ฉํ์ฌ Querydsl ์ธ์คํด์ค๋ฅผ ์์ฑํฉ๋๋ค.
@PersistenceContext
private EntityManager entityManager;
private JPAQueryFactory queryFactory;
@BeforeEach
public void setUp() {
queryFactory = new JPAQueryFactory(entityManager);
}
3. ๋์ ์ฟผ๋ฆฌ ์์ฑ
- BooleanBuilder๋ฅผ ์ฌ์ฉํ์ฌ ๋์ ์กฐ๊ฑด์ ์์ฑํฉ๋๋ค.
- ๊ฒ์ ์กฐ๊ฑด์ ๋ฐ๋ผ BooleanBuilder์ ์กฐ๊ฑด์ ์ถ๊ฐํฉ๋๋ค.
@Test
public void ์ํ_๊ฒ์() {
QProduct product = QProduct.product;
BooleanBuilder builder = new BooleanBuilder();
String name = "์ด๋ํ";
String category = "์๋ฅ";
Integer price = 10000;
if (StringUtils.hasText(name)) {
builder.and(product.name.contains(name));
}
if (StringUtils.hasText(category)) {
builder.and(product.category.eq(category));
}
if (price != null) {
builder.and(product.price.goe(price));
}
List<Product> products = queryFactory
.selectFrom(product)
.where(builder)
.fetch();
// ... ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
}
4. ์์ ์ฝ๋ ์ ์ฒด
@SpringBootTest
public class ProductRepositoryTest {
@PersistenceContext
private EntityManager entityManager;
private JPAQueryFactory queryFactory;
@BeforeEach
public void setUp() {
queryFactory = new JPAQueryFactory(entityManager);
}
@Test
public void ์ํ_๊ฒ์() {
QProduct product = QProduct.product;
BooleanBuilder builder = new BooleanBuilder();
String name = "์ด๋ํ";
String category = "์๋ฅ";
Integer price = 10000;
if (StringUtils.hasText(name)) {
builder.and(product.name.contains(name));
}
if (StringUtils.hasText(category)) {
builder.and(product.category.eq(category));
}
if (price != null) {
builder.and(product.price.goe(price));
}
List<Product> products = queryFactory
.selectFrom(product)
.where(builder)
.fetch();
// ... ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
}
}
5. ์คํ ๊ฒฐ๊ณผ
- ์ ์์ ์ฝ๋๋ฅผ ์คํํ๋ฉด ๊ฒ์ ์กฐ๊ฑด์ ๋ฐ๋ผ ์ํ ๋ชฉ๋ก์ ์กฐํํ ์ ์์ต๋๋ค.
Querydsl ํ์ด์ง ์ฒ๋ฆฌ: ๊ฐ๋จํ ์์์ ํจ๊ป ์์๋ณด๊ธฐ
Querydsl์ ์ฌ์ฉํ์ฌ ํ์ด์ง ์ฒ๋ฆฌ๋ฅผ ๊ตฌํํ๋ฉด ๋๋์ ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๊ณ ์ฌ์ฉ์์๊ฒ ํ์ํ ๋ถ๋ถ๋ง ๋ณด์ฌ์ค ์ ์์ต๋๋ค. ๋ค์์ ๊ธฐ๋ณธ์ ์ธ ์์๋ฅผ ํตํด Querydsl ํ์ด์ง ์ฒ๋ฆฌ ๋ฐฉ๋ฒ์ ์ค๋ช ํฉ๋๋ค.
1. ํ๋ก์ ํธ ์ค์ ๋ฐ ์ํฐํฐ ์์ฑ
- Spring Boot ํ๋ก์ ํธ๋ฅผ ์์ฑํ๊ณ Querydsl ์์กด์ฑ์ ์ถ๊ฐํฉ๋๋ค.
- ํ์ด์ง ์ฒ๋ฆฌ ๋์ ์ํฐํฐ(Product)๋ฅผ ์์ฑํฉ๋๋ค.
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int price;
// ... getter, setter ๋ฑ
}
2. Q-Type ์์ฑ ๋ฐ Querydsl ์ธ์คํด์ค ์์ฑ
- ์ํฐํฐ ํด๋์ค๋ฅผ ๊ธฐ๋ฐ์ผ๋ก Q-Type์ ์์ฑํฉ๋๋ค.
- JPAQueryFactory๋ฅผ ์ฌ์ฉํ์ฌ Querydsl ์ธ์คํด์ค๋ฅผ ์์ฑํฉ๋๋ค.
@PersistenceContext
private EntityManager entityManager;
private JPAQueryFactory queryFactory;
@BeforeEach
public void setUp() {
queryFactory = new JPAQueryFactory(entityManager);
}
3. ํ์ด์ง ์ฟผ๋ฆฌ ์์ฑ
- Querydsl์ offset()๊ณผ limit()์ ์ฌ์ฉํ์ฌ ํ์ด์ง ์ฒ๋ฆฌ๋ฅผ ๊ตฌํํฉ๋๋ค.
- offset()์ ์์ ์์น๋ฅผ, limit()์ ํ์ด์ง ํฌ๊ธฐ๋ฅผ ์ง์ ํฉ๋๋ค.
@Test
public void ์ํ_ํ์ด์ง() {
QProduct product = QProduct.product;
int page = 0; // ํ์ด์ง ๋ฒํธ (0๋ถํฐ ์์)
int size = 10; // ํ์ด์ง ํฌ๊ธฐ
List<Product> products = queryFactory
.selectFrom(product)
.orderBy(product.id.asc()) // ์ ๋ ฌ ๊ธฐ์ค (id ์ญ์)
.offset(page * size) // ์์ ์์น ๊ณ์ฐ
.limit(size) // ํ์ด์ง ํฌ๊ธฐ
.fetch();
// ... ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
}
4. ์์ ์ฝ๋ ์ ์ฒด
@SpringBootTest
public class ProductRepositoryTest {
@PersistenceContext
private EntityManager entityManager;
private JPAQueryFactory queryFactory;
@BeforeEach
public void setUp() {
queryFactory = new JPAQueryFactory(entityManager);
}
@Test
public void ์ํ_ํ์ด์ง() {
QProduct product = QProduct.product;
int page = 0; // ํ์ด์ง ๋ฒํธ (0๋ถํฐ ์์)
int size = 10; // ํ์ด์ง ํฌ๊ธฐ
List<Product> products = queryFactory
.selectFrom(product)
.orderBy(product.id.asc()) // ์ ๋ ฌ ๊ธฐ์ค (id ์ญ์)
.offset(page * size) // ์์ ์์น ๊ณ์ฐ
.limit(size) // ํ์ด์ง ํฌ๊ธฐ
.fetch();
// ... ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
}
}
5. ์คํ ๊ฒฐ๊ณผ
- ์ ์์ ์ฝ๋๋ฅผ ์คํํ๋ฉด ์ํ ๋ชฉ๋ก์ ํ์ด์ง ๋จ์๋ก ์กฐํํ ์ ์์ต๋๋ค.