<목적>
애플리케이션을 구현하다보면 사용자의 요구사항에 맞추어
DB에서 조건에 맞는 데이터를 가져오는 ORM은 여러가지가 있다.
내가 사용했던 대표적인 ORM에는 2가지가 있다.
1. JPA - Spring Data JPA
2. JPA - JPQL
하지만 위의 두가지 기술에는 데이터를 가져오는 조건을
모두 포함시키는 메서드 이름 컨벤션을 작성해서 사용하든,
직접 쿼리문으로 해당 조건들을 모두 기입하든,
개발자가 필터링 조건들을 모두 사전에 셋팅을 해야한다는 단점이 있다.
내가 진행했던 사이드 프로젝트에는 다음과 같은 요구사항이 있었다.
A. 도서 제목 검색
B. 도서관 이름 검색
C. 도서관에 보관중인 도서 검색
D. 도서관에 등록된 회원 검색
E. 특정 태그를 가진 도서 검색
F. 연체 기록이 있는 회원 검색
G. 미납된 도서가 있는 회원 검색
위의 조건들을 API로 적용해보면
HTTP Method : GET
URI : /books, /members, /library etc...
Parameter : 위의 조건에 맞도록 아래와 같이 여러 내용이 들어갈 것이다.
names, storingBooks, registeredMembers, tagMember, tagBook, tagLibrary, overDueMember, UnreturnedBook etc...
한번에 하나의 조건만 필요하다면 다행이지만,
어떤 요청에는 두개가 들어가고, 세개가 들어가고 형태는 정말 많은 경우의 수를 가지게 된다.
예시를 들어보자.
1. JPA - Spring Data JPA
A. 도서 제목 검색
public Book findByBookTitle(String content);
B. 특정 도서관 이름 검색
public Library findByLibraryTitle(String content);
C. 도서관에 보관중인 도서 검색
public Book findBookByTitleAndLibrary_Title(String bookTitle, String libraryTitle);
D. 도서관에 등록된 회원 검색
public Member findMemberByNameAndLibrary_Title(String memberName, String libraryName);
E. 특정 태그를 가진 도서 검색
List<TagBook> findTagBooksByName(String tagName);
F. 연체 기록이 있는 회원 검색
List<Member> findByOverduedaysIsNotNull();
G. 미납된 도서가 있는 회원 검색
List<Member> findByReturnedAtIsNull ();
2. JPA - JPQL
A. 도서 제목 검색
@Query("SELECT b FROM book b WHERE b.title =:bookTitle")
public Book findBooks (@Param("bookTitle")String bookTitle);
B. 도서관 이름 검색
@Query("SELECT l FROM Library l WHERE l.title = :libraryName")
public List<Library> findLibrary(@Param("libraryName") String libraryName);
C. 도서관에 보관중인 도서 검색
@Query("SELECT lb FROM LibraryBook lb
WHERE lb.book.title = :bookTitle AND lb.library.name = :libraryName")
public List<LibraryBook> findBooks(@Param("bookTitle") String bookTitle,
@Param("libraryName") String libraryName);
D. 도서관에 등록된 회원 검색
@Query("SELECT lm FROM LibraryMember lm
WHERE lm.member.name = :memberName AND lb.library.name = :libraryName")
public List<LibraryMember> findMembers(@Param("memberName") String memberName,
@Param("libraryName") String libraryName);
E. 특정 태그를 가진 도서 검색
@Query("SELECT tb FROM TagBook tb WHERE tb.tag.name = :tagName")
public List<Book> findBooks(@Param("tagName") String tagName);
F. 연체 기록이 있는 회원 검색
@Query("SELECT lm FROM LibraryMember lm WHERE lm.overduedays IS NOT NULL")
public List<LibraryMember> findMembers();
G. 미납된 도서가 있는 회원 검색
@Query("SELECT lm FROM LibraryMember lm WHERE lm.returnedAt IS NULL")
public List<LibraryMember> findMembers();
< QueryDsl 의 필요성 >
예시로 작성해둔 위의 조건들은 간단한 편에 속하지만, 실제 서비스에서 필요한 데이터를 호출 할 때는
필요한 요구사항들이 고정되어 있지 않는 경우가 많다. (다양한 필터링 옵션, 정렬 기준, 페이징 옵션 등등)
실제 서비스에서 도서 검색 기능에는 제목, 작가, 장르, 출판년도 에 대해 각각 검색하고
이 모든 조건들을 조합하여 검색할 수 있어야 하는데,
<Spring Data JPA의 경우>
public book findBooksByTitleAndAuthorAndGenreAndPublicationYear (String content)
보이는것 처럼 메서드의 이름이 너어어어어무우우우우 길어지고, 파라미터가 많아진다.
<JPQL의 경우>
1. 그만큼 SQL 문을 일일이 추가해야하고 동적으로 쿼리를 생성하는 로직도 추가로 필요하다.
2. 또한 @Query 안에 작성하는 SQL은 문자열로 작성되므로 컴파일 단계에서는 오류를 확인 불가.
이처럼 복잡해지는 데이터 호출 조건과 같은 동적인 요소들을 처리하는데 좋은 방법이 QueryDSL 이다.
public List<Book> findBooks(String title, String author, String genre, Integer publicationYear) {
JPAQuery<Book> query = new JPAQuery<>(entityManager);
QBook book = QBook.book;
if (title != null) {
query.where(book.title.eq(title));
}
if (author != null) {
query.where(book.author.eq(author));
}
if (genre != null) {
query.where(book.genre.eq(genre));
}
if (publicationYear != null) {
query.where(book.publicationYear.eq(publicationYear));
}
return query.from(book).fetch();
}
이 코드에서는 QueryDSL의 JPAQuery 객체를 사용해 쿼리를 동적으로 생성하고 있다.
필터링 조건은 null이 아닌 경우에만 쿼리에 추가된다.
이처럼 여러 필터 조건 중 일부만 사용하는 경우에도 유연하게 대응할 수 있다.
QueryDsl
정적 타입을 이용하여 SQL과 같은 쿼리를 생성할 수 있게 해주는 프레임 워크.
<장점>
1. 문자가 아닌 코드로 쿼리를 작성하므로, 컴파일 시점에 문법 오류를 확인 가능
2. IDE에서 자동 완성 기능의 도움을 받을 수 있다.
3. 복잡한 쿼리나 동적 쿼리 작성이 편리하다.
4. 쿼리 작성 시 제약 조건 등을 메서드 추출을 통해 재사용할 수 있다.
5. JPQL 문법과 유사한 형태로 작성할 수 있어 작성 난이도가 낮다.
< 결론 >
지금까지 ORM 프레임워크 ( Spring Data JPA, JPA-JPQL 그리고 QueryDsl) 에 대한 장단점을 알아봤다.
QueryDsl의 장점인 타입 안정성과 유연성을 이용하면, 복잡한 쿼리를 작성하고 관리하는 과정에서
발생할 수 있는 버그를 컴파일 단계에서 확인할 수 있고 유지보수성을 향상시킬 수 있다.
앞으로 개발할 때 프로젝트의 상황에 따라 QueryDsl을 이용해야겠다.
<적용법>
1. build.gradle 추가
// QueryDSL #1.
buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.11'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
id "org.asciidoctor.jvm.convert" version "3.3.2"
// QueryDSL #2.
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
dependencies {
// QueryDSL #3.
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
}
//QueryDSL #4.
def querydslDir = "$buildDir/generated/querydsl"
//QueryDSL #5.
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
//QueryDSL #6.
sourceSets {
main.java.srcDir querydslDir
}
//QueryDSL #7.
configurations {
compileOnly {
extendsFrom annotationProcessor
}
querydsl.extendsFrom compileClasspath
}
//QueryDSL #8.
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
2. Q 클래스 생성
Gradle -> Tasks -> other -> compileQuerydsl 클릭
위와 같이 실행하고 나면 build.gradle에 추가한 #4.코드와 같이
build/generated/querydsl 디렉토리의 각 엔티티에 Q Class가 생성된다.
이 Q 클래스를 사용하면 QueryDsl을 사용하여 type-safe 쿼리를 사용할 수 있다.
이제 예시로써 기존에 있던 Repository의 메서드를 대상으로 QueryDsl로 변경해보자.
[기존 메서드]
@Repository
public interface LibraryMemberRepository extends JpaRepository<LibraryMember, Long> {
@Query("SELECT lm FROM LibraryMember lm WHERE lm.library.id = :libraryId")
public Page<LibraryMember> findAllLibraryMembersByLibraryId(@Param("libraryId")Long libraryId, Pageable pageable);
@Query("SELECT lm FROM LibraryMember lm
WHERE lm.library.id = :libraryId AND lm.member.id =:memberId")
LibraryMember findByLibrary_IdAndMember_Id(@Param("libraryId") Long libraryId, @Param("memberId") Long memberId);
}
[QueryDsl] 적용하는 순서
1. custom 인터페이스 생성.
기존 JPA 를 상속하는 기존의 Repository 인터페이스, 이 인터페이스의 커스텀 버젼 인터페이스. 총 2개.
이 custom 인터페이스에는 기존의 인터페이스가 가진 메서드와 유사한 메서드를 생성한다.
public interface LibraryMemberRepositoryCustom {
List<LibraryMember> Q_DSL_findEveryLibraryMembersByLibraryId(Long libraryId);
LibraryMember Q_DSL_findLibraryMembersByLibrary_IdAndMember_Id(Long libraryId, Long memberId);
}
2. 기존 Repository가 새로 생성한 custom Repository를 상속받도록 수정.
>> 그래야 필요한 Service 클래스에서 LibraryMemberRepository를 주입받아 사용 시,
Spring Data JPA와 QueryDsl 두 가지 메서드를 사용할 수 있음.
@Repository
public interface LibraryMemberRepository
extends JpaRepository<LibraryMember, Long>, LibraryMemberRepositoryCustom {
@Query("SELECT lm FROM LibraryMember lm WHERE lm.library.id = :libraryId")
public Page<LibraryMember> findAllLibraryMembersByLibraryId(@Param("libraryId")Long libraryId, Pageable pageable);
@Query("SELECT lm FROM LibraryMember lm
WHERE lm.library.id = :libraryId AND lm.member.id =:memberId")
LibraryMember findByLibrary_IdAndMember_Id(@Param("libraryId") Long libraryId, @Param("memberId") Long memberId);
}
3. 커스텀 Repository의 구현체인 LibraryMemberRepositorylmpl 클래스를
커스텀 Repository와 같은 디렉토리 내에 생성. => Custom 인터페이스 implements
<이곳에서 QueryDSL을 이용해 커스텀 메서드를 구현>
public class LibraryMemberRepositoryImpl implements LibraryMemberRepositoryCustom{
private final JPAQueryFactory queryFactory;
private QLibraryMember qLibraryMember = QLibraryMember.libraryMember;
public LibraryMemberRepositorylmpl(JPAQueryFactory queryFactory) {
this.queryFactory = queryFactory;
}
@Override
public List<LibraryMember> Q_DSL_findEveryLibraryMembersByLibraryId(Long libraryId) {
return queryFactory
.selectFrom(qLibraryMember)
.where(qLibraryMember.library.libraryId.eq(libraryId))
.fetch();
}
@Override
public LibraryMember Q_DSL_findLibraryMembersByLibrary_IdAndMember_Id(Long libraryId, Long memberId) {
return queryFactory
.selectFrom(qLibraryMember)
.where(qLibraryMember.library.libraryId.eq(libraryId)
.and(qLibraryMember.member.memberId.eq(memberId)))
.fetchOne();
}
}
fetch()
쿼리의 결과를 List 형태로 반환. 즉 쿼리에 일치하는 모든 결과를 반환. 일치하지 않으면 빈 List 반환
fetchOne()
쿼리의 결과를 단일 객체 형태로 반환. 즉, 쿼리에 일치하는 결과 중 첫번째 결과만 반환.
만약 일치하는 결과가 없다면 null 반환하고 2개 이상의 결과가 나오면 NonUniqueResultException 반환.
4. JPAQueryFactory를 Bean으로 등록해줄 QueryDslConfig 클래스 생성.
@Configuration
public class QuerydslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory(){
return new JPAQueryFactory(entityManager);
}
}
JPAQueryFactory는 QueryDsl에서 제공하는 클래스로,
JPA를 이용한 쿼리를 생성하고 실행하는데 필요한 메서드를 제공.
@Configuration 애너테이션
스프링이 애플리케이션을 시작할 때 자동으로 읽고, 클래스 안의 @Bean이 붙은 메서드는 스프링에서 관리.
즉, JPAQueryFactory를 Bean으로 등록하면, 앱 내부 어느곳에서나 JPAQueryFactory를 사용가능하다.
@PersistenceContext 애너테이션을 사용해서 EntityManager를 주입받는 이유
1. JPAQueryFactory를 생성할 때, EntityManager로 인자를 전달해야하기 때문.
2. EntityManager는 JPA의 핵심 인터페이스로, 엔티티 생성, 조회, 수정, 삭제 등을 관리하며,
JPAQueryFactory는 이 EntityManager를 통해 DB와 상호작용을 수행한다.
'백엔드 기술 > DataBase' 카테고리의 다른 글
SQL 문제 (0) | 2023.07.04 |
---|---|
SQL - JOIN 키워드 (0) | 2023.06.22 |
데이터베이스 - SQL, NoSQL, DB 설계 (0) | 2023.04.23 |