Spring Boot과 Spring Data JPA의 효율적 데이터 접근 패턴
Spring Boot와 Spring Data JPA를 활용한 효율적인 데이터 접근 패턴과 페이징·정렬, 쿼리 최적화 방법을 초보자도 이해하기 쉽게 정리한 기술자료
목차
개요
Spring Boot 환경에서 데이터 접근은 애플리케이션 성능과 유지보수성에 직접적인 영향을 준다. Spring Data JPA는 개발 생산성을 높여준다. 다만 기본 사용만으로는 성능 병목이 발생할 수 있다. 이 문서에서는 spring data jpa 사용법을 기본으로 하여, 페이징·정렬과 쿼리 최적화 관점에서 실무적으로 적용 가능한 패턴을 정리한다.
핵심 원칙
- 도메인 중심 설계: 엔티티는 도메인 모델로 설계한다.
- 읽기와 쓰기를 분리: 읽기 집중 API는 DTO 투영을 적극 활용한다.
- 지연로딩과 즉시로딩을 상황별로 선택한다.
- 쿼리 최적화는 측정(Profiling) 후 적용한다.
Repository 설계
Repository는 단순한 CRUD 이상의 책임을 지지 않게 설계한다. 복잡한 조회 로직은 커스텀 리포지토리 또는 QueryDSL/JPQL로 분리한다. 이렇게 하면 테스트와 재사용이 쉬워진다.
예: 커스텀 리포지토리 인터페이스
@Repository
public interface OrderRepository extends JpaRepository<Order, Long>, OrderRepositoryCustom {
}
public interface OrderRepositoryCustom {
List<OrderSummary> findRecentOrders(int days);
}
public class OrderRepositoryImpl implements OrderRepositoryCustom {
@PersistenceContext
private EntityManager em;
@Override
public List<OrderSummary> findRecentOrders(int days) {
return em.createQuery("SELECT new com.example.dto.OrderSummary(o.id, o.total) FROM Order o WHERE o.createdAt > CURRENT_DATE - :days", OrderSummary.class)
.setParameter("days", days)
.getResultList();
}
}
페이징과 정렬
대용량 데이터 조회는 반드시 페이징을 적용한다. Spring Data JPA의 Pageable을 활용하면 페이징과 정렬을 한 번에 처리할 수 있다. 다만 offset 기반 페이징은 페이지가 깊어지면 성능이 떨어진다. 이 경우 keyset 페이징을 고려한다.
예: Pageable 사용
Page<Order> findByStatus(String status, Pageable pageable);
// 사용 예
Pageable pageable = PageRequest.of(0, 20, Sort.by("createdAt").descending());
Page<Order> page = orderRepository.findByStatus("COMPLETED", pageable);
Keyset 페이징
keyset 페이징은 마지막 항목의 키를 기반으로 다음 페이지를 조회한다. OFFSET 기반 페이징보다 성능이 우수하다. QueryDSL이나 네이티브 쿼리를 통해 구현한다.
쿼리 최적화
쿼리 최적화는 세 단계로 접근한다: 문제 발견, 원인 분석, 해결 적용. 먼저 쿼리 로그와 실행 계획을 확인한다. 그다음 N+1 문제, 불필요한 컬럼 조회, 잘못된 인덱스 사용 여부를 점검한다.
N+1 문제 해결
연관 엔티티 조회 시 N+1 문제가 흔하다. @EntityGraph 또는 fetch join으로 해결한다. 단, 페이징과 함께 fetch join을 쓸 때는 주의가 필요하다.
@Query("SELECT o FROM Order o JOIN FETCH o.customer WHERE o.id = :id")
Optional<Order> findWithCustomerById(@Param("id") Long id);
// 또는
@EntityGraph(attributePaths = {"customer"})
Optional<Order> findById(Long id);
프로젝션과 DTO
전체 엔티티가 필요하지 않은 경우 DTO 투영을 사용한다. 생성자 표현식(new ... )이나 인터페이스 기반 프로젝션을 선택할 수 있다. 이는 전송 데이터량을 줄이고 쿼리 비용을 낮춘다.
대량 연산과 배치 처리
대량 저장이나 업데이트는 JPA의 flush와 clear를 적절히 사용해 메모리 사용을 제어한다. spring data jpa 쿼리 최적화 측면에서 배치사이즈 설정과 벌크 연산은 필수다.
for (int i = 0; i < entities.size(); i++) {
em.persist(entities.get(i));
if (i % batchSize == 0) {
em.flush();
em.clear();
}
}
트랜잭션과 읽기전용 최적화
읽기 전용 트랜잭션(readOnly=true)은 데이터베이스와 JPA 구현체에게 힌트를 준다. 변경 감지 비용을 줄여 성능에 도움이 된다. 단, 쓰기 작업에서는 사용하지 않는다.
모니터링과 테스트
쿼리 실행 시간을 지속적으로 모니터링한다. 로깅 설정과 APM을 활용한다. 또한 주요 리포지토리는 인메모리 DB 또는 테스트 컨테이너로 통합 테스트를 수행해 성능 회귀를 방지한다.
결론
정리하면, spring data jpa 사용법을 단순히 아는 것을 넘어서서 페이징·정렬, N+1 회피, DTO 투영, 배치 처리 등 실무 패턴을 적용해야 효율적이다. 먼저 측정하고 문제를 파악한 뒤 적절한 패턴을 선택하면 안정적인 성능을 유지할 수 있다.