ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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

    반응형

    댓글

Designed by Tistory.