6 분 소요

 인프런 스프링 DB 2편 - 데이터 접근 활용 기술편을 학습하고 정리한 내용 입니다.

Querydsl 소개 1 - 기존 방식의 문제점

긴급 요구 사항!!!!!

  • 검색 조건 추가
    • 나이
    • 이름

퇴근 시간 10분전…

쿼리 추가

1
2
3
String sql = "select * from member" + 
	"where name like ?" + 
	"and age between ? and ?";

바로 퇴근 후 오류 발생. 띄어쓰기를 안했다…

QUERY의 문제점

  • QUERY는 문자, Type-check 불가능
  • 실행하기 전까지 작동 여부 확인 불가

에러는 크게 2가지

  • 컴파일 에러 (좋은 에러)
  • 런타임 에러 (나쁜 에러)

만약 SQL이 클래스처럼 타입이 있고 자바 코드로 작성 할 수 있다면? → type-safe

QueryDSL 나온 계기기

  • 쿼리를 Java로 type-safe하게 개발할 수 있게 지원하는 프레임워크
  • 주로 JPA 쿼리 (JPQL)에 사용

사람을 찾아보자.

  • 20~40살
  • 성 = 김씨
  • 나이 많은 순서
  • 3명을 출력하라

  • 장점 : SQL 쿼리와 비슷해서 금방 익숙해짐
  • 단점 : type-safe 아님. 동적 쿼리 생성이 어려움

결과 쿼리다.

  • 장점 : 동적 쿼리 생성이 가능함
  • 단점 :
    1. type-safe 아님
    2. 너무 너무 너무 복잡함
    3. 알아야 할게 너무 많음

MetaModel Criteria API라는 것도 있는데, 결론은 복잡하기는 매한가지다.

QueryDSL

  • Domain (도메인)
  • Specific (특화)
  • Language (언어)

DSL

  • 도메인 + 특화 + 언어
  • 특정한 도메인에 초점을 맞춘 제한적인 표현력을 가진 컴퓨터 프로그래밍 언어
  • 단순, 간결, 유창
  • 다양한 저장소 쿼리 기능 통합

코드 생성기

  • APT : Annotation Processing Tool
    • @Entity

이제 다시 질문의 사람을 찾아보자.

1
2
3
4
5
6
7
8
9
10
11
12
JPAQueryFactoryquery = newJPAQueryFactory(entityManager);  
QMemberm = QMember.member;  
  
List<Member>list = query  
        .select(m)  
        .from(m)  
        .where(  
			m.age.between(20,40).and(m.name.like("김%"))  
        )  
        .orderBy(m.age.desc())  
        .limit(3)  
        .fetch(m);

이렇게 쿼리처럼 작성할 수 있고 Type-Safe 장점까지 가져갈 수 있다.

  • 장점
    • type-safe
    • 단순함
    • 쉬움
  • 단점
    • Q코드 생성을 위한 APT를 설정해야 함.

더 다양한 내용은 querydsl 강의 정리에서 확인.

Querydsl 설정

나는 스프링 부트 3.xx 대를 사용하고 있기 때문에

1
2
3
4
5
//Querydsl 추가  
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'  
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"  
annotationProcessor "jakarta.annotation:jakarta.annotation-api"  
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

다음과 같이 build.gradle에 추가했다.

그리고

1
2
3
4
//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거  
clean {  
    delete file('src/main/generated')  
}

해당 부분을 bulid.gradle 맨 하단에 작성했다.

참고로 스프링 부트 2.xx 대는

1
2
3
4
5
6
dependencies { 
//Querydsl 추가 
implementation 'com.querydsl:querydsl-jpa' 
annotationProcessor "com.querydsl:querydsl-apt:$ {dependencyManagement.importedProperties['querydsl.version']}:jpa" 
annotationProcessor "jakarta.annotation:jakarta.annotation-api" 
annotationProcessor "jakarta.persistence:jakarta.persistence-api" }

다음과 같다고 한다.

해당 세팅에 따라 또 Q클래스를 생성하는 방법이 다르다.

Gradle

만약 Gradle이라면

  • Gradle 탭 -> Tasks -> build -> clean : 먼저 이전 파일 정리
  • Gradle 탭 -> Tasks -> other -> compileJava : 이제 Q파일 생성

Gradle을 콘솔로 작동 시킬 거라면

1
./gradlew clean compileJava

다음과 같이 프로젝트 최상단 디렉토리에서 실행하면된다.

build/generated/sources/annotationProcessor/java/main/hello/itemservice/domain/QItem.java

해당 위치에 Q파일이 @Entity가 있는 클래스를 기반으로 생성된 걸 확인할 수 있다.

참고 : Q타입은 컴파일 시점에 자동 생성되므로 Git에 포함시키지 않는게 좋다.

Q타입 삭제 gradle clean을 수행하면 build폴더 자체가 삭제된다. 따라서 별도의 설정은 없어도 된다.

IntelliJ IDEA

다음과 같이 세팅해놓으면

Build -> Build Project 또는 Build -> Rebuild 또는 main(), 또는 테스트를 실행하면 된다.

이러면 src/main/generated/ 하위에 다음과 같이 생긴다. 경로가 좀 다르다.

여기는 Git에서 커밋이 될 수 있기 때문에 gitignore나 다른 세팅이 필요하다.

Q타입 삭제

1
2
3
4
//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거 
clean { 
	delete file('src/main/generated') 
}

이렇게 gradle에 세팅해놔서 gradle clean해서 삭제해도 되고,

아니라면 직접 폴더 삭제하면 된다.

참고

Querydsl은 이렇게 설정하는 부분이 사용하면서 조금 귀찮은 부분인데, IntelliJ가 버전업 하거나 Querydsl의 Gradle 설정이 버전업 하면서 적용 방법이 조금씩 달라지기도 한다. 그리고 본인의 환경에 따라서 잘 동작하지 않기도 한다. 공식 메뉴얼에 소개 되어 있는 부분이 아니기 때문에, 설정에 수고로움이 있지만 querydsl gradle로 검색하면 본인 환경에 맞는 대안을 금방 찾을 수 있을 것이다.

Querydsl 적용

JpaItemRepositoryV3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class JpaItemRepositoryV3 implements ItemRepository {  
  
    private final EntityManager em;  
    private final JPAQueryFactory query;  
  
    public JpaItemRepositoryV3(EntityManager em) {  
        this.em = em;  
        this.query = new JPAQueryFactory(em);  
    }  
  
    @Override  
    public Item save(Item item) {  
        em.persist(item);  
        return item;  
    }  
  
    @Override  
    public void update(Long itemId, ItemUpdateDto updateParam) {  
        Item findItem = em.find(Item.class, itemId);  
        findItem.setItemName(updateParam.getItemName());  
        findItem.setPrice(updateParam.getPrice());  
        findItem.setQuantity(updateParam.getQuantity());  
    }  
  
    @Override  
    public Optional<Item> findById(Long id) {  
        Item item = em.find(Item.class, id);  
        return Optional.ofNullable(item);  
    }  
  
    public List<Item> findAllOld(ItemSearchCond cond) {  
  
        String itemName = cond.getItemName();  
        Integer maxPrice = cond.getMaxPrice();  
  
        BooleanBuilder builder = new BooleanBuilder();  
  
        if (StringUtils.hasText(itemName)) {  
             builder.and(QItem.item.itemName.like("%" + itemName + "%"));  
        }  
  
        if (maxPrice != null) {  
            builder.and(QItem.item.price.loe(maxPrice));  
        }  
  
        return query.select(QItem.item)  
                .from(QItem.item)  
                .where(builder)  
                .fetch();  
    }  
  
    @Override  
    public List<Item> findAll(ItemSearchCond cond) {  
        String itemName = cond.getItemName();  
        Integer maxPrice = cond.getMaxPrice();  
  
        return query.select(QItem.item)  
                .from(QItem.item)  
                .where(likeItemName(itemName), maxPrice(maxPrice))  
                .fetch();  
    }  
  
    private BooleanExpression maxPrice(Integer maxPrice) {  
        if (maxPrice != null) {  
            return QItem.item.price.loe(maxPrice);  
        }  
        return null;  
    }  
  
    private BooleanExpression likeItemName(String itemName) {  
        if (StringUtils.hasText(itemName)) {  
            return QItem.item.itemName.like("%" + itemName + "%");  
        }  
        return null;  
    }  
}
  • Querydsl을 사용하려면 JPAQueryFactory가 필요하다. JPAQueryFactory는 JPA가 쿼리인 JPQL을 만들기 때문에 EntityManager가 필요하다.
  • JdbcTemplate을 설정하듯이 생성자에서 EntityManager를 얻어서 생성된다.
  • 참고로 JPAQueryFactory를 스프링 빈으로 등록해서 사용해도 된다.

save(), update(), findById()

얘내는 이전 JPA그대로 일단 쓰겠다.

findAllOld

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public List<Item> findAllOld(ItemSearchCond cond) {  
  
	String itemName = cond.getItemName();  
	Integer maxPrice = cond.getMaxPrice();  

	BooleanBuilder builder = new BooleanBuilder();  

	if (StringUtils.hasText(itemName)) {  
		 builder.and(QItem.item.itemName.like("%" + itemName + "%"));  
	}  

	if (maxPrice != null) {  
		builder.and(QItem.item.price.loe(maxPrice));  
	}  

	return query.select(QItem.item)  
			.from(QItem.item)  
			.where(builder)  
			.fetch();  
}

Querydsl을 사용해서 동적 쿼리 문제를 해결한다.

BooleanBuilder를 사용해서 원하는 where 조건을 넣어주면 된다.

이 모든 것을 자바코드로 작성하기 때문에 동적 쿼리를 매우 편리하게 작성할 수 있다.

하지만 이 코드보다 좀 더 깔끔하게 리팩토링 해보자.

먼저 QItem.item 여기서 QItem.item은 static이기 때문에

static import로 더 깔끔하게 표현할 수 있을 것 같다.

그리고 이제 동적 조건문을 메서드로 추상화하여 리팩토링 해보자.

findAll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override  
public List<Item> findAll(ItemSearchCond cond) {  
    String itemName = cond.getItemName();  
    Integer maxPrice = cond.getMaxPrice();  
  
    return query.select(item)  
            .from(item)  
            .where(likeItemName(itemName), maxPrice(maxPrice))  
            .fetch();  
}  
  
private BooleanExpression maxPrice(Integer maxPrice) {  
    if (maxPrice != null) {  
        return item.price.loe(maxPrice);  
    }  
    return null;  
}  
  
private BooleanExpression likeItemName(String itemName) {  
    if (StringUtils.hasText(itemName)) {  
        return item.itemName.like("%" + itemName + "%");  
    }  
    return null;  
}

다음과 같이 동적 쿼리를 사용하기 위해

BooleanExpression이라는 판단할 수 있는 객체로 리턴하는 메서드를 작성했다.

null이 들어오면 querydsl에서 그 조건을 무시해버리기 때문에 동적 쿼리를 쉽게 작성할 수 있다.

  • 이 코드의 또 다른 장점은 likeItemName(), maxPrice()를 다른 쿼리를 작성할 때 재사용 할 수 있다는 점이다. 쉽게 이야기해서 쿼리 조건을 부분적으로 모듈화 할 수 있다. 자바 코드로 개발하기 때문에 얻을 수 잇는 큰 장점이다.
1
2
3
4
return query.select(item)  
		.from(item)  
		.where(likeItemName(itemName), maxPrice(maxPrice))  
		.fetch();  

이건 진짜 신기하긴 하다;;

이제 설정하고 실행해보자.

설정 및 실행

QuerydslConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration  
@RequiredArgsConstructor  
public class QuerydslConfig {  
  
    private final EntityManager em;  
  
    @Bean  
    public ItemService itemService() {  
        return new ItemServiceV1(itemRepository());  
    }  
  
    @Bean  
    public ItemRepository itemRepository() {  
        return new JpaItemRepositoryV3(em);  
    }  
  
}

ItemServiceApplication

1
2
3
4
5
6
//@Import(SpringDataJpaConfig.class)  
@Import(QuerydslConfig.class)  
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")  
public class ItemServiceApplication { 
	..
}

findAll()메서드가 정상적으로 동작했다.

예외 변환

Querydsl은 별도의 스프링 예외 추상화를 지원하지 않는다. 대신에 JPA에서 학습한 것 처럼 @Repository에서 스프링 예외 추상화를 처리해준다.

정리

1
2
3
4
return query.select(item)  
		.from(item)  
		.where(likeItemName(itemName), maxPrice(maxPrice))  
		.fetch();  
  • 쿼리 문장에 오타가 있어도 컴파일 시점에 오류를 막을 수 있다.
  • 메서드 추출을 통해서 코드를 재사용할 수 있다. 예를 들어서 여기서 만든 likeItemName(itemName), maxPrice(maxPrice) 메서드를 다른 쿼리에서도 함께 사용할 수 있다.

댓글남기기