[JPA] Spring Data JPA 페이징 (3/3) - 성능 최적화와 테스트

2025. 12. 13. 07:57·공부일기../JPA

시리즈!

[JPA] Spring Data JPA 페이징 (1/3) - 기본 개념과 핵심 객체

[JPA] Spring Data JPA 페이징(2/3) - 실무 패턴과 동적 쿼리

[JPA] Spring Data JPA 페이징 (3/3) - 성능 최적화와 테스트

 

 

1. 페이징 성능 최적화

1-1. COUNT 쿼리 최적화

Page를 사용할 때 COUNT 쿼리가 자동으로 실행되는데, 복잡한 조인이 포함되면 성능 문제가 발생한다.

// 문제 상황: 복잡한 조인이 COUNT 쿼리에도 포함됨
@Query("SELECT p FROM Product p " +
       "LEFT JOIN FETCH p.category c " +
       "LEFT JOIN FETCH p.supplier s " +
       "LEFT JOIN FETCH p.images i " +
       "WHERE p.isDeleted = :isDeleted")
Page<Product> findAllWithDetails(@Param("isDeleted") String isDeleted, Pageable pageable);

// 실행되는 쿼리
// 1. 데이터 조회
// SELECT p.*, c.*, s.*, i.* 
// FROM product p 
// LEFT JOIN category c ON p.category_id = c.id
// LEFT JOIN supplier s ON p.supplier_id = s.id
// LEFT JOIN product_image i ON p.id = i.product_id
// WHERE p.is_deleted = 'N'
// LIMIT 20 OFFSET 0;

// 2. COUNT 쿼리 (불필요한 조인 포함)
// SELECT COUNT(p) 
// FROM product p 
// LEFT JOIN category c ON p.category_id = c.id
// LEFT JOIN supplier s ON p.supplier_id = s.id
// LEFT JOIN product_image i ON p.id = i.product_id
// WHERE p.is_deleted = 'N';

해결 방법은 COUNT 쿼리를 별도로 지정하는 것이다.

// 해결: COUNT 쿼리 분리
@Query(
    value = "SELECT p FROM Product p " +
            "LEFT JOIN FETCH p.category c " +
            "LEFT JOIN FETCH p.supplier s " +
            "LEFT JOIN FETCH p.images i " +
            "WHERE p.isDeleted = :isDeleted",
    countQuery = "SELECT COUNT(p) FROM Product p " +
                 "WHERE p.isDeleted = :isDeleted"  // 조인 제거
)
Page<Product> findAllWithDetails(@Param("isDeleted") String isDeleted, Pageable pageable);

// COUNT 쿼리가 단순해짐
// SELECT COUNT(p.id) 
// FROM product p 
// WHERE p.is_deleted = 'N';
// 조인이 없어서 훨씬 빠름

성능 차이는 다음과 같다.

// 데이터 10만 건 기준 성능 측정
// 최적화 전: COUNT 쿼리 500ms (복잡한 조인 포함)
// 최적화 후: COUNT 쿼리 50ms (단순 COUNT만)
// 10배 성능 향상

1-2. 커버링 인덱스 활용

필요한 컬럼만 조회하여 인덱스만으로 쿼리를 처리할 수 있도록 최적화한다.

// 비효율적: 모든 컬럼 조회
@Query("SELECT p FROM Product p WHERE p.category.id = :categoryId")
Page<Product> findByCategoryId(@Param("categoryId") Long categoryId, Pageable pageable);

// 실행되는 쿼리
// SELECT p.id, p.name, p.price, p.description, p.stock, p.created_at, ... (모든 컬럼)
// FROM product p 
// WHERE p.category_id = ?
// LIMIT 20 OFFSET 0;
// 인덱스 이후 테이블 접근 필요

// 효율적: 필요한 컬럼만 조회 (DTO 프로젝션)
@Query("SELECT new com.server.smzshop.product.dto.ProductSimpleDto(" +
       "p.id, p.name, p.price) " +
       "FROM Product p " +
       "WHERE p.category.id = :categoryId")
Page<ProductSimpleDto> findSimpleByCategoryId(
    @Param("categoryId") Long categoryId, 
    Pageable pageable
);

// 실행되는 쿼리
// SELECT p.id, p.name, p.price 
// FROM product p 
// WHERE p.category_id = ?
// LIMIT 20 OFFSET 0;

// 인덱스 생성
// CREATE INDEX idx_product_category_covering 
// ON product(category_id, id, name, price);
// 이 인덱스만으로 쿼리 완료 (테이블 접근 불필요)

커버링 인덱스를 사용하면 성능이 크게 향상된다.

// 성능 비교 (데이터 10만 건)
// 전체 컬럼 조회: 100ms (인덱스 + 테이블 접근)
// 커버링 인덱스 활용: 20ms (인덱스만 접근)
// 5배 성능 향상

 


1-3. N+1 문제 해결

페이징 조회 시 연관 엔티티를 함께 조회할 때 N+1 문제가 발생할 수 있다.

// N+1 문제 발생
@Query("SELECT p FROM Product p WHERE p.isDeleted = :isDeleted")
Page<Product> findAll(@Param("isDeleted") String isDeleted, Pageable pageable);

// Service
public Page<ProductResponse> getProducts(Pageable pageable) {
    Page<Product> productPage = productRepository.findAll("N", pageable);
    
    return productPage.map(product -> ProductResponse.builder()
        .id(product.getId())
        .name(product.getName())
        .categoryName(product.getCategory().getName())  // N+1 발생!
        .build()
    );
}

// 실행되는 쿼리
// 1. Product 20건 조회
// SELECT * FROM product WHERE is_deleted = 'N' LIMIT 20;

// 2. 각 Product마다 Category 조회 (20번)
// SELECT * FROM category WHERE id = ?;
// SELECT * FROM category WHERE id = ?;
// ...
// 총 21개 쿼리 실행

해결 방법은 여러 가지가 있다.

// 해결 방법 1: JOIN FETCH 사용
@Query("SELECT p FROM Product p " +
       "LEFT JOIN FETCH p.category c " +
       "WHERE p.isDeleted = :isDeleted")
Page<Product> findAllWithCategory(@Param("isDeleted") String isDeleted, Pageable pageable);

// 실행되는 쿼리 (1개로 줄어듦)
// SELECT p.*, c.* 
// FROM product p 
// LEFT JOIN category c ON p.category_id = c.id
// WHERE p.is_deleted = 'N'
// LIMIT 20;

// 해결 방법 2: @EntityGraph 사용
@EntityGraph(attributePaths = {"category"})
@Query("SELECT p FROM Product p WHERE p.isDeleted = :isDeleted")
Page<Product> findAllWithCategory(@Param("isDeleted") String isDeleted, Pageable pageable);

// 해결 방법 3: @BatchSize 사용 (OneToMany 관계에 적합)
@Entity
public class Product {
    @ManyToOne(fetch = FetchType.LAZY)
    private Category category;
    
    @BatchSize(size = 100)
    @OneToMany(mappedBy = "product")
    private List<ProductImage> images;
}

// images를 조회할 때 IN 쿼리로 100개씩 묶어서 조회
// SELECT * FROM product_image WHERE product_id IN (?, ?, ..., ?);  // 100개씩

1-4. No Offset 페이징

OFFSET 방식의 성능 문제를 근본적으로 해결하는 방법이다.

// OFFSET 방식의 문제
// 1000페이지 조회: OFFSET 20000 (page=1000, size=20)
// SELECT * FROM product ORDER BY id LIMIT 20 OFFSET 20000;
// DB가 20000건을 읽고 건너뛴 후 20건 반환
// 페이지가 뒤로 갈수록 기하급수적으로 느려짐

// No Offset 방식
@Query("SELECT p FROM Product p " +
       "WHERE p.id > :lastId " +
       "AND p.isDeleted = :isDeleted " +
       "ORDER BY p.id ASC")
List<Product> findNextPage(
    @Param("lastId") Long lastId,
    @Param("isDeleted") String isDeleted,
    Pageable pageable
);

// 실행되는 쿼리
// SELECT * FROM product 
// WHERE id > 980 AND is_deleted = 'N'
// ORDER BY id ASC 
// LIMIT 20;
// OFFSET 없이 WHERE로 필터링
// 인덱스를 효율적으로 사용

No Offset 페이징의 성능 비교는 다음과 같다.

// 성능 비교 (데이터 100만 건)
// OFFSET 방식
// 1페이지: OFFSET 0 → 10ms
// 100페이지: OFFSET 2000 → 50ms
// 1000페이지: OFFSET 20000 → 500ms
// 10000페이지: OFFSET 200000 → 5000ms (급격히 느려짐)

// No Offset 방식
// 1페이지: WHERE id > 0 → 10ms
// 100페이지: WHERE id > 98000 → 10ms
// 1000페이지: WHERE id > 980000 → 10ms
// 10000페이지: WHERE id > 9980000 → 10ms (일정한 성능)

 


2. 페이징 테스트 작성

2-1. Repository 테스트

Repository 계층은 실제 데이터베이스와 연동하여 테스트한다.

@SpringBootTest
@Transactional  // 테스트 후 자동 롤백
class ProductRepositoryTest {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Test
    void 페이징_조회_테스트() {
        // given: 테스트 데이터 15개 생성
        List<Product> testProducts = createTestProducts(15);
        testProducts.forEach(productRepository::save);
        
        // when: 첫 페이지 조회 (0페이지, 5개씩)
        PageRequest pageable = PageRequest.of(0, 5, Sort.by("id").ascending());
        Page<Product> result = productRepository.findAllByIsDeleted("N", pageable);
        
        // then: 페이징 결과 검증
        assertThat(result.getTotalElements()).isEqualTo(15);  // 전체 데이터 15개
        assertThat(result.getContent()).hasSize(5);           // 현재 페이지 5개
        assertThat(result.getTotalPages()).isEqualTo(3);      // 총 3페이지
        assertThat(result.getNumber()).isEqualTo(0);          // 0페이지
        assertThat(result.isFirst()).isTrue();                // 첫 페이지
        assertThat(result.isLast()).isFalse();                // 마지막 아님
        assertThat(result.hasNext()).isTrue();                // 다음 페이지 있음
    }
    
    @Test
    void 두번째_페이지_조회_테스트() {
        // given
        List<Product> testProducts = createTestProducts(15);
        testProducts.forEach(productRepository::save);
        
        // when: 1페이지 조회 (두 번째 페이지)
        PageRequest pageable = PageRequest.of(1, 5, Sort.by("id").ascending());
        Page<Product> result = productRepository.findAllByIsDeleted("N", pageable);
        
        // then
        assertThat(result.getNumber()).isEqualTo(1);       // 1페이지
        assertThat(result.getContent()).hasSize(5);        // 5개
        assertThat(result.isFirst()).isFalse();            // 첫 페이지 아님
        assertThat(result.isLast()).isFalse();             // 마지막도 아님
        assertThat(result.hasNext()).isTrue();             // 다음 있음
        assertThat(result.hasPrevious()).isTrue();         // 이전 있음
    }
    
    @Test
    void 마지막_페이지_조회_테스트() {
        // given
        List<Product> testProducts = createTestProducts(12);
        testProducts.forEach(productRepository::save);
        
        // when: 2페이지 조회 (마지막 페이지, 2개만 있음)
        PageRequest pageable = PageRequest.of(2, 5);
        Page<Product> result = productRepository.findAllByIsDeleted("N", pageable);
        
        // then
        assertThat(result.isLast()).isTrue();                     // 마지막 페이지
        assertThat(result.getNumberOfElements()).isEqualTo(2);    // 2개만 있음
        assertThat(result.hasNext()).isFalse();                   // 다음 없음
    }
    
    @Test
    void 정렬_조건_테스트() {
        // given
        List<Product> testProducts = createTestProducts(10);
        testProducts.forEach(productRepository::save);
        
        // when: 가격 내림차순 정렬
        PageRequest pageable = PageRequest.of(0, 5, 
            Sort.by("price").descending()
                .and(Sort.by("id").ascending())
        );
        Page<Product> result = productRepository.findAllByIsDeleted("N", pageable);
        
        // then: 가격이 높은 순으로 정렬되었는지 확인
        List<Product> content = result.getContent();
        for (int i = 0; i < content.size() - 1; i++) {
            assertThat(content.get(i).getPrice())
                .isGreaterThanOrEqualTo(content.get(i + 1).getPrice());
        }
    }
    
    private List<Product> createTestProducts(int count) {
        List<Product> products = new ArrayList<>();
        for (int i = 1; i <= count; i++) {
            Product product = Product.builder()
                .name("상품" + i)
                .price(BigDecimal.valueOf(1000 * i))
                .stock(10)
                .description("상품 설명" + i)
                .isDeleted("N")
                .build();
            products.add(product);
        }
        return products;
    }
}

2-2. Service 테스트 (Mock)

Service 계층은 Mock을 사용하여 단위 테스트를 작성한다.

@ExtendWith(MockitoExtension.class)
class ProductServiceTest {
    
    @InjectMocks
    private ProductService productService;
    
    @Mock
    private ProductRepository productRepository;
    
    @Test
    void 페이징_조회_서비스_테스트() {
        // given: Mock 데이터 준비
        List<Product> mockProducts = createTestProducts(3);
        Pageable pageable = PageRequest.of(0, 3);
        
        // PageImpl로 가짜 Page 객체 생성
        Page<Product> mockPage = new PageImpl<>(
            mockProducts,    // 현재 페이지 데이터 (3개)
            pageable,        // 페이징 정보
            10               // 전체 데이터는 10개라고 가정
        );
        
        // Repository의 동작 정의
        when(productRepository.findAllByIsDeleted("N", pageable))
            .thenReturn(mockPage);
        
        // when: Service 메서드 호출
        Page<ProductResponse> result = productService.getProducts(pageable);
        
        // then: 결과 검증
        assertThat(result.getContent()).hasSize(3);              // 3개 조회됨
        assertThat(result.getTotalElements()).isEqualTo(10);     // 전체 10개
        assertThat(result.getTotalPages()).isEqualTo(4);         // 10개/3개 = 4페이지
        assertThat(result.getNumber()).isEqualTo(0);             // 0페이지
        
        // Repository가 올바르게 호출되었는지 검증
        verify(productRepository, times(1))
            .findAllByIsDeleted("N", pageable);
    }
    
    @Test
    void 동적_검색_서비스_테스트() {
        // given
        ProductSearchCondition condition = ProductSearchCondition.builder()
            .name("노트북")
            .minPrice(BigDecimal.valueOf(500000))
            .maxPrice(BigDecimal.valueOf(2000000))
            .build();
        
        Pageable pageable = PageRequest.of(0, 10);
        
        List<Product> mockProducts = List.of(
            createMockProduct(1L, "맥북", BigDecimal.valueOf(1500000)),
            createMockProduct(2L, "삼성 노트북", BigDecimal.valueOf(900000))
        );
        
        Page<Product> mockPage = new PageImpl<>(mockProducts, pageable, 2);
        
        // Specification은 equals가 작동하지 않으므로 any() 사용
        when(productRepository.findAll(any(Specification.class), eq(pageable)))
            .thenReturn(mockPage);
        
        // when
        Page<ProductResponse> result = productService.searchProducts(condition, pageable);
        
        // then
        assertThat(result.getContent()).hasSize(2);
        assertThat(result.getTotalElements()).isEqualTo(2);
        
        // Specification을 사용하여 조회했는지 확인
        verify(productRepository).findAll(any(Specification.class), eq(pageable));
    }
    
    @Test
    void Entity를_DTO로_변환하는지_테스트() {
        // given
        List<Product> mockProducts = createTestProducts(5);
        Pageable pageable = PageRequest.of(0, 5);
        Page<Product> mockPage = new PageImpl<>(mockProducts, pageable, 5);
        
        when(productRepository.findAllByIsDeleted("N", pageable))
            .thenReturn(mockPage);
        
        // when
        Page<ProductResponse> result = productService.getProducts(pageable);
        
        // then: DTO 변환 확인
        assertThat(result.getContent()).hasSize(5);
        assertThat(result.getContent().get(0)).isInstanceOf(ProductResponse.class);
        assertThat(result.getContent().get(0).getId())
            .isEqualTo(mockProducts.get(0).getId());
        assertThat(result.getContent().get(0).getName())
            .isEqualTo(mockProducts.get(0).getName());
    }
    
    private Product createMockProduct(Long id, String name, BigDecimal price) {
        return Product.builder()
            .id(id)
            .name(name)
            .price(price)
            .stock(10)
            .isDeleted("N")
            .build();
    }
    
    private List<Product> createTestProducts(int count) {
        List<Product> products = new ArrayList<>();
        for (int i = 1; i <= count; i++) {
            products.add(createMockProduct((long) i, "상품" + i, 
                BigDecimal.valueOf(1000 * i)));
        }
        return products;
    }
}

2-3. PageImpl 생성자 이해

Mock 테스트에서 Page 객체를 생성할 때 사용하는 PageImpl의 생성자를 이해해야 한다.

// PageImpl 생성자
public PageImpl(
    List<T> content,      // 현재 페이지의 실제 데이터
    Pageable pageable,    // 페이징 정보 (페이지 번호, 크기, 정렬)
    long total            // 전체 데이터 개수
)

// 사용 예시
List<Product> currentPageData = Arrays.asList(product1, product2, product3);
Pageable pageable = PageRequest.of(1, 3);  // 1페이지, 3개씩
long totalElements = 10;  // 전체 10개

Page<Product> page = new PageImpl<>(currentPageData, pageable, totalElements);

// 결과
page.getContent();          // [product1, product2, product3]
page.getNumber();           // 1 (두 번째 페이지)
page.getSize();             // 3
page.getTotalElements();    // 10
page.getTotalPages();       // 4 (10개를 3개씩 = 4페이지)
page.isFirst();             // false
page.isLast();              // false
page.hasNext();             // true
page.hasPrevious();         // true

실제 시나리오를 예로 들면 다음과 같다.

// 시나리오: 전체 100개 상품 중 2페이지(5개씩) 조회
List<Product> page2Products = Arrays.asList(
    product6, product7, product8, product9, product10  // 6~10번 상품
);

Pageable pageable = PageRequest.of(1, 5);  // 1페이지 (두 번째), 5개씩

Page<Product> result = new PageImpl<>(
    page2Products,  // 현재 페이지 데이터 (5개)
    pageable,       // 페이징 정보
    100             // 전체 100개
);

// 검증
assertThat(result.getNumber()).isEqualTo(1);              // 1페이지
assertThat(result.getSize()).isEqualTo(5);                // 5개씩
assertThat(result.getContent()).hasSize(5);               // 현재 5개
assertThat(result.getTotalElements()).isEqualTo(100);     // 전체 100개
assertThat(result.getTotalPages()).isEqualTo(20);         // 총 20페이지
assertThat(result.isFirst()).isFalse();                   // 첫 페이지 아님
assertThat(result.isLast()).isFalse();                    // 마지막 아님

 


3. 실무 적용 가이드

3-1. 상황별 페이징 전략 선택

실무에서 상황에 따라 적절한 페이징 전략을 선택한다.

// 1. 관리자 페이지 - Page 사용
// 전체 개수, 페이지 번호가 필요한 경우
@GetMapping("/admin/products")
public ResponseEntity<Page<ProductResponse>> getProductsForAdmin(
    @PageableDefault(size = 20) Pageable pageable
) {
    Page<ProductResponse> products = productService.getProducts(pageable);
    return ResponseEntity.ok(products);
}

// 2. 모바일 무한 스크롤 - Slice 사용
// 다음 페이지 존재 여부만 필요한 경우
@GetMapping("/mobile/products")
public ResponseEntity<Slice<ProductResponse>> getProductsForMobile(
    @PageableDefault(size = 20) Pageable pageable
) {
    Slice<ProductResponse> products = productService.getProductsSlice(pageable);
    return ResponseEntity.ok(products);
}

// 3. 대용량 실시간 데이터 - Cursor 사용
// OFFSET 성능 문제 해결이 필요한 경우
@GetMapping("/feed")
public ResponseEntity<CursorResponse<FeedResponse>> getFeed(
    @RequestParam(required = false) Long cursor,
    @RequestParam(defaultValue = "20") int size
) {
    CursorResponse<FeedResponse> feed = feedService.getFeedByCursor(cursor, size);
    return ResponseEntity.ok(feed);
}

// 4. 복잡한 검색 - Specifications + Page
// 동적 쿼리가 필요한 경우
@GetMapping("/search")
public ResponseEntity<Page<ProductResponse>> searchProducts(
    @ModelAttribute ProductSearchCondition condition,
    @PageableDefault(size = 20) Pageable pageable
) {
    Page<ProductResponse> products = productService.searchProducts(condition, pageable);
    return ResponseEntity.ok(products);
}

3-2. 프로젝트 구조 예시

실무 프로젝트에서 페이징을 적용한 전체 구조는 다음과 같다.

// 프로젝트 구조
com.server.smzshop
├── product
│   ├── domain
│   │   ├── Product.java                    // Entity
│   │   └── ProductRepository.java          // Repository
│   ├── application
│   │   ├── ProductService.java             // Service
│   │   ├── ProductSpecifications.java      // Specifications
│   │   └── ProductSpecificationBuilder.java // Builder
│   ├── presentation
│   │   ├── ProductController.java          // Controller
│   │   ├── dto
│   │   │   ├── ProductResponse.java        // Response DTO
│   │   │   ├── ProductSearchCondition.java // Search Condition
│   │   │   ├── SliceResponse.java          // Slice Response
│   │   │   └── CursorResponse.java         // Cursor Response
│   └── test
│       ├── ProductRepositoryTest.java      // Repository Test
│       └── ProductServiceTest.java         // Service Test

// Repository
public interface ProductRepository extends JpaRepository<Product, Long>,
                                          JpaSpecificationExecutor<Product> {
    Page<Product> findByIsDeleted(String isDeleted, Pageable pageable);
    Slice<Product> findSliceByIsDeleted(String isDeleted, Pageable pageable);
    List<Product> findTop20ByIdLessThanAndIsDeletedOrderByIdDesc(Long id, String isDeleted);
}

// Service
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ProductService {
    private final ProductRepository productRepository;
    
    // Page 방식
    public Page<ProductResponse> getProducts(Pageable pageable) {
        Page<Product> productPage = productRepository.findByIsDeleted("N", pageable);
        return productPage.map(this::toResponse);
    }
    
    // Slice 방식
    public SliceResponse<ProductResponse> getProductsSlice(Pageable pageable) {
        Slice<Product> productSlice = productRepository.findSliceByIsDeleted("N", pageable);
        return toSliceResponse(productSlice);
    }
    
    // Cursor 방식
    public CursorResponse<ProductResponse> getProductsByCursor(Long cursor, int size) {
        List<Product> products = cursor == null
            ? productRepository.findTop20ByIsDeletedOrderByIdDesc("N")
            : productRepository.findTop20ByIdLessThanAndIsDeletedOrderByIdDesc(cursor, "N");
        return toCursorResponse(products, size);
    }
    
    // Specifications 방식
    public Page<ProductResponse> searchProducts(
        ProductSearchCondition condition,
        Pageable pageable
    ) {
        Specification<Product> spec = new ProductSpecificationBuilder()
            .isNotDeleted()
            .nameLike(condition.getName())
            .priceBetween(condition.getMinPrice(), condition.getMaxPrice())
            .build();
        
        Page<Product> productPage = productRepository.findAll(spec, pageable);
        return productPage.map(this::toResponse);
    }
}

// Controller
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/products")
public class ProductController {
    private final ProductService productService;
    
    @GetMapping
    public ResponseEntity<Page<ProductResponse>> getProducts(
        @PageableDefault(size = 20, sort = "id", direction = Sort.Direction.DESC) 
        Pageable pageable
    ) {
        return ResponseEntity.ok(productService.getProducts(pageable));
    }
    
    @GetMapping("/scroll")
    public ResponseEntity<SliceResponse<ProductResponse>> getProductsForScroll(
        @PageableDefault(size = 20) Pageable pageable
    ) {
        return ResponseEntity.ok(productService.getProductsSlice(pageable));
    }
    
    @GetMapping("/cursor")
    public ResponseEntity<CursorResponse<ProductResponse>> getProductsByCursor(
        @RequestParam(required = false) Long cursor,
        @RequestParam(defaultValue = "20") int size
    ) {
        return ResponseEntity.ok(productService.getProductsByCursor(cursor, size));
    }
    
    @GetMapping("/search")
    public ResponseEntity<Page<ProductResponse>> searchProducts(
        @ModelAttribute ProductSearchCondition condition,
        @PageableDefault(size = 20) Pageable pageable
    ) {
        return ResponseEntity.ok(productService.searchProducts(condition, pageable));
    }
}

이러한 구조로 실무 프로젝트를 구성하면 다양한 페이징 요구사항을 효율적으로 처리할 수 있다.

'공부일기.. > JPA' 카테고리의 다른 글

[JPA] Spring Data JPA 페이징(2/3) - 실무 패턴과 동적 쿼리  (0) 2025.12.11
[JPA] Spring Data JPA 페이징 (1/3) - 기본 개념과 핵심 객체  (0) 2025.12.08
[jpa] BaseEntity와 JPA Auditing - 동작 원리 , 중복필드 제거 (예시코드)  (0) 2025.10.18
[JPA] JPA의 필수개념_2 (식별자 전략 (@Id, @GeneratedValue))  (1) 2025.06.26
[JPA] JPA의 필수개념_1 (JPA정의, Projection)  (0) 2025.06.26
'공부일기../JPA' 카테고리의 다른 글
  • [JPA] Spring Data JPA 페이징(2/3) - 실무 패턴과 동적 쿼리
  • [JPA] Spring Data JPA 페이징 (1/3) - 기본 개념과 핵심 객체
  • [jpa] BaseEntity와 JPA Auditing - 동작 원리 , 중복필드 제거 (예시코드)
  • [JPA] JPA의 필수개념_2 (식별자 전략 (@Id, @GeneratedValue))
s0-0mzzang
s0-0mzzang
공부한것을 기록합니다...
  • s0-0mzzang
    승민이의..개발일기..🐰
    s0-0mzzang
  • 전체
    오늘
    어제
    • 전체~ (108)
      • 마음가짐..! (10)
      • 공부일기.. (76)
        • weekly-log (6)
        • Spring (19)
        • Java (18)
        • DataBase (10)
        • git (2)
        • JPA (6)
        • kafka (1)
        • Backend Architecture (3)
        • Troubleshooting (삽질..ㅋ) (2)
        • Cloud (1)
        • Docker (2)
        • 알고리즘 (1)
        • 리액트 (2)
        • Infra (3)
      • 하루일기.. (22)
        • 그림일기 (8)
        • 생각일기 (14)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 깃허브
  • 공지사항

  • 인기 글

  • 태그

    BufferedReader
    인프라 기초
    항해99
    React
    TDD
    항해플러스
    swagger
    JPA
    StringTokenizer
    MySQL
    spring
    ADC 환경
    스프링부트
    spring boot
    자바
    ERD
    리팩토링
    SpringBoot
    Paging
    다짐
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
s0-0mzzang
[JPA] Spring Data JPA 페이징 (3/3) - 성능 최적화와 테스트
상단으로

티스토리툴바