๐Ÿ“š[Spring] QueryDSL

ํ…ํ…ยท2025๋…„ 6์›” 24์ผ

์ŠคํŠธ๋ง ์ฟผ๋ฆฌ์˜ ๋‹จ์ 

์ŠคํŠธ๋ง ์ฟผ๋ฆฌ(String Query)๋ž€ JPA ์—์„œ JPQL ์ด๋‚˜ SQL ์ฟผ๋ฆฌ๋ฅผ ๋ฌธ์ž์—ด๋กœ ์ง์ ‘ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์„ ๋งํ•˜๋ฉฐ
๋ณดํ†ต @Query ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด ์‚ฌ์šฉ๋œ๋‹ค.
๋ณต์žกํ•œ ์กฐ๊ฑด์„ ๋ช…์‹œ์ ์œผ๋กœ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ์–ด ๋น ๋ฅด๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์ง€๋งŒ ์˜คํƒ€๋‚˜ ๋ฌธ๋ฒ• ์˜ค๋ฅ˜๊ฐ€
์ปดํŒŒ์ผ ํƒ€์ž„์— ๊ฒ€์ถœ๋˜์ง€ ์•Š์•„ ๋Ÿฐํƒ€์ž„ ์˜ค๋ฅ˜๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ๋‹ค๋Š” ์น˜๋ช…์ ์ธ ๋‹จ์ ์ด ์žˆ๋‹ค.
๋˜ํ•œ ๋ฌธ์ž์—ด๋กœ ์กฐ๊ฑด์„ ์กฐํ•ฉํ•ด์•ผ ํ•˜๋ฏ€๋กœ ๋™์  ์ฟผ๋ฆฌ ์ž‘์„ฑ์ด ๋ณต์žกํ•˜๊ณ  ๋ถˆํŽธํ•˜๋‹ค๋Š” ํ•œ๊ณ„๋„ ์กด์žฌํ•œ๋‹ค.

JPQL ์˜ ๋™์  ์ฟผ๋ฆฌ ์˜ˆ์‹œ - TaskCustomRepository

	public List<Task> searchTasks(String title, String content, String status) {
	//์กฐ๊ฑด๋ฌธ์„ ์ถ”๊ฐ€ํ•  ๊ธฐ๋ณธ jpql
	String jpql = "select t from Task t";

	//์กฐ๊ฑด๋ฌธ์„ ๋‹ด์„ ๋ฆฌ์ŠคํŠธ
	List<String> conditionList = new ArrayList<>();

	if(StringUtils.hasText(title)) {
		conditionList.add("t.title like concat('%', :title, '%')"); //์™€์ผ๋“œ์นด๋“œ ํฌํ•จ
	}
	if(StringUtils.hasText(content)) {
		conditionList.add("t.content like concat('%', :content, '%')");
	}
	if(StringUtils.hasText(status)) {
		conditionList.add("t.status = :status");
	}

	//์กฐ๊ฑด๋ฆฌ์ŠคํŠธ๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ์žˆ๋‹ค๋ฉด where + conditionList.get(0) + and + .get(1) ...
	if(!conditionList.isEmpty()) {
		jpql += " where " + String.join(" and ", conditionList);
	}

	//์™„์„ฑ๋œ jpql ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฟผ๋ฆฌ ๊ฐ์ฒด ์ƒ์„ฑ
	TypedQuery<Task> query = em.createQuery(jpql, Task.class);

	//ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ
	if(StringUtils.hasText(title)) {
		query.setParameter("title", title); //์™€์ผ๋“œ์นด๋“œ ํฌํ•จ
	}
	if(StringUtils.hasText(content)) {
		query.setParameter("content", content);
	}
	if(StringUtils.hasText(status)) {
		//์ด๋„˜ํƒ€์ž… ๋ณ€ํ™˜ ๋ฉ”์„œ๋“œ ์†Œ๋ฌธ์ž๋„ ๊ฐ€๋Šฅ
		Status fromStatus = Status.from(status);
		query.setParameter("status", fromStatus);
	}

	System.out.println("JPQL: " + jpql);
	//๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ ๋ฐ˜ํ™˜
	return query.getResultList();
}

3๊ฐœ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋™์  ์ฟผ๋ฆฌ์ž„์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ์ฝ”๋“œ๊ฐ€ ๊ฝค ๋ณต์žกํ•˜๋‹ค๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.
์กฐ๊ฑด์ด ๋Š˜์–ด๊ฐˆ์ˆ˜๋ก ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์€ ๊ธ‰๊ฒฉํ•˜๊ฒŒ ๋–จ์–ด์ง€๊ณ  ๊ฐ ์กฐ๊ฑด์„ ์ผ์ผ์ด ๊ฒ€์‚ฌํ•˜๊ณ  ๋ฌธ์ž์—ด์„ ์กฐ๋ฆฝํ•˜๊ณ  ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ”์ธ๋”ฉํ•˜๋Š” ๊ณผ์ •์ด ๋ฒˆ๊ฑฐ๋กญ๊ณ  ์‹ค์ˆ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ธฐ ์‰ฝ๋‹ค.
์ด์ฒ˜๋Ÿผ ๋‹จ์ˆœํ•œ ์กฐ๊ฑด์ด๋ผ๋„ ๋กœ์ง์ด ๋ณต์žกํ•ด์ง€๋ฉด ๊ด€๋ฆฌ๊ฐ€ ์–ด๋ ค์›Œ์ง€๊ธฐ ๋•Œ๋ฌธ์— ์‹ค๋ฌด์—์„œ๋Š” ์ฝ”๋“œ์˜ ์•ˆ์ •์„ฑ๊ณผ ๊ฐ€๋…์„ฑ, ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ์œ„ํ•ด QueryDSL ๊ฐ™์€ ๋„๊ตฌ๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•œ๋‹ค.


QueryDSL

๋“ค์–ด๊ฐ€๊ธฐ์— ์•ž์„œ QueryDSL์€ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์— ๋‚ด์žฅ๋œ ๊ธฐ๋Šฅ์ด ์•„๋‹ˆ๋‹ค.
๋”ฐ๋ผ์„œ QueryDsl์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„  ์„ ํ–‰๋˜์–ด์•ผ ํ•  ์ž‘์—…๋“ค์ด ์žˆ๋‹ค.

๋จผ์ € ์ถ”๊ฐ€ํ•ด์ค„ ์˜์กด์„ฑ์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

    //QueryDSL ์ถ”๊ฐ€
    implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
  1. implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
    • QueryDSL ์—์„œ JPA๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
    • ์ด ์˜์กด์„ฑ์„ ํ†ตํ•ด JPAQueryFactory, Qํด๋ž˜์Šค ๋“ฑ Qํด๋ž˜์Šค๋ฅผ ํ™œ์šฉํ•œ ์ฟผ๋ฆฌ ์ž‘์„ฑ์ด ๊ฐ€๋Šฅ
  2. annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta"
    • APT: Annotation Processing Tool (์†Œ์Šค์ฝ”๋“œ ์ƒ์„ฑ๋„๊ตฌ)
    • @Entity ๊ฐ€ ๋ถ™์€ ํด๋ž˜์Šค๋“ค์„ ๋ถ„์„ํ•ด์„œ Qํด๋ž˜์Šค๋ฅผ ์ž๋™ ์ƒ์„ฑํ•ด์ฃผ๋Š” ๋„๊ตฌ
  3. annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    • QueryDSL APT๊ฐ€ ์ž‘๋™ํ•  ๋•Œ ํ•„์š”ํ•œ ์ž๋ฐ” ์–ด๋…ธํ…Œ์ด์…˜ ์ฒ˜๋ฆฌ๊ด€๋ จ API
    • @Generated, @Nullable, @PostConstruct ๊ฐ™์€ ์–ด๋…ธํ…Œ์ด์…˜์˜ ์ •์˜๊ฐ€ ๋“ค์–ด ์žˆ์Œ
  4. annotationProcessor "jakarta.persistence:jakarta.persistence-api"
    • JPA ์–ด๋…ธํ…Œ์ด์…˜ ์ •์˜(@Entity, @Id, @Column)๋ฅผ ์ œ๊ณต
    • QueryDsl ์ด ์—”ํ‹ฐํ‹ฐ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋“ฑ์„ ๋ณด๊ณ  Qํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น API๊ฐ€ ํ•„์š”

์˜์กด์„ฑ ์ถ”๊ฐ€ํ›„

  • compileJava ์‹คํ–‰ ์‹œ build/generated/.../Qํด๋ž˜์Šค ์ž๋™์œผ๋กœ ์ƒ์„ฑ

QueryDslConfig

QueryDSL์„ ํ™œ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” JPAQueryFactory ๊ฐ์ฒด๊ฐ€ ํ•„์š”ํ•˜๋‹ค.
์ด๋ฅผ ์œ„ํ•ด ๋ณ„๋„์˜ ์„ค์ • ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ณ  EntityManager๋ฅผ ์ฃผ์ž…๋ฐ›์•„ JPAQueryFactory๋ฅผ ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•œ๋‹ค.

@Configuration
public class QueryDslConfig {

	@PersistenceContext
	private EntityManager entityManager;

	@Bean
	public JPAQueryFactory jpaQueryFactory() {
		return new JPAQueryFactory(entityManager);
	}
}

์ด๋ ‡๊ฒŒ ๋“ฑ๋ก๋œ JPAQueryFactory๋Š” Spring Bean์œผ๋กœ ๊ด€๋ฆฌ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋น„์Šค๋‚˜ ์ปค์Šคํ…€ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ํด๋ž˜์Šค์—์„œ ๋ฐ”๋กœ ์ฃผ์ž…๋ฐ›์•„ QueryDSL ์ฟผ๋ฆฌ๋ฅผ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.


Projection

QueryDsl์—์„œ๋Š” JPA์˜ ๊ธฐ๋ณธ ํ”„๋กœ์ ์…˜(SELECT new ํŒจํ‚ค์ง€๋ช…...)๊ณผ ๋‹ค๋ฅด๊ฒŒ ๋‹ค์–‘ํ•œ ๋ฐฉ์‹์œผ๋กœ DTO์— ์ง์ ‘ ๋ฐ์ดํ„ฐ๋ฅผ ๋งคํ•‘ํ•  ์ˆ˜ ์žˆ๋‹ค.

@Getter
public class TaskSearchRequestDto {
	private String title;
	private String content;
	private String status;
	
	public TaskSearchRequestDto(String title, String content, String status) {
		this.title = title;
		this.content = content;
		this.status = status;
    }
}

์ด์ฒ˜๋Ÿผ ์ƒ์„ฑ์ž๋ฅผ ์ •์˜ํ•ด๋†“๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ”„๋กœ์ ์…˜์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

.select(Projections.constructor(TaskSearchRequestDto.class, task.title, task.content, task.status))

@QueryProjection

์ƒ์„ฑ์ž์— @QueryProjection ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์—ฌ์ค€ ๋’ค compileJava๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ํ•ด๋‹น DTO์— ๋Œ€ํ•œ Qํด๋ž˜์Šค๊ฐ€ ํ•จ๊ป˜ ์ƒ์„ฑ๋œ๋‹ค.

ํ•ด๋‹น ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉ์‹œ ๋‹ค์–‘ํ•œ ์žฅ์ ์„ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค.

  • ํƒ€์ž…์•ˆ์ •์„ฑ: ์ปดํŒŒ์ผ ํƒ€์ž„์— ์˜ค๋ฅ˜๋ฅผ ์žก์•„์คŒ(ํ•„๋“œ ๋ˆ„๋ฝ, ์ž˜๋ชป๋œ ์ˆœ์„œ ๋“ฑ)
  • ๋ฆฌํŒฉํ† ๋ง ์•ˆ์ „: ์ƒ์„ฑ์ž ๋ณ€๊ฒฝ ์‹œ ์ปดํŒŒ์ผ ์—๋Ÿฌ๋กœ ์ฆ‰์‹œ ํ™•ใ…‡๋‹ˆ ๊ฐ€๋Šฅ
  • ๊น”๋”ํ•œ ๋ฌธ๋ฒ•: Projections.constructor(...) ๋ณด๋‹ค ์ง๊ด€์ ์ด๊ณ  ์งง์Œ
.select(new QTaskSearchRequestDto(task.title, task.content, task.status))

ํ•˜์ง€๋งŒ DTO๊นŒ์ง€ Qํด๋ž˜์Šค๊ฐ€ ์ƒ์„ฑ๋จ์— ๋”ฐ๋ผ QueryDSL์— ๋Œ€ํ•œ ์˜์กด๋„๊ฐ€ ๋„ˆ๋ฌด ๋†’์•„์ง€๊ฒŒ ๋œ๋‹ค.
๋ชจ๋“ˆํ™”๋‚˜ ๊ณ„์ธต ๋ถ„๋ฆฌ๋ฅผ ์ค‘์‹œํ•˜๋Š” ๊ตฌ์กฐ์—์„œ๋Š” ์‚ฌ์šฉ์— ๋Œ€ํ•ด ๊ณ ๋ คํ•ด๋ณผ ํ•„์š”๊ฐ€ ์žˆ๋‹ค.


Where์ ˆ ๋‹ค์ค‘ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐฉ์‹

QueryDsl์—์„œ .where() ์ ˆ ์•ˆ์— ์—ฌ๋Ÿฌ๊ฐœ์˜ ์กฐ๊ฑด์„ ๋‚˜์—ดํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์กฐ๊ฑด๋“ค์ด null์ธ์ง€ ์—ฌ๋ถ€์™€ ์ƒ๊ด€์—†์ด ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์„ ๊ฐ€์ง„๋‹ค.

@Repository
@RequiredArgsConstructor
public class TaskCustomRepositoryImpl implements TaskCustomRepository{

	private final JPAQueryFactory queryFactory;

	@Override
	public List<Task> searchTasksByQueryDsl(String title, String content, String status) {
		return queryFactory
			.selectFrom(task)
			.where(titleContains(title), contentContains(content), statusEq(status))
			.fetch();
	}

	private BooleanExpression titleContains(String title) {
		return title != null ? task.title.contains(title) : null;
	}

	private BooleanExpression contentContains(String content) {
		return content != null ? task.content.contains(content) : null;
	}

	private BooleanExpression statusEq(String status) {
		return status != null ? task.status.eq(Status.from(status)) : null;
	}

}

.where() ์ ˆ ์•ˆ์— BooleanExpression ์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ๋‚˜์—ดํ•ด์„œ ์‚ฌ์šฉํ•œ๋‹ค.
ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋“ค์€ ํŒŒ๋ผ๋ฏธํ„ฐ์— ์ „๋‹ฌ๋œ ๊ฐ’์ด ์žˆ์„ ๊ฒฝ์šฐ ๋ฌธ์ž์—ด ์กฐ๊ฑด์— ๋งž๋Š” ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
queryDsl์€ ์กฐ๊ฑด์— null์ด ํฌํ•จ๋  ๊ฒฝ์šฐ ์กฐ๊ฑด์„ ๋ฌด์‹œํ•˜๋ฏ€๋กœ ์˜ค๋ฅ˜ ์—†์ด ๋™์ ์ฟผ๋ฆฌ๊ฐ€ ์™„์„ฑ๋œ๋‹ค.


๊ฐ€๋…์„ฑ/์žฌ์‚ฌ์šฉ์„ฑ ํ–ฅ์ƒ

private BooleanExpression titleContains(String title) {
	//์‚ผํ•ญ ์—ฐ์‚ฐ์ž๋กœ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ํ‘œํ˜„
   return title != null ? task.title.contains(title) : null;
}

์œ„์ฒ˜๋Ÿผ BooleanExpression ๋ฉ”์„œ๋“œ๋งŒ ์ •์˜ํ•ด ๋†“์œผ๋ฉด ์ฟผ๋ฆฌ๋ฌธ์ด ๊น”๋”ํ•ด์ง€๊ณ  ์žฌ์‚ฌ์šฉ๋„ ์šฉ์ดํ•ด์ง„๋‹ค.


๋งˆ์น˜๋ฉฐ

BooleanExpression ๋ฐฉ์‹ ์™ธ์—๋„ ๋ณด๋‹ค ๋ณต์žกํ•œ ์กฐ๊ฑด์„ ์“ธ ๋• BooleanBuilder๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹๋„ ์žˆ๋‹ค.
๋‹ค์Œ ๊ธฐํšŒ์—” BooleanBuilder๋ฅผ ํ†ตํ•ด ์กฐ๊ธˆ ๋” ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๋ฌธ๋„ ์ž‘์„ฑํ•ด ๋ณผ ์˜ˆ์ •์ด๋‹ค.
์ถ”๊ฐ€๋กœ QueryDSL ๋ฌธ์ž์—ด ์กฐ๊ฑด ๋ฉ”์„œ๋“œ์— ๋Œ€ํ•ด ์ •๋ฆฌํ•œ ํ‘œ๋ฅผ ์ฒจ๋ถ€ํ•˜๋ฉฐ ๋งˆ๋ฌด๋ฆฌํ•˜๊ฒ ๋‹ค.


QueryDSL ๋ฌธ์ž์—ด ์กฐ๊ฑด ๋ฉ”์„œ๋“œ

๋ฉ”์„œ๋“œSQL ๋ณ€ํ™˜์„ค๋ช…
eq("abc")= 'abc'์ •ํ™•ํžˆ ์ผ์น˜
ne("abc")!= 'abc'์ผ์น˜ํ•˜์ง€ ์•Š์Œ
contains("abc")LIKE '%abc%'ํŠน์ • ๋ฌธ์ž์—ด ํฌํ•จ
startsWith("abc")LIKE 'abc%'ํŠน์ • ๋ฌธ์ž์—ด๋กœ ์‹œ์ž‘
endsWith("abc")LIKE '%abc'ํŠน์ • ๋ฌธ์ž์—ด๋กœ ๋๋‚จ
like("%abc%")LIKE '%abc%'์™€์ผ๋“œ์นด๋“œ ์ง์ ‘ ์ง€์ •
in(list)IN (...)์—ฌ๋Ÿฌ ๊ฐ’ ์ค‘ ํฌํ•จ
isNull()IS NULL๊ฐ’์ด null ์ธ ๊ฒฝ์šฐ
isNotNull()IS NOT NULLnull ์ด ์•„๋‹Œ ๊ฒฝ์šฐ

profile
์ฐจ๊ทผ์ฐจ๊ทผ

0๊ฐœ์˜ ๋Œ“๊ธ€