And Brain said,

QueryDSL, JPA ORM의 최종장 본문

IT/Java & Kotlin & Spring boot

QueryDSL, JPA ORM의 최종장

The Man 2023. 3. 28. 22:47
반응형

오늘은 간단히 JPA ORM 부터 나아가 QueryDSL 까지 배워보도록 하자.

 

이전에 이미 한 번 포스팅한 적이 있었지만, 미흡한 부분이 많아 다시 한 번 배워보자.

 

 

먼저, JPA ORM부터 살펴보자.

 

JPA ORM은 Java Persistence API를 기반으로 하는 객체-관계 매핑(Object-Relational Mapping) 기술로, Java 객체와 관계형 데이터베이스 사이의 매핑을 자동화해 주는 프레임워크다.

 

이를 통해 개발자들은 SQL 쿼리를 직접 작성하는 것이 아니라, Java 객체를 사용하여 데이터베이스 작업을 수행할 수 있다.

 

이렇게 SQL 쿼리를 작성하지 않고도 데이터베이스 작업을 수행할 수 있어 개발자들의 생산성이 향상되고, 코드 가독성 및 유지보수성 증가 및 데이터베이스 독립성을 가질 수 있어 매우 유용하다.


그러나 JPA ORM만으로는 복잡한 쿼리 작업에 제한이 있으며, 이러한 단점을 해결하기 위해 QueryDSL이라는 라이브러리가 등장했다.

 

QueryDSL은 JPA ORM과 함께 사용되어 동적 쿼리 생성, Type-Safe 코드 작성 등의 기능을 제공하여, 개발자들은 더 간편하고 안전한 방법으로 복잡한 쿼리를 작성할 수 있게 되었다.

 

자, 그럼 본론으로 들어가보자.

 

간단한 도서 관리 시스템을 만들 것이다.

 

도메인은 Author와 Book이다.

 

// Author Class

@Entity
@Getter
@NoArgsConstructor
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String country;

    // ... 생성자, 메서드 등 생략
}

 

// Book Class

@Entity
@Getter
@NoArgsConstructor
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private String genre;

    private Integer publicationYear;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id")
    private Author author;

    // ... 생성자, 메서드 등 생략
}

 

 

이제, AuthorRepository와 BookRepository 인터페이스를 생성하고, 각각 JpaRepository를 상속받도록 하자.

 

Author 엔티티를 다루기 위한 기본적인 CRUD 메서드를 사용할 수 있도록 AuthorRepository 인터페이스를 생성하고, JpaRepository를 상속받는다.

public interface AuthorRepository extends JpaRepository<Author, Long> {
}

 

 

다음은, Book 엔티티를 다루기 위해 BookRepository 인터페이스를 생성한다.

 

먼저, 사용자 정의 쿼리가 필요하므로, BookRepositoryCustom 인터페이스를 생성해보자.

public interface BookRepositoryCustom {
    // 여기에 사용자 정의 쿼리 메서드를 선언
    List<Book> findByAuthorName(String authorName);
    List<Book> findBooksByGenreAndPublicationYear(String genre, Integer publicationYear);
}

 

이제 BookRepository 인터페이스에 BookRepositoryCustom 인터페이스를 상속하여, 사용자 정의 쿼리와 기본 CRUD 메서드를 함께 사용할 수 있도록 해보자.

 

public interface BookRepository extends JpaRepository<Book, Long>, BookRepositoryCustom {
}

 

마지막으로, BookRepositoryImpl 클래스를 생성하고 사용자 정의 쿼리를 작성한다.

 

여기서는 JPAQueryFactory를 사용하여 QueryDSL 쿼리를 작성해보도록 하자.

 

@RequiredArgsConstructor
public class BookRepositoryImpl implements BookRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    @Override
    public List<Book> findByAuthorName(String authorName) {
        QBook book = QBook.book;
        QAuthor author = QAuthor.author;

        return queryFactory
                .selectFrom(book)
                .join(book.author, author)
                .where(author.name.eq(authorName))
                .fetch();
    }

    @Override
    public List<Book> findBooksByGenreAndPublicationYear(String genre, Integer publicationYear) {
        QBook book = QBook.book;

        return queryFactory
                .selectFrom(book)
                .where(book.genre.eq(genre).and(book.publicationYear.eq(publicationYear)))
                .fetch();
    }
}

 

이런 식으로 작성을 완료했다면, DTO를 만들어보자.

 

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class BookDTO {
    private Long id;
    private String title;
    private String genre;
    private Integer publicationYear;
    private String authorName;
    private String authorCountry;

    public static BookDTO fromEntity(Book book) {
        return new BookDTO(
                book.getId(),
                book.getTitle(),
                book.getGenre(),
                book.getPublicationYear(),
                book.getAuthor().getName(),
                book.getAuthor().getCountry()
        );
    }
}

 

남은건 이제 서비스 레이어와 컨트롤러 레이어에서 Repository를 사용하는 것만이 남았다.

 

// Service Layer

@Service
@RequiredArgsConstructor
public class BookService {

    private final BookRepository bookRepository;

    public List<BookDTO> findByAuthorName(String authorName) {
        List<Book> books = bookRepository.findByAuthorName(authorName);
        return books.stream().map(BookDTO::fromEntity).collect(Collectors.toList());
    }

    public List<BookDTO> findBooksByGenreAndPublicationYear(String genre, Integer publicationYear) {
        List<Book> books = bookRepository.findBooksByGenreAndPublicationYear(genre, publicationYear);
        return books.stream().map(BookDTO::fromEntity).collect(Collectors.toList());
    }

    // 다른 CRUD 메서드들 구현
}
// Controller Layer

@RestController
@RequestMapping("/api/v1/books")
@RequiredArgsConstructor
public class BookController {

    private final BookService bookService;

    @GetMapping("/author/{authorName}")
    public ResponseEntity<List<BookDTO>> findByAuthorName(@PathVariable String authorName) {
        List<BookDTO> books = bookService.findByAuthorName(authorName);
        return ResponseEntity.ok(books);
    }

    @GetMapping("/genre/{genre}/year/{publicationYear}")
    public ResponseEntity<List<BookDTO>> findBooksByGenreAndPublicationYear(
            @PathVariable String genre, @PathVariable Integer publicationYear) {
        List<BookDTO> books = bookService.findBooksByGenreAndPublicationYear(genre, publicationYear);
        return ResponseEntity.ok(books);
    }

    // 다른 CRUD API 구현
}

여기까지 아주 간단하게 JPA와 QueryDSL을 사용하여 도서관리 시스템을 만들어냈다.

 

 

마지막으로,

 

JPA ORM과 QueryDSL은 굉장히 매력적이지만, 여전히 QueryDSL은 JPA에 의존하므로 몇 가지 제한 사항이 존재하며, 성능이 강조되는 미세한 쿼리튜닝이나 복잡한 표현식에 있어서 여전히 한계는 존재하고 이럴 경우엔 Native Query가 필요하다.
이러한 이유로, JPA와 MyBatis를 동시에 사용하는 방안을 고려해 볼 만 하다.

 

복잡한 조회 작업을 수행하는 도메인에서는 CUD와 CQRS(Command Query Responsibility Segregation) 패턴을 사용하여 명령과 조회의 책임을 분리함으로써 ORM의 장점을 최대한 활용할 수도 있다.

다만, 프로젝트 규모가 크지 않은 경우에는 CQRS 패턴을 사용하지 않더라도 다양한 대안이 존재한다.

개발 속도 측면에서 JPA는 Mybatis Mapper보다 우월하므로 ORM을 포기할 이유는 없다.

결국, 적재적소에 맞는 기술, 언어 및 데이터베이스를 선택하여 적절하게 사용할 수 있도록 하자.

 

자, 이제 여러분의 프로젝트에 JPA와 QueryDSL을 도입하여 그 매력을 느껴보시길 바란다.

 

 

Thanks for watching, Have a nice day.

반응형
Comments