Spring Boot 프로젝트를 진행하면서 JPA를 통해 데이터를 조회하는 다양한 방법들이 있습니다.
JPA는 정말 강력하지만, 때로는 어떤 방법을 써야 할지 고민될 때가 있습니다. Spring Data JPA에서 제공하는 데이터 조회 전략들을 하나씩 살펴보고, 언제 어떤 방법을 사용하는 것이 효과적인지 함께 알아보시죠!
JPA 데이터 조회, 무엇부터 시작해야 할까요?
JpaRepository 기본 메소드 활용
핵심: JpaRepository<EntityType, IDType>를 상속받아 사용합니다.
Spring Data JPA를 사용하면 가장 먼저 만나게 되는 것이 바로 JpaRepository 인터페이스입니다. 이 인터페이스를 상속받는 것만으로도 기본적인 CRUD(Create, Read, Update, Delete) 메소드와 페이징/정렬 기능을 손쉽게 사용할 수 있습니다.
public interface TodoRepository extends JpaRepository<TodoEntity, Long> {
// 기본적인 CRUD 메소드는 이미 제공됩니다!
// 예: save(), findById(), findAll(), deleteById() 등
}
JpaRepository 기본 메소드 활용 (+페이징)
가장 기본적인 방법입니다. findAll() 메소드에 Pageable 객체를 전달하면 페이징 처리가 가능합니다.
// TodoService.java (예시)
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
@Service
public class TodoService {
private final TodoRepository todoRepository;
public TodoService(TodoRepository todoRepository) {
this.todoRepository = todoRepository;
}
public Page<TodoEntity> getAllTodosWithPaging(int page, int size) {
// mno 필드를 기준으로 내림차순 정렬, 0번째 페이지부터 10개씩
Pageable pageable = PageRequest.of(page, size, Sort.by("mno").descending());
Page<TodoEntity> result = todoRepository.findAll(pageable);
return result;
}
}
쿼리 메소드 (Query Methods / Derived Queries)
메소드 이름을 특정 규칙에 맞게 작성하면 Spring Data JPA가 알아서 해당 JPQL 쿼리를 생성해주는 편리한 기능입니다.
규칙 예시
- findBy[속성이름]([타입] [값]): 특정 속성으로 조회
- findBy[속성이름]Like([타입] [값]): 특정 속성으로 LIKE 검색
- findBy[속성이름]And[다른속성이름](...): 여러 조건 AND
- countBy[속성이름](...): 개수 조회
- 페이징 처리는 메소드 파라미터에 Pageable 추가
public interface TodoRepository extends JpaRepository<TodoEntity, Long> {
// 1. findBy[속성이름]([타입] [값]): 특정 속성으로 정확히 일치하는 데이터 조회
// 예시: 제목(title)으로 정확히 일치하는 Todo 항목 조회
Optional<TodoEntity> findByTitle(String exactTitle);
// 2. findBy[속성이름]Like([타입] [값]): 특정 속성으로 LIKE 검색
// (주의: Like를 사용 시, 파라미터로 전달하는 값에 와일드카드(%)를 포함해야 함)
// 예시: 내용(content)에 특정 키워드가 포함된 Todo 항목 조회 (Pageable 적용)
// 실제 사용 시 서비스 로직에서 keyword 앞뒤로 "%"를 붙여서 전달합니다.
Page<TodoEntity> findByContentLike(String contentKeyword, Pageable pageable);
// 참고: Containing 키워드를 사용하면 자동으로 '%keyword%' 처리를 해줍니다.
// title 필드에 특정 문자열이 포함된 TodoEntity를 찾아 페이징
// Containing은 전달된 keyword 문자열이 Title 속성 값의 어느 위치에든 포함되어 있으면 참(true)으로 간주합니다.
Page<TodoEntity> findByTitleContaining(String keyword, Pageable pageable);
// 예시: 특정 작성자(author)가 작성하고 중요도(priority)가 특정 값 이상인 Todo 항목 조회
List<TodoEntity> findByAuthorAndPriorityGreaterThanEqual(String author, Integer minPriority);
}
Service에서 todoRepository.findByContentLike(searchKeyword, pageable) 와 같이 인터페이스에서 구현 한 내용을 적용합니다.
import com.example.todo.entity.TodoEntity;
import com.example.todo.repository.TodoRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class TodoService {
private final TodoRepository todoRepository;
@Autowired
public TodoService(TodoRepository todoRepository) {
this.todoRepository = todoRepository;
}
// 2. findByContentLike 사용 예시 (+ 페이징)
public void searchTodosByContentKeyword(String keyword, int pageNum, int pageSize) {
// Like 검색을 위해 키워드 앞뒤에 '%' 추가
String searchKeyword = "%" + keyword + "%";
Pageable pageable = PageRequest.of(pageNum, pageSize, Sort.by("id").descending());
Page<TodoEntity> todoPage = todoRepository.findByContentLike(searchKeyword, pageable);
System.out.println("내용에 '" + keyword + "' 포함된 할 일 (페이지: " + (pageNum + 1) + "):");
todoPage.getContent().forEach(System.out::println);
System.out.println("총 페이지 수: " + todoPage.getTotalPages() + ", 총 항목 수: " + todoPage.getTotalElements());
}
}
쿼리 메소드는 언제 주로 사용 할까요?
- 장점: 간단한 쿼리는 매우 쉽게 작성할 수 있습니다. 가독성도 좋습니다.
- 단점: 조건이 복잡해지면 메소드 이름이 매우 길어지고 관리가 어려워집니다. (그래서 위 내용처럼 개발에서 잘 사용하지 않는다는 의견도 있습니다. 하지만 간단한 경우에는 매우 유용합니다!)
- 언제 사용할까? 1~2개의 명확한 조건으로 조회할 때 유용합니다.
@Query 어노테이션을 이용한 JPQL 방식 / Native SQL Query 방식.
JPQL (Java Persistence Query Language)
SQL과 문법이 유사하지만, SQL이 아닙니다. JPA 명세의 일부로 정의되어 있습니다. 엔티티 객체 중심의 쿼리 언어입니다.
데이터베이스 테이블이 아닌, 매핑된 엔티티 객체와 그 속성(필드)을 대상으로 쿼리를 작성합니다.
- 지원 기능: JOIN, 서브쿼리, 그룹화(GROUP BY), 집계 함수(COUNT, SUM, AVG 등) 등 대부분의 SQL 기능을 지원합니다.
// TodoRepository.java
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
public interface TodoRepository extends JpaRepository<TodoEntity, Long> {
// JPQL 사용
@Query(value = "SELECT t FROM TodoEntity t WHERE t.mno > :minMno",
countQuery = "SELECT COUNT(t) FROM TodoEntity t WHERE t.mno > :minMno")
Page<TodoEntity> findTodosWithMinMno(Long minMno, Pageable pageable);
// TodoEntity에서 completed가 true인 항목들을 title 내림차순으로 조회
@Query("SELECT t FROM TodoEntity t WHERE t.completed = :status ORDER BY t.title DESC")
List<TodoEntity> findCompletedTodosOrderByTitleDesc(@Param("status") boolean status);
// TodoEntity와 연관된 MemberEntity를 함께 조회 (JOIN)
// (TodoEntity에 MemberEntity 타입의 member 필드가 있고, MemberEntity에 username 필드가 있다고 가정)
@Query("SELECT t FROM TodoEntity t JOIN FETCH t.member m WHERE m.username = :username")
List<TodoEntity> findTodosByUsernameWithMember(@Param("username") String username);
}
createNativeQuery (Native SQL Query)
JPQL과 다르게 데이터베이스에 직접 실행되는 표준 SQL(ANSI SQL) 또는 특정 데이터베이스의 방언(Dialect)을 사용하는 쿼리입니다. 따라서 특정 데이터베이스에 종속적일 수 있습니다. 즉, 사용하는 데이터베이스 종류가 변경되면 쿼리를 수정해야 할 가능성이 높습니다. (nativeQuery = true 필수!)
- 쿼리 대상: 실제 데이터베이스 테이블 이름 (예: SELECT * FROM todos t)
//// TodoEntity에서 completed 상태가 주어진 status 값과 일치하는 항목들을 todo_title 내림차순으로 조회
@Query(value = "SELECT * FROM todos t WHERE t.is_completed = :status ORDER BY t.todo_title DESC",
nativeQuery = true) // nativeQuery = true 필수!
List<TodoEntity> findCompletedTodosOrderByTitleDescNative(@Param("status") boolean status);
Querydsl ( JPAQueryFactory ,QuerydslRepositorySupport )
- 타입 안전한 쿼리를 작성할 수 있게 도와주는 자바 라이브러리입니다.
- SQL이나 JPQL처럼 문자열로 쿼리를 작성하는 대신, 자바 코드로 쿼리를 작성합니다.
- 컴파일 시점에 오류를 확인할 수 있어 안정성이 높습니다.
- 동적 쿼리 생성을 간편하게 할 수 있습니다.
build.gradle 의존성 설정 및 Q클래스 생성
💡 Q클래스란?
@Entity로 선언된 엔티티 클래스를 기반으로 Querydsl이 생성하는 메타모델 클래스입니다. 이 Q클래스를 통해 엔티티의 속성에 타입-세이프하게 접근할 수 있습니다. (예: TodoEntity -> QTodoEntity)
빌드 후 build/generated/querydsl 디렉토리 (경로 설정에 따라 다를 수 있음)에 Q로 시작하는 클래스들이 생성됩니다.
* build.gradle 예시
// build.gradle
// ... (기존 설정들)
buildscript {
ext {
queryDslVersion = "5.0.0" // 최신 버전 확인 권장
}
}
dependencies {
// ... (기존 의존성들)
//QueryDSL
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta"
annotationProcessor(
"jakarta.persistence:jakarta.persistence-api",
"jakarta.activation:jakarta.activation-api",
"com.querydsl:querydsl-apt:${queryDslVersion}:jakarta"
)
}
// Q클래스 생성 경로 지정 (선택 사항, 기본은 build/generated/querydsl)
// sourceSets {
// main {
// java {
// srcDirs = ['src/main/java', 'build/generated/querydsl']
// }
// }
// }
// [중요!] Q클래스 생성을 위한 Gradle Task 실행
// 방법 1: IntelliJ 우측 Gradle 탭 -> Tasks -> other -> compileJava 실행
// 방법 2: 터미널에서 ./gradlew compileJava (또는 ./gradlew build) 실행
* build 에서 other-complie.java를 실행하면, Q로 시작하는 클래스들이 build 파일에 생성됩니다. *
JPAQueryFactory를 Bean으로 등록하여 사용하는 방식
- JPAQueryFactory는 Querydsl 쿼리를 생성하고 실행하는 데 사용됩니다. @PersistenceContext 를 이용하여 Spring Bean으로 등록해야 합니다
@Configuration
public class QuerydslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
2.@Repository 클래스에서 private final JPAQueryFactory queryFactory; 와 같이
JPAQueryFactory 타입의 필드를 선언 하 고 @RequiredArgsConstructor 어노테이션을 추가합니다.
@Repository
@RequiredArgsConstructor
public class MemberRepository {
private final JPAQueryFactory queryFactory;
public List<Member> findByName(String name) {
QMember member = QMember.member; // QMember는 QType 클래스
return queryFactory.selectFrom(member)
.where(member.name.eq(name)) // where 조건
.fetch(); // 결과 조회
}
}
위는 기본 사용 방법이며 JPAQueryFactory 방식을 사용하기 위해서는 아래 상황들도 고려 해야합니다.
[참 고 사 항]
- QType 생성 및 관리: Querydsl은 QType을 사용하여 타입 안전한 쿼리를 생성합니다. 프로젝트의 엔티티 클래스에 대한 QType 클래스가 정상적으로 생성되고 관리되는지 확인해야 합니다. 보통 빌드 과정에서 자동으로 생성되지만, IDE 설정에 따라 수동으로 생성하거나 소스 폴더를 추가해야 할 수도 있습니다.
- 쿼리 작성 스타일 및 가독성: 단순히 쿼리를 실행하는 것뿐만 아니라, 효율적이고 가독성 높은 쿼리를 작성하는 것이 중요합니다. 적절한 메서드 체이닝, BooleanBuilder 활용, Alias 지정 등을 통해 쿼리의 의도를 명확하게 표현하고 유지보수성을 높여야 합니다.
- 페이징 처리: JPAQueryFactory는 자체적인 페이징 기능을 제공하지 않습니다. 따라서 offset()과 limit() 메서드를 사용하거나, Count 쿼리를 별도로 실행하여 페이징을 직접 구현해야 합니다. Spring Data의 Pageable 객체와 함께 사용하는 방법을 숙지하는 것이 좋습니다.
- 성능 최적화: 복잡한 쿼리의 경우 성능 최적화가 중요합니다. 인덱스 활용, 불필요한 조인 제거, 적절한 fetch join 사용 등을 통해 쿼리 성능을 개선해야 합니다.
- 테스트 코드 작성: Querydsl 쿼리에 대한 테스트 코드를 작성하여 쿼리의 정확성과 안정성을 확보해야 합니다. JPAQueryFactory를 Mock 객체로 주입하여 테스트하는 방법을 숙지하는 것이 좋습니다.
- 트랜잭션 관리: JPAQueryFactory는 트랜잭션 관리를 직접 담당하지 않습니다. Spring의 @Transactional 어노테이션을 사용하여 트랜잭션을 관리해야 합니다.
▶ JPAQueryFactory 를 이용한 페이징 처리 +
public interface MemberRepositoryCustom {
Page<MemberTeamDto> searchPage(MemberSearchCondition condition, Pageable pageable);
}
위 인터페이스를 활용하여 PageableExecutionUtils.getPage() 메서드를 사용하는 방식은 효율적인 페이징 처리를 위한 좋은 접근 방식입니다.
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public Page<MemberTeamDto> searchPage(MemberSearchCondition condition, Pageable pageable) {
List<MemberTeamDto> content = queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name))
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.offset(pageable.getOffset()) // 몇 번째 페이지부터 시작할 것 인지.
.limit(pageable.getPageSize()) // 페이지당 몇개의 데이터를 보여줄껀지
.fetch();
JPAQuery<Long> countQuery = queryFactory
.select(member.count())
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()));
return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchOne());
}
사용방식 - QuerydslRepositorySupport를 상속하는 방식:
QuerydslRepositorySupport는 Querydsl을 Spring Data JPA 환경에서 사용하는 한 가지 방법을 제공하지만, 여러 가지 단점으로 인해 권장되지 않는 방식입니다. QuerydslRepositorySupport를 사용하는 방식과 그 단점, 그리고 대안에 대해 자세히 설명드리겠습니다.
- QuerydslRepositorySupport를 상속하면 JpaRepository가 제공하는 기본적인 CRUD 메서드들을 직접 사용할 수 없습니다.따라서 JpaRepository의 편리함과 기능을 포기해야 합니다.
- . QuerydslRepositorySupport를 사용하면 이러한 Spring Data JPA 생태계의 기능들을 활용하기 어렵습니다. 예를 들어, @Transactional 어노테이션을 사용하여 트랜잭션을 관리하는 경우, QuerydslRepositorySupport 내부에서 사용하는 EntityManager와 충돌이 발생할 수 있습니다. 이는 예상치 못한 동작이나 오류로 이어질 수 있습니다.
1.커스텀 레포지토리 인터페이스 정의
public interface TodoSearch {
List<Member> findByName(String name);
// ... 다른 커스텀 쿼리 메서드
}
2. Impl 에서 인터페이스 사용하기 및 QuerydslRepositorySupport 상속.
- QuerydslRepositorySupport: JpaRepository와는 다른 방식으로 Querydsl을 사용합니다. EntityManager를 직접 주입받지 않고, QuerydslRepositorySupport가 제공하는 from(), getQuerydsl() 등의 메서드를 활용합니다.
public class TodoSearchImpl extends QuerydslRepositorySupport implements TodoSearch {
public TodoSearchImpl(){
super(TodoEntity.class);
}
public Page<TodoEntity> search1(Pageable pageable) {
log.info("search1-------------");
QTodoEntity todoEntity = QTodoEntity.todoEntity;
JPQLQuery<TodoEntity> query = from(todoEntity);
query.where(todoEntity.mno.gt(0L));
getQuerydsl().applyPagination(pageable,query);
java.util.List<TodoEntity> entityList = query.fetch();
long count = query.fetchCount();
return new PageImpl<>(entityList,pageable,count);
}
'개발일지 > SPRINGBOOT' 카테고리의 다른 글
Builder 패턴을 적용한 엔터티 (2) | 2025.06.08 |
---|---|
JPA , 값 객체는 무엇이고 어떻게 활용하는가? (1) | 2025.06.04 |
JPA 에서 @Entity 간 연관 관계 (1) | 2025.06.04 |
예외 처리 , Springboot @RestControllerAdvice (1) | 2025.05.22 |
Spring Boot JPA 영속성 완벽 가이드: 엔티티 생명주기 이해하기 (2) | 2025.05.17 |