본문 바로가기
Framework/Spring Boot

[Spring Boot] Spring Data JPA의 마법: QueryMethod로 검색과 페이징을 박살내다

반응형

과거 JPA(Hibernate)만 순수하게 쓰던 시절에는 개발자가 직접 EntityManager를 불러와서 em.persist(), em.find() 같은 코드를 짰습니다. 이 정도만 해도 SQL을 안 짜서 행복했지만, 스프링 진영은 여기서 만족하지 않았습니다. "아니, 저장하고 찾고 삭제하는 CRUD 코드는 전 세계 모든 프로젝트가 다 똑같은데, 개발자가 왜 굳이 그 코드를 또 짜야 해? 우리가 다 만들어 줄게!"라는 미친 발상에서 탄생한 괴물이 바로 Spring Data JPA입니다.
이제 우리는 그저 JpaRepository<Entity, ID>라는 인터페이스 하나를 상속받는 것만으로 수십 개의 기본 쿼리 기능이 하늘에서 뚝 떨어지는 기적을 맛봅니다. 하지만 진짜 마법은 지금부터입니다. 만약 "나이가 20살 이상이고 이름이 '김'으로 시작하는 회원들을 가입일 순으로 내림차순 정렬해서 가져와 줘"라는 복잡한 조건을 원한다면 어떻게 할까요?

이번 포스팅에서는 "메서드 이름 자체가 곧 쿼리(Query)가 되는 마법!"
Spring Data JPA의 QueryMethod(쿼리 메서드) 완벽 정복과 실무 페이징(Paging) 노하우에 대하여 다뤄보겠습니다.

🪄 1. Query Method: 이름이 곧 쿼리(SQL)가 된다

Spring Data JPA는 클래스가 아니라 인터페이스(Interface)입니다. 개발자는 구현체를 짤 필요가 없습니다. 스프링이 인터페이스 안에 적혀있는 메서드의 '이름'을 파싱(분석)해서 자동으로 SQL 구문으로 번역해버립니다.

public interface UserRepository extends JpaRepository<User, Long> {
    // 1. 단순 검색 (SELECT * FROM user WHERE email = ?)
    Optional<User> findByEmail(String email);

    // 2. And, Or 조건 결합 (SELECT * FROM user WHERE name = ? AND age = ?)
    List<User> findByNameAndAge(String name, int age);

    // 3. 비교 연산자 (SELECT * FROM user WHERE age >= ?)
    List<User> findByAgeGreaterThanEqual(int age);

    // 4. 패턴 매칭 (SELECT * FROM user WHERE name LIKE '김%')
    List<User> findByNameStartingWith(String prefix);

    // 5. 정렬 (SELECT * FROM user WHERE status = ? ORDER BY createdAt DESC)
    List<User> findByStatusOrderByCreatedAtDesc(String status);
}

위 코드에서 구현 클래스(class)는 단 한 줄도 없습니다! 인터페이스에 메서드 이름만 선언해두면 끝납니다. find...By... 규칙만 지켜주면, 그 사이에 들어가는 필드명(Name, Age)과 조건자(And, GreaterThan, OrderBy)를 조합하여 무한한 쿼리를 찍어낼 수 있습니다. 이 얼마나 미친 생산성입니까?


📑 2. Pageable: 극한의 노가다, 페이징 처리를 1초 만에

백엔드 개발에서 가장 귀찮고 오류가 많이 나는 작업이 무엇일까요? 바로 '게시판 페이징(Pagination)' 처리입니다. "LIMIT 10 OFFSET 20, 전체 데이터 개수 구하기 위한 COUNT 쿼리 추가 작성, 총 페이지 수 계산..." 생각만 해도 머리가 아픕니다.

하지만 Spring Data JPA는 Pageable 객체 하나로 이 모든 지옥을 평정했습니다. 컨트롤러에서 Pageable 파라미터만 던져주면 알아서 쪼개줍니다.

// 1. Repository: 파라미터로 Pageable 객체만 툭 던져줍니다. 반환 타입은 List가 아닌 Page입니다!
public interface PostRepository extends JpaRepository<Post, Long> {
    // 스프링이 알아서 데이터 쿼리 + COUNT 쿼리 2방을 쏴서 계산해줍니다.
    Page<Post> findByTitleContaining(String keyword, Pageable pageable);
}

// 2. Controller: 프론트에서 ?page=0&size=10&sort=createdAt,desc 요청이 들어오면 스프링이 Pageable로 자동 변환해 줍니다!
@GetMapping("/posts")
public Page<Post> getPosts(
        @RequestParam String keyword, 
        Pageable pageable // 마법의 파라미터
) {
    // 프론트엔드는 총 데이터 개수(totalElements), 총 페이지 수(totalPages), 현재 페이지 등의 완벽한 JSON 결과를 받게 됩니다.
    return postRepository.findByTitleContaining(keyword, pageable);
}

🛠️ 3. 너무 복잡한 쿼리는? @Query로 직접 때리기

Query Method가 마법 같긴 하지만, 조건이 5~6개씩 넘어가면 메서드 이름이 findByNameAndAgeGreaterThanAndStatusAnd... 처럼 기차놀이 마냥 흉측하게 길어집니다. 이럴 때는 가독성을 위해 직접 JPQL(객체지향 쿼리)을 작성하는 @Query 어노테이션을 씁니다.

public interface UserRepository extends JpaRepository<User, Long> {
    // 메서드 이름은 내 마음대로 짧고 예쁘게 짓고, 쿼리는 @Query 안에 명시합니다.
    // 주의: 테이블명(users)이 아니라 클래스명(User)을 적어야 하는 JPQL 문법입니다!
    @Query("SELECT u FROM User u WHERE u.age >= :age AND u.name LIKE %:keyword%")
    List<User> searchCustomUsers(@Param("age") int age, @Param("keyword") String keyword);
}

🎯 4. 마무리 및 다음 단계

지금까지 코드 한 줄 안 짜고 메서드 이름 규칙만으로 수십 개의 쿼리를 자동 생성해 내는 Spring Data JPA의 Query Method 마법과, 개발자의 수명을 갉아먹던 페이징 처리를 단 한 큐에 끝내버리는 Pageable 인터페이스의 경이로움에 대해 다루어 보았습니다. 이로써 기본적인 CRUD와 검색, 페이징은 실무 레벨에서 완벽히 마스터했습니다.

그런데, @Query로 쿼리를 직접 짜다 보니 오타가 나거나 띄어쓰기를 잘못하면 '서버가 켜지고 나서야(런타임 에러)' 에러가 터지는 치명적인 문제가 발생합니다. 게다가 "이름이 넘어오면 이름으로 검색하고, 안 넘어오면 전체 검색해 줘" 같은 '동적 쿼리(Dynamic Query)'를 짤 때 문자열(String)을 더하기(+)로 덕지덕지 붙이다 보면 코드가 쓰레기장이 되어버립니다. 자바 백엔드 생태계는 이를 구원할 궁극의 무기를 준비했습니다. 이어지는 11단계 포스팅에서는 "오타를 내면 자바 컴파일러가 빨간 줄을 그어준다! 복잡한 동적 쿼리를 완벽한 자바 코드로 타입 세이프(Type-safe)하게 짜는 실무 끝판왕 기술!" QueryDSL 입문과 완벽 세팅 가이드에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!

반응형