회사에 SaaS Portal을 개발을 진행한 지 반년이 막 넘어갈 때쯤 일 것이다.
기획과 개발 초기에는 프로젝트 규모가 굉장히 작았기 때문에 순수 Spring Data JPA만을 활용하여
데이터 CRUD 메서드 쿼리를 활용했는데,
규모가 커지면서 테이블 스키마도 복잡해지고, 조회 쿼리가 점점 복잡해지면서
JPA @Query Annotation을 이용하여 JPQL을 사용하는 일이 많아지고
직접 쿼리를 작성하는 일이 많아졌는데,
그러다 보니 스키마가 약간 변경이 될 때마다 수정해야 하는 쿼리들과 로직들이 많아지고
쿼리를 '직접' 쓰다 보니 수정점을 찾기가 힘들어지고
결국엔 수정점을 놓쳐 에러를 많이 내곤 했었다.
그래서 나는 '객체지향 쿼리 언어'인 Querydsl을 도입하게 되었다.
도입에 타당한 근거가 있는가?
지금 회사를 다니면서 많이 배우게 된 부분이다.
신기술 물론 좋다. 하지만 무작정 무자비하게 도입을 하게 된다면 검증되지 못한 부분으로 인해
더 큰 개발 공수가 들 수도 있고,
다 벌려놓은 판을 제대로 해결하지 못할 수도 있다.
그래서 항상 무언가의 기능을 도입하려면 도입에 타당한 근거를 두려고 하는 편이다.
(안될 때도 있다. 위에서 하라면 해야.. 읍읍)
JPA에서 사용할 수 있는 객체지향 쿼리 언어에는 크게 3가지가 있다
1. JPQL (Java Persistence Query Language)
- 데이터베이스 테이블을 대상으로 하는 데이터 중심의 쿼리가 아닌 객체를 대상으로 검색하는 객체지향 쿼리
- SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않음
-SQL보다 간단
2. Criteri
- JPQL을 편하게 작성하도록 도화주는 API, 빌더 클래스 모음
- 문자가 아닌 query.select(m). where.. 과 같은 프로그래밍 코드로 JPQL을 작성할 수 있어, 컴파일 시 에러를 잡아낼 수 있음
- IDE를 사용하면 코드 자동완성을 지원
- 장점도 많지만, 사용하기 복잡하고 어려워 코드의 가독성이 떨어지는 단점이 있음
3. QueryDSL
- JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음, 비표준 오픈소스 프레임워크
-Criteria처럼 코드 기반이면서 단순하고 사용하기 쉬움
-코드의 가독성 또한 높음
QueryDSL은 코드로 JPQL을 작성하여 문법 오류를 컴파일 단계에 잡을 수 있다는 Cirteria의 장점과
쉽고 간결하게 코드를 통해 쿼리문을 바로 생각할 수 있다는 JPQL의 장점이 모두 담겨있다.
이에 나는 JPQL의 장점과 Criteria의 장점이 모두 섞인 QueryDSL을 도입하기로 했다.
그래서 이런 객체지향 쿼리 언어, 특히 QueryDSL을 왜 사용하는 걸까?
Querydsl 레퍼런스 문서에는 다음과 같이 설명하고 있다.
QueryDSL은 타입에 안전한 방식으로 HQL 쿼리를 실행하기 위한 목적으로 만들어졌다.
HQL 쿼리를 작성하다 보면 String 연결을 이용하게 되고, 이는 결과적으로 읽기 어려운 코드를 만드는 문제를 야기한다.
String을 이용해서 도메인 타입과 프로퍼티를 참조하다 보면 오타 등으로 잘못된 참조를 하게 될 수 있으며,
이는 String을 이용해서 HQL 작성할 때 발생하는 또 다른 문제다.
타입에 안전하도록 도메인 모델을 변경하면 소프트웨어 개발에서 큰 이득을 얻게 된다.
도메인의 변경이 직접적으로 쿼리에 반영되고, 쿼리 작성 과정에서 코드 자동완성 기능을 사용함으로써
쿼리를 더 빠르고 안전하게 만들 수 있게 된다.
백문이불여일견이라고,
일단 써봐야 위에 적힌 장점들을 체감할 수 있을 듯하다.
1. 기본 세팅
1-1. 라이브러리 의존성 주입
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
- querydsl-jpa: QueryDSL JPA 라이브러리
- querydsl-apt: 쿼리 타입(Q)을 생성할 때 필요한 라이브러리
1-2. 플러그인 추가
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
<options>
<querydsl.entityAccessors>true</querydsl.entityAccessors>
</options>
</configuration>
</execution>
</executions>
</plugin>
- JPAAnnotationProcessor는
javax.persistence.Entity 애노테이션을 가진 도메인 타입을 찾아서 쿼리 타입을 생성
- 도메인 타입으로 Hibernate 애노테이션을 사용하면, APT 프로세서로 com.querydsl.apt.hibernate.HibernateAnnotationProcessor를 사용해야 함
- mvn clean install을 실행하면,
target/generated-sources/java 디렉터리에 Query 타입이 생성
- 생성된 Query 타입을 이용하면 JPA 쿼리 인스턴스와 쿼리 도메인 모델 인스턴스를 생성할 수 있음
참고! QEntity가 생기는 경로는 gitignore 설정하는 것을 권장한다.메이븐이 알아서 생성해 주는 파일이기 때문에 프로젝트 설정에 따라 알아서 생성된다.
라이브러리 사용에 따라 변경될 수 있는 부분이기 때문에 git에 올리지 말고 빌드해서 새로 생성하여 쓰는 것이 올바르다.
1-3. Configuration 등록
@Configuration
public class QuerydslConfiguration {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
- Configuration으로 등록해서 프로젝트 어디에서나
JPAQueryFactory를 주입받아 Querydsl을 사용할 수 있게 됨
2. 사용해보기!!!
실제로 구현하는 방식이 여러 개 있지만,
나는 그중에 상속/구현이 없는 Repository 방식을 사용하기로 했다.
이는 방식도 가장 간단하며 최소한의 Bean등록을 할 수 있고,
별도의 상속과 구현이 없이 사용하기 때문에 사용법이 좀 더 간편하기 때문이다
나머지 방식들의 대한 설명은 이 링크를 참고하자!
(https://jojoldu.tistory.com/372)
Spring Boot Data Jpa 프로젝트에 Querydsl 적용하기
안녕하세요? 이번 시간에는 Spring Boot Data Jpa 프로젝트에 Querydsl을 적용하는 방법을 소개 드리겠습니다. 모든 코드는 Github에 있습니다. Spring Data Jpa를 써보신 분들은 아시겠지만, 기본으로 제공해
jojoldu.tistory.com
2-1. 엔티티 구성
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Builder
public class Academy {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String address;
}
2-2. Repository 구성
@RequiredArgsConstructor
@Repository
public class AcademyQueryRepository {
private final JPAQueryFactory queryFactory;
public List<Academy> findByName(String name) {
return queryFactory.selectFrom(academy)
.where(academy.name.eq(name))
.fetch();
}
}
기초적인 사용법은 이 블로그를 많이 참고했다 (https://jinjinyang.tistory.com/8)
그렇다면 내가 실제 내 프로젝트에 어떻게 녹여냈을까?
사실 Querydsl을 사용하는 가장 큰 이유는 간단하게 자동완성으로 복잡한 쿼리와 동적 쿼리를 만들기 위해서 일 거다
그래서 우리 프로젝트에 직접 도입하여 동적 쿼리를 실행할 수 있는 여러 개의 엔티티 중 한 개를 변경해 보았다.
1. Entity
@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString
public class ApplyHistory {
@Id
@GeneratedValue
private long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "email")
private UserInfo userInfo;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "productCode")
private ProductInfo productInfo;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "applyType")
private ApplyInfo applyInfo;
private Instant applyDate;
private Boolean mailSend;
private Boolean inUse;
private String comment;
}
2. EntitySearchCondition
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Builder
public class ApplyHistorySearchCondition {
private String email;
private String applyType;
private String productCode;
private Integer ackResult;
private boolean inUse;
private Instant startTime;
private Instant endTime;
}
- 동적 쿼리에서 넘길 검색 조건 클래스이다.
- where절에 eq, ne, between 조건 등으로 사용할 칼럼들을 담고 있는 예시이다. (언제든지 추가될 수 있다.)
- Builder를 동해 동적으로 생성하기 좋은 형태로 구성한다.
3. EntityDto
@Getter
@Setter
public class ApplyHistoryDto {
private long applyId;
private String comment;
}
- 필요한 칼럼들만 조회할 때 사용하는 컬럼들만 담은 dto 객체이다.
4. Repository
@RequiredArgsConstructor
@Repository
public class ApplyHistoryQuerydslRepository {
private final JPAQueryFactory queryFactory;
private final QApplyHistory applyHistory = new QApplyHistory(ALIAS.APPLY_HISTORY);
private final QUserInfo userInfo = new QUserInfo(ALIAS.USER_INFO);
private final QProductInfo productInfo = new QProductInfo(ALIAS.PRODUCT_INFO);
private final QApplyInfo applyInfo = new QApplyInfo(ALIAS.APPLY_INFO);
public List<ApplyHistoryDto> searchAll(ApplyHistorySearchCondition applyHistorySearchCondition) {
return searchAllByCondition(applyHistorySearchCondition).fetch();
}
private JPAQuery<ApplyHistoryDto> searchAllByCondition(ApplyHistorySearchCondition applyHistorySearchCondition) {
return queryFactory
.select(
Projections.fields(
ApplyHistoryDto.class,
applyHistory.id.as("applyId"),
applyHistory.comment)
.from(applyHistory)
.innerJoin(userInfo)
.on(userInfo.email.eq(applyHistory.userInfo().email))
.fetchJoin()
.innerJoin(productInfo)
.on(productInfo.productCode.eq(applyHistory.productInfo().productCode))
.fetchJoin()
.innerJoin(applyInfo)
.on(applyInfo.applyType.eq(applyHistory.applyInfo().applyType))
.fetchJoin()
.where(
eqEmail(applyHistorySearchCondition.getEmail()),
eqProductCode(applyHistorySearchCondition.getProductCode()),
eqApplyType(applyHistorySearchCondition.getApplyType())
);
}
private BooleanExpression eqEmail(String email) {
return email == null ? null : applyHistory.userInfo().email.eq(email);
}
private BooleanExpression eqProductCode(String productCode) {
return productCode == null ? null : applyHistory.productInfo().productCode.eq(productCode);
}
private BooleanExpression eqApplyType(String applyType) {
return applyType == null ? null : applyHistory.applyInfo().applyType.eq(applyType);
}
}
가장 중요한 클래스라고 할 수 있겠다!!
- private final QApplyHistory applyHistory = new QApplyHistory(ALIAS.APPLY_HISTORY);
사용할 QClass를 할당하고 할당하면서 별칭을 사용하였다.
별칭은 전역적으로 동일 테이블에 동일 명을 사용하면 좋을 것 같아서 Enum 타입으로 구성하였다.
- select(Projections.fields(ApplyHistoryDto.class, applyHistory.id.as("applyId"), applyHistory.comment)
일부 데이터만 조회하기 위해서 dto 객체와 Projection을 사용하여 데이터를 조회한다.
- innerJoin(userInfo). on(userInfo.email.eq(applyHistory.userInfo(). email)). fetchJoin()
즉시 로딩에 대한 과부하와 n+1 이슈 대응을 위해 지연 로딩으로 수정함에 따른 데이터
미 조회 이슈를 위한 fetch join을 활용한다.
- eqEmail(applyHistorySearchCondition.getEmail())
세부 메소들을 구성하여 동적 쿼리를 구성한다.
- return email == null? null : applyHistory.userInfo(). email.eq(email);
동적 쿼리 조회를 위한 분기 처리를 구성한다.
1탄 정리는 여기까지 하고자 한다.
Querydsl을 도입하고 더 다이내믹하게 구성하기 위해 굉장히 많은 일들을 했었다.
응답 데이터 자체를 다이내믹하게 구성하고
여러 쿼리 조건도 (=, like, in 등) 성능이 좋지 않은 쿼리 튜닝도
데이터 조회 형식도 다이내믹하게 구성했던 기억이 있다.
2탄에 내가 어떻게 점점 고도화를 해나갔는지 정리해 보겠다.

(과거 코드를 뒤적이는 나....)
왜 안바뀔까
그럼 안녕!
'Programing > Jpa' 카테고리의 다른 글
우아한 테크캠프 Pro 회고록 - EnableJpaAuditing (4) | 2022.11.08 |
---|---|
JPA / Hibernate / Spring Data JPA (0) | 2022.05.30 |