[23.12.20] Spring Data JPA 성능 최적화
소개글
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