-
[23.12.20] Spring Data JPA 성능 최적화블로그 번역 2023. 12. 21. 11:37반응형
소개글
Spring Data JPA는 데이터 베이스 관리 및 상호 작용 시 강력한 Tool이다.
그러나 잘못 사용하는 경우 성능이 떨어질 수 있다.
성능을 최적화하는 방법에 대해서 알아보자.
1. 적절한 인덱싱 사용
WHERE, JOIN, ORDER BY에 자주 사용하는 Column Index를 생성하여 쿼리 속도를 높이자.
과도한 인덱싱은 피하는 것이 좋다. INSERT/UPDATE 성능에 영향을 줄 수 있다.
@Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private String isbn; @ManyToOne @JoinColumn(name = "author_id") private Author author; // Constructors, getters, setters } @Entity public class Author { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // Constructors, getters, setters }
위 예시에서 특정 Author의 Book을 불러온다 하자. 인덱스가 없으면 모든 Book Table을 찾을 것이다.
아래와 같이 인덱스를 추가하면 특정 인덱스 열만 읽어 속도가 빨라진다.
과도하게는 사용하지 말자.
@Entity public class Book { // ... @ManyToOne @JoinColumn(name = "author_id") @org.hibernate.annotations.Index(name = "author_id_index") // Adding an index private Author author; // ... }
2. Fetching Strategies
흔하게 사용되는 "Eager", "Lazy"에 대한 내용이다.
@Entity public class Author { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToMany(mappedBy = "author", fetch = FetchType.LAZY) // Lazy fetch strategy private List<Book> books = new ArrayList<>(); // getters and setters } @Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; @ManyToOne(fetch = FetchType.EAGER) // Eager fetch strategy @JoinColumn(name = "author_id") private Author author; // getters and setters }
한 명의 Author는 여러 Book을 가질 수 있다.
EAGER 타입은 Book을 가져올 때 Author내용도 즉시 가져오는 것
LAZE 타입은 Author 가져올 때 Book을 가져오지 않고 코드에서 사용될 때 쿼리가 나가면서 가져오는 것
주의점은 LAZY타입은 N+1 문제를 야기할 수 있어 코드 Layer에서 주의가 필요하다.
3. Batch Fetching
쿼리를 최소화하고 N+1 문제를 해결하기 위해 주로 사용한다.
방법은 많지만 대표적으로 @BatchSize를 사용해보자.
엔티티 정의
@Entity public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @OneToMany(mapped = "order") private List<OrderItem> orderItem; // Other order attributes, getters, setters } @Entity public class OrderItem { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) // Use LAZY to avoid loading items eagerly @JoinColumn(name = "order_id") private Order order; // Other order item attributes, getters, setters }
그리고 Order를 findAll로 여러 개 조회 후 내부 OrderItem을 조회하면 N+1 문제가 발생한다.(OneToMany는 기본 LAZY)
만약 findAllf로 Order가 10개 조회되었다면 for-loop를 사용하여 각 OrderItem 조회 시 쿼리가 계속 나가게된다.
총 1(Order.findAll) + 10(각 OrderItem 조회)
select * from order; select * from order_item where order_id=1 select * from order_item where order_id=2 select * from order_item where order_id=3 ... select * from order_item where order_id=10
@BatchSize 설정 시
select * from order; select * from order_item where order_id in (1,2,3,...,10)
4. Caching
Spring cache, Ehache, Caffeine 등 서드 파티 캐시 툴을 사용하여 자주 필요로하는 데이터를 메모리에 저장한다.
@Cacheable: Product 조회 시 레디스 캐시 처리
@CacheEvict: Update 시 레디스 캐시에서 Key가 Products인 데이터 제거
import org.springframework.cache.annotation.CacheEvict; import org.springframework.stereotype.Service; @Service public class ProductService { @Cacheable("products") public Product getProductById(Long productId) { // Simulate fetching data from the database return databaseService.fetchProductById(productId); } @CacheEvict(value = "products", key = "#productId") public void updateProduct(Long productId, Product updatedProduct) { // Simulate updating data in the database databaseService.updateProduct(productId, updatedProduct); } }
@CachePut: 초기 저장 또는 저장된 캐시 데이터를 업데이트 할 때 사용
import org.springframework.cache.annotation.CachePut; import org.springframework.stereotype.Service; @Service public class ProductService { @CachePut(value = "products", key = "#productId") public Product updateProduct(Long productId, Product updatedProduct) { // Simulate updating data in the database databaseService.updateProduct(productId, updatedProduct); return updatedProduct; } }
5. Query Optimization
효율적인 JPQL 및 Criteria API queries를 사용하자.(SELECT * from... 은 피하자.)
필요한 필드는 DTO Projection을 이용하여 가져 올 수 있다.
Spring Data JPA에선 인터페이스 메서드를 이용해 최적화된 쿼리를 만들어 실행해준다.
import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface ProductRepository extends JpaRepository<Product, Long> { List<Product> findByPriceBetween(double minPrice, double maxPrice); }
6. Use Pagination & Sorting
페이징과 정렬은 특정 기준에 따라 결과를 가져오는 방법이다.
Spring Data JPA에서는 Pageable 인터페이스를 활용한다.
아래 처럼 페이지 및 정렬 정보를 정의한다.
Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by(sortBy).descending());
사용은 Repository에서 아래와 같이 사용한다.
public Page<Product> findProducts(String keyword, Pageable pageable) { return productRepository.findByName(keyword, pageable); }
7. Read-Only Operation@Transactional(readOnly = true)
어노테이션의 속성 사용에 따라 JPA에서 Dirty Checking을 하지 않는다.
8. Batch Operation
Bulk INSERT, UPDATE, DELETE를 사용하는 경우 아래 메서드를 사용해보자.
@Service public class OrderService { @Autowired private OrderRepository orderRepository; public List<Order> saveOrders(List<Order> orders) { return orderRepository.saveAll(orders); } }
9. Avoid N+1 Query Problem
N+1 문제를 피하는 방법에 대해서 소개하려 한다.
1. Eagar Loading
Entity를 조회 시 Proxy객체가 아닌 실제 객체를 가져오는 것이다.
그러나 필요보다 많은 데이터들을 가져오면서 성능에 큰 문제를 발생시킬 수 있다.
좋은 선택은 아니다.
2. Join Fetch
아래 JPQL을 작성하며 성능 향상 및 쿼리를 최소화 할 수 있다.
주의 사항으로는 페이지네이션 금지, 하나의 Collection에 대한 Join 수행, Distinct 사용 등이 있다.
@Repository public interface AuthorRepository extends JpaRepository<Author, Long> { @Query("SELECT DISTINCT a FROM Author a JOIN FETCH a.books") List<Author> findAllWithBooks(); }
3. default_batch_fetch_size, @BatchSize
지연 로딩 프록시 객체 조회 시 where in 절로 한 번에 묶어서 조회할 수 있는 기능이다.
주의 사항으로는 하나의 SQL이 아닌 여러 개의 SQL로 나눠 실행되는 경우가 있는데 이런 경우는 적절한 Size조절이 필요하다.
참고: https://medium.com/@avi.singh.iit01/optimizing-performance-with-spring-data-jpa-85583362cf3a
반응형'블로그 번역' 카테고리의 다른 글
[23.12.27] @Retryable 사용법 (1) 2023.12.27 [23.12.26] Outbox Transaction Pattern (0) 2023.12.26 [23.12.19] 단위 테스트에서 환경 변수 Mocking (0) 2023.12.19 [23.12.19] JVM: 아키텍처 및 성능 최적화 (1) 2023.12.19 [23.12.18] 로컬 데이터 레이크 만들기 (2) 2023.12.18