블로그 번역

[23.12.20] Spring Data JPA 성능 최적화

HOONY_612 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

반응형