본문 바로가기

Spring/Spring JPA

배치 쿼리의 성능 이슈 원인 간단하게

스프링 애플리케이션에서 배치 쿼리(Batch Query)를 사용하는 경우, 성능 이슈는 주로 데이터 처리량과 쿼리 실행 방식에서 발생합니다. 배치 쿼리는 대량의 데이터를 한 번에 처리하기 위한 방법이지만, 올바르게 설정하지 않으면 성능 저하나 예기치 않은 문제를 일으킬 수 있습니다.

 

배치 쿼리의 성능 이슈 원인

1. JDBC Batch Size 미설정

  • 문제: 기본적으로 JDBC는 배치 쿼리를 처리할 때 한 번에 한 레코드씩 처리합니다. 이는 데이터베이스와의 연결 횟수가 증가해 성능이 저하됩니다.
  • 원인: hibernate.jdbc.batch_size(Hibernate) 또는 spring.jpa.properties.hibernate.jdbc.batch_size(Spring Boot)와 같은 설정을 하지 않은 경우.
  • 해결 방법:
    • Hibernate 설정 파일 또는 Spring Boot 설정 파일에 다음과 같이 배치 크기를 설정:
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
  • batch_size는 데이터베이스와 한 번의 연결로 처리할 레코드 수를 지정합니다. 일반적으로 50~100 사이의 값을 권장합니다.

2. 1-N 관계에서 N+1 문제

  • 문제: 부모 엔티티와 자식 엔티티 간의 관계를 배치 쿼리로 처리할 때, 자식 엔티티를 개별적으로 조회하면서 N+1 쿼리가 발생.
  • 원인: JPA가 연관된 엔티티를 조회하거나 저장할 때 @OneToMany 또는 @ManyToOne 관계를 즉시 로딩(Eager Loading)으로 처리.
  • 해결 방법:
    • 연관 관계를 LAZY 로딩으로 변경
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
private List<Child> children;
  • 필요한 경우 JOIN FETCH 또는 EntityGraph를 사용해 한 번의 쿼리로 데이터를 가져오기
@Query("SELECT p FROM Parent p JOIN FETCH p.children WHERE p.id = :id")
Parent findParentWithChildren(@Param("id") Long id);

 

 

3. 영속성 컨텍스트 관리 문제

  • 문제: 대량의 데이터를 처리하면서 영속성 컨텍스트(Persistence Context)에 많은 엔티티가 쌓이면 메모리 부족(OutOfMemoryError) 문제가 발생.
  • 원인: JPA의 변경 감지(Dirty Checking)로 인해 모든 엔티티를 영속성 컨텍스트에 저장하고 변경 여부를 추적.
  • 해결 방법:
    • 청소 및 플러시 사용: 배치 작업 중 일정 주기마다 EntityManager.flush()와 EntityManager.clear()를 호출해 영속성 컨텍스트를 비워줍니다.
for (int i = 0; i < entities.size(); i++) {
    entityManager.persist(entities.get(i));
    if (i % batchSize == 0) {
        entityManager.flush();
        entityManager.clear();
    }
}
  • Spring Data JPA의 saveAll() 사용 시에도 주기적으로 청소하는 로직을 추가.

 

4. 식별자 생성 방식 문제

  • 문제: @GeneratedValue(strategy = GenerationType.AUTO)나 GenerationType.IDENTITY를 사용하는 경우, 데이터베이스가 식별자를 생성하면서 배치 처리가 비효율적으로 작동.
  • 원인: IDENTITY 전략은 엔티티 저장마다 개별적인 INSERT가 실행되기 때문.
  • 해결 방법:
    • Sequence 또는 Table 전략 사용: 데이터베이스에서 식별자를 미리 생성해 배치로 처리할 수 있도록 설정.
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_gen")
@SequenceGenerator(name = "seq_gen", sequenceName = "seq_name", allocationSize = 50)
private Long id;
  • allocationSize를 적절히 설정해 ID를 한 번에 여러 개 생성.

 

5. 데이터베이스 잠금(Locking) 문제

  • 문제: 대량의 데이터를 처리하는 동안 다른 트랜잭션이 동일한 데이터를 수정하려 하면 데드락이나 대기 시간 초과가 발생.
  • 원인: 배치 작업 중 트랜잭션 범위가 넓거나 데이터베이스 인덱스가 적절하지 않을 때.
  • 해결 방법:
    • 트랜잭션 크기를 줄이기 위해 배치 단위를 작게 설정.
    • 데이터베이스 테이블에 적절한 인덱스를 추가.
    • 필요한 경우 비관적 락(Pessimistic Lock) 또는 낙관적 락(Optimistic Lock) 사용.

6. 쿼리 최적화 문제

  • 문제: 잘못된 쿼리로 인해 배치 작업 속도가 느려질 수 있음.
  • 원인: 비효율적인 쿼리 작성, JOIN 사용 시 불필요한 데이터 조회.
  • 해결 방법:
    • JPQL 대신 네이티브 쿼리 사용: 복잡한 쿼리는 JPQL보다 네이티브 SQL이 더 효율적일 수 있음.
    • 데이터베이스 쿼리 플랜 확인: EXPLAIN 명령어를 사용해 쿼리 실행 계획을 분석하고 최적화.

 

배치 쿼리 성능 최적화를 위한 팁

  1. 적절한 배치 크기 설정: 일반적으로 50~100 사이가 적당.
  2. 네트워크 비용 줄이기: 배치 크기를 너무 작게 설정하면 DB와의 연결이 많아져 오버헤드가 발생.
  3. 트랜잭션 관리: 각 배치 처리 단위를 독립된 트랜잭션으로 관리.
  4. 인덱스 확인: 배치 작업이 많이 발생하는 컬럼에 적절한 인덱스를 추가.
  5. 프로파일링 도구 사용: JPA/Hibernate의 SQL 로그를 활성화하거나 APM(Application Performance Monitoring) 도구를 활용해 병목 지점을 식별.

'Spring > Spring JPA' 카테고리의 다른 글

영속성 컨텍스트(Persistence Context)  (0) 2025.01.07
@Embedded, @Embeddable 간단하게  (0) 2025.01.07
@Converter 간단하게  (1) 2025.01.07
Native Query  (0) 2025.01.06
JPQL 간단히  (0) 2025.01.06