728x90

 

 

 

배치란? 스프링 배치란? 

  • 배치 는 일정 시간마다 또는 특정 조건에 따라 대규모 데이터를 일괄 처리하는 방식을 의미한다.
  • 스프링 배치(Spring Batch)는 이러한 배치 작업을 보다 쉽게 구현하고 관리할 수 있도록 돕는 프레임워크이다.
  • 스프링 배치는 스프링 생태계의 일환으로, 스프링의 강력한 기능을 활용하여 배치 작업을 안정적이고 효율적으로 수행할 수 있도록 지원한다.
  • 이를 통해 개발자는 복잡한 배치 처리 로직을 간결하게 구현하고 모니터링 기능을 통해 배치 작업을 효과적으로 관리할 수 있다.

 

스프링 배치와 스프링 스케줄러 차이

  • 많은 개발자들이 이 두 가지를 혼동하곤 하지만, 스프링 배치(Spring Batch)와 스프링 스케줄러(Spring Scheduler)는 서로 다른 목적과 기능을 가지고 있다.

 

스프링 배치

  • 대량의 데이터를 효율적으로 처리하기 위해 설계된 프레임워크
  • 복잡한 배치 작업을 구성하고 실행하기 위한 다양한 도구와 기능을 제공
    • 배치 프로세스를 주기적으로 커밋: 막강한 커밋 전략을 제공
    • 동시 다발적인 job 의 배치 처리, 대용량 병렬 처리
    • 실패 후 수동 또는 스케줄링에 의한 재시작: 오류에 대한 대처 옵션을 제공
    • 반복, 재시도, skip 처리 : 실패한 배치 작업을 중단된 지점부터 다시 시작하거나 skip 할 수 있다.
    • 모니터링: 배치 작업의 진행 상태와 소요 시간 등의 정보를 제공
    • 트랜잭션 관리: 트랜잭션 관리를 지원하여 데이터의 일관성 유지

 

스프링 배치는 주로 금융, 전자상거래, 건강관리 등 대량의 데이터 처리가 필요한 도메인에서 널리 사용된다.

배치(batch)는 데이터를 실시간으로 처리하는게 아니라, 일괄적으로 모아서 한번에 처리하는 작업을 의미한다.

 

 

스프링 스케줄러

  • 스프링 스케줄러는 특정 시간에 작업을 실행하기 위해 설계된 도구이다.
  • 반복적인 작업을 자동화하고, 시간 기반의 작업을 쉽게 설정할 수 있다.
    • 시간 기반 작업 실행: 특정 시간 또는 주기적으로 작업을 실행할 수 있다.
    • 다양한 스케줄링 방식: 크론 표현식(Cron Expression) 또는 Fixed Delay 를 통해 작업 스케줄을 설정할 수 있다.
    • 비동기 처리: 스케줄된 작업을 비동기적으로 실행하여 애플리케이션의 성능을 유지할 수 있다.
    • 유연한 구성: XML 설정 또는 애노테이션을 통해 쉽게 설정할 수 있다.

 

스프링 스케줄러는 주로 정기적인 백업, 이메일 알림, 데이터 동기화 등의 작업을 자동화하는 데 사용된다.

 

 

특징

                                              스프링 배치 (Spring Batch)                                         스프링 스케줄러 (Spring Scheduler)

목적 대량 데이터 일괄 처리 시간 기반 작업 실행
주요 기능 트랜잭션 관리, 재시작 및 중단, 데이터 검증 및 오류 처리 크론 표현식, 고정된 지연, 비동기 처리
사용 사례 금융 데이터 처리, 대량 파일 처리, 시스템 통합 정기 백업, 이메일 알림, 데이터 동기화
구성 방식 주로 XML 설정 또는 Java Config 애노테이션 또는 XML 설정

 

 

 


 

스프링 배치 구조

 

스프링배치 주요 흐름 이미지 출처 (https://terasoluna-batch.github.io/)

 

  1. 스프링 배치 (Spring Batch):
    • 스프링 배치는 대규모 데이터 처리 작업을 자바 기반의 배치 처리 어플리케이션을 만들기 위한 프레임워크
    • 대용량 데이터를 처리하는 경우나 주기적인 업무 일괄 처리 등에 사용
    • 스프링 배치는 배치 작업을 트랜잭션 처리와 함께 관리하고, 실패한 작업을 복구하고 재시작할 수 있는 기능을 제공
    • 잡(Job), 스텝(Step), 리더(Reader), 프로세서(Processor), 라이터(Writer) 등의 개념을 이용하여 배치 처리를 설계하고 실행한다.
    • job은 여러개의 step 으로 구성되고 step은 taskelt(기능) 으로 구성된다.
    • 나는 tasklet으로 구현하였다.!!
  2. 쿼츠 (Quartz):
    • 쿼츠는 자바 기반의 오픈 소스 스케줄링 라이브러리
    • 스케줄링 작업을 관리하고 실행할 수 있는 기능을 제공
    • 주기적으로 실행되어야 하는 작업이나 예약된 작업을 관리하고 실행하는 데 사용
    • 쿼츠는 크론 표현식을 사용하여 실행 스케줄을 설정하고, 다양한 트리거(trigger)를 통해 작업을 실행할 수 있음
    • 분산 환경에서도 사용할 수 있는 확장 가능한 아키텍처를 가지고 있어, 대규모 시스템에서도 유연하게 활용됨
요약하자면, 스프링 배치는 대규모 데이터 처리를 위한 일괄 처리 프레임워크이고, 쿼츠는 예약된 작업을 스케줄링하고 실행하는 데 사용되는 라이브러리 
-> 함께 사용되어 시스템에서 정기적인 작업 처리와 스케줄링을 효과적으로 관리할 수 있음

 

 

 

Job : 전체 배치 프로세스를 캡슐화한 엔티티, 배치 처리 과정을 하나의 단위로 만들어 놓은 객체

 

JobInstance : Job 실행의 단위(job이 각각 실행할 때마다 따로 추적할 수 있도록 매 실행마다 생성되며 특정 job과 식별 가능한 JobParameters에 상응하는 JobInstance는 단 한 개뿐이다.)

 

JobParameters : JobInstance에 전달되는 매개변수 역할, JobInstance를 구분하는 식별자 역할

 

JobExecution : Job에 대한 실행 시도 정보를 담은 객체.

 

Step : 배치 Job의 배치처리를 정의하고 순차적인 단계를 캡슐화 한 도메인 객체. Job은 하나 이상의 Step을 가져야 한다.

 

StepExecution : Step에 대한 실행 시도 정보를 담은 객체.

 

ExecutionContext : 프레임워크에서 유지/관리하는 key-value의 컬렉션. StepExecution객체 또는 JobExecution객체에 속하는 상태를 저장

 

JobExecutionContext : Commit 시점에 저장

 

StepExecutionContext : 실행 사이에 저장

 

JobRepository : 위에 업근된 모든 저장(Persistence)매커니즘을 담당.(JobLauncher, Job, Step 구현체에 CRUD 기능을 제공)

 

JobLauncher : 주어진 JobParameters로 Job을 실행하는 간단한 인터페이스.

 

- Step에서 아이템을 한 번에 하나씩 읽어오는 작업을 추상화한 개념

Tasklet 

   or

Item Reader 

Item Writer : Step에서 배치나 청크 단위로 아이템을 출력하는 작업을 추상화한다.

Item Processor : 아이템을 처리하는 비즈니스 로직을 나타내는 추상화 개념

 


 

https://github.com/spring-projects/spring-batch/wiki/Spring-Batch-5.0-Migration-Guide

 

Spring Batch 5.0 Migration Guide

Spring Batch is a framework for writing batch applications using Java and Spring - spring-projects/spring-batch

github.com

스프링배치5부터는 매인 애플리케이션에 @EnableBatchProcessing 어노테이션을 사용할 필요가 없다.

자세한 내용은 마이그레이션 가이드부터 읽어 보길 권장한다!!

 

 

728x90
728x90

개요

이전 블로그에 지연로딩, 즉시로딩에 대해서 살펴보았다.

JPA를 사용하다 보면 바로 N+1의 문제에 마주치고 바로 Fetch Join을 접하게 된다.
처음 Fetch Join을 접했을 때 왜 일반 Join으로 해결하면 안되는지에 대해 명확히 정리가 안된 채로 Fetch Join을 사용했다.
어떤 문제 때문에 일반 join으로 N+1을 해결하지 못하는지를 시작으로 해서 Fetch Join, Join의 차이점을 정리해보겠다.

 

 

Join, Fetch Join 차이점 요약

  • 일반 Join
    • Fetch Join과 달리 연관 Entity에 Join을 걸어도 실제 쿼리에서 SELECT 하는 Entity는 오직 JPQL에서 조회하는 주체가 되는 Entity만 조회하여 영속화
    • 조회의 주체가 되는 Entity만 SELECT 해서 영속화하기 때문에 데이터는 필요하지 않지만 연관 Entity가 검색조건에는 필요한 경우에 주로 사용됨

 

  • Fetch Join
    • 조회의 주체가 되는 Entity 이외에 Fetch Join이 걸린 연관 Entity도 함께 SELECT 하여 모두 영속화
    • Fetch Join이 걸린 Entity 모두 영속화하기 때문에 FetchType이 Lazy인 Entity를 참조하더라도 이미 영속성 컨텍스트에 들어있기 때문에 따로 쿼리가 실행되지 않은 채로 N+1문제가 해결됨

 

 

N+1 문제

단순 테이블 조회 쿼리를 하나 생성하였는데, 연관관계에 있는 데이터를 모두 불러오기 위해서 select 쿼리가 테이블의 레코드 수(혹은 호출한 데이터 수) 만큼 더 생성되는 문제를 의미한다.

 

즉시로딩은 최초 테이블 데이터를 모두 불러온 후 바로 연관관계가 있는 엔티티를 조회하는 방식을,

지연로딩은 최초 테이블 데이터를 모두 불러온 후 필요한 시점에 연관관계가 있는 엔티티를 조회하는 방식을 채택한다.

 

즉시로딩은 워딩 그대로 조회 시점에 연관관계에 있는 엔티티가 모두 조회되어 N+1 문제가 생긴다는 것을 직관적으로 알 수 있지만 지연로딩은 얼핏보면 로딩 시점에 쿼리가 더 나가지 않아 N+1 문제가 발생하지 않을 것으로 보인다. 하지만 지연로딩 N+1 문제를 피해갈 수는 없는데, 그 이유는 연관관계에 있는 엔티티가 조회 시점에 영속화되지 않았기 때문이다.

 

일반적으로 JPA는 JPQL 로 생성된 쿼리문을 데이터베이스와 connection 을 만든 후에 전송하고, 이에 대한 결과 데이터를 전달받아 실제 엔티티 객체를 생성하고 이를 영속화 한다.

 

 

 

예시

@Entity
class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    
    String title;

    @OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
    List<Comment> cpmments = ArrayList()
}

@Entity
class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    
    String content;

    @ManyToOne(fetch = FetchType.LAZY)
    Post post;
}
@Service
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;

    @Transactional(readOnly = true)
    public void getMockposts() {
        postRepository.findAll().forEach(post -> {
            post.getpPostComments().forEach(cpmments ->
                System.out.println(cpmments.getContent())
            );
        });
    }
}

위 코드에서 N+1 문제가 발생할 수 있다. 예를 들어, 100개의 게시글이 있다고 가정하면, PostRepository의 findAll() 메서드가 호출될 때 모든 게시글을 조회한다. 이때, 각 게시글의 댓글을 조회하기 위해 추가로 100번의 쿼리가 실행다. 이는 총 101번의 쿼리 실행으로 이어지며, 매우 비효율적인 방법이다. 즉, 다음과 같이 SQL이 실행되면서 N+1 문제가 발생한 것을 알 수 있다.

SELECT * FROM post;
SELECT * FROM comment WHERE post_id = 1;
SELECT * FROM comment WHERE post_id = 2;
SELECT * FROM comment WHERE post_id = 3;
...
SELECT * FROM comment WHERE post_id = 99;
SELECT * FROM comment WHERE post_id = 100;

 

 

해결 방법

1. Fetch Join (@Query 어노테이션 사용)

  • Fetch Join은 N+1 문제를 방지하고 성능을 최적화할 수 있는 수단으로서 데이터베이스에서 연관된 엔터티를 함께 로딩하는 방법이다.
  • Fetch Join을 사용하면 조회 성능을 최적화 할 수 있다.
  • 연관된 엔티티를 쿼리 시 함께 조회하는 방법이다.
  • SQL 쿼리에 직접 Join fetch를 명시한다.
  • 조회의 주체가 되는 엔티티 외에 Fetch Join이 걸린 연관 엔티티도 같이 영속화 시켜준다.
  •  
@Repository 
public interface PostRepository extends JpaRepository<Post, String> {
@Query("SELECT p from Post p JOIN FETCH p.comments")
List<Post> findAllPostsWithComments();
}
  •  "JOIN FETCH p.comments" 구문을 통해서 게시글을 조회할 때 댓글 엔티티도 함께 조회한다.
  • join fetch는 Fetch Join을 사용하여 연관된 데이터를 조회하겠다는 뜻이다.

 

2. Entity Graph

  • EntityGraph를 사용하면 Fetch Join를 사용한 경우와 마찬가지로 연관된 엔티티나 컬렉션을 같이 조회 할 수 있다.
  • JPQL과 함께 사용하며 attributePaths에 쿼리 수행 시 조회할 필드를 지정해준다.
  • 지정된 필드는 지연 로딩이 아닌 즉시 로딩으로 조회된다.
  • EntityGraph는 Fetch Join과는 다르게 Outer Join이 사용되어 동작한다
@Repository 
public interface PostRepository extends JpaRepository<Post, String> {

@EntityGraph(
        attributePaths = {"comments"},
        type = EntityGraph.EntityGraphType.LOAD
    )
@Query("SELECT p FROM post p")
List<Post> findAllPostsWithComments();

}

 

 

Fetch Join 및 EntityGraph 사용 시 발생하는 카테시안 곱 문제

Fetch Join 또는 EntityGraph를 사용한 경우엔 카테시안 곱(Cartesian Product)으로 인하여 중복 문제가 발생할 수 있습니다. 카테시안 곱은 연관관계의 엔티티에 Join 규칙을 사용하지 않아 결합 조건이 주어지지 않았을 때 모든 데이터의 경우의 수를 결합(M * N)하여 반환하는 문제입니다. 즉, 조인 조건이 없는 경우에 대한 두 테이블의 결합 결과를 반환해야 하기 때문에 존재하는 모든 행의 개수를 곱한 결과를 반환하게 됩니다. 카테시안 곱 문제를 해결하기 위해서는 다음과 같은 방법을 사용합니다.
 

 

 


1. JPQL에 DISTINCT 사용하기


JPQL을 이용한 쿼리를 사용할 때 다음과 같이 DISTINCT를 사용하여 중복을 제거합니다.

@Repository 
public interface PostRepository extends JpaRepository<Post, String> {

    @Query("SELECT DISTINCT p from Post p JOIN FETCH p.comments")
	List<Post> findAllPostsWithComments();
    
    @EntityGraph(
        attributePaths = {"comments"},
        type = EntityGraph.EntityGraphType.LOAD
    )
	@Query("SELECT DISTINCT p FROM post p")
	List<Post> findAllPostsWithComments();
}


 

2. 연관관계의 필드 타입에 Set 사용하기

  • Set은 중복을 허용하지 않기 때문에 중복 데이터가 들어가지 않지만 순서가 보장되지 않는다.
  • 이러한 경우 다음과 같이 연관관계의 필드에 LinkedHashSet을 사용하여 중복을 제거하고 순서를 보장 해줄 수 있다.
@Getter
@NoArgsConstructor
@Entity(name = "posts")
public class post {

    @OneToMany(mappedBy = "post")
    private Set<Comment> comments = new LinkedHashSet<>();

}

 

 

728x90
728x90

개요 

스프링 JPA를 활용한 프로젝트 리팩토링 과정에서 JPA N+1 문제가 발생하여 정리해볼려고 한다.

 

Fetch 전략

특정 엔티티를 조회할 때, 연관된 다른 엔티티를 [언제] 불러오는지에 대한 옵션이다. 위와 같이 연관관계 매핑 어노테이션의 옵션을 통해 전략을 명시한다. 옵션에는 두 가지의 전략이 있다. 

 

 

1. 즉시로딩

  • 즉시로딩이란 연관된 모든 엔티티를 함께 즉시 로드하는 방식이다.
  • 즉, 부모 엔티티를 조회할 때 연관된 자식 엔티티들도 함께 한번에 가져온다. 
  • 현재 엔티티를 조회한 [직후], 연관된 엔티티까지 조회한다.
@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩
    private User user;
}

 

위 Post 엔티티는 User와 다대일(@ManyToOne) 관계를 맺고 있으며, 즉시 로딩(EAGER)으로 설정되었다. 이 경우 Post를 조회할 때 User 정보도 함께 조회된다. 즉, Post 엔티티를 가져오면 바로 User 엔티티도 DB에서 쿼리되어 로드된다.

장점으로는 쿼리가 한 번만 실행되므로 사용 시 추가적인 데이터베이스 접근이 필요 없다. 이 방법으로 인해 N+1 문제를 해결할 수 있지만, 불필요하게 많은 데이터를 한 번에 로드할 수 있어 성능에 영향을 줄 수 있다.  

 

 

 

 

2. 지연로딩

  • 지연로딩이란 연관된 엔티티를 즉시 로드하지 않고 실제로 해당 엔티티에 접근할 때 데이터를 로드하는 방식이다.
  • 즉 부모 엔티티만 먼저 조회하고 자식 엔티티는 실제로 필요할 때 쿼리가 발생하는 식이다.
  • 현재 엔티티를 조회하고, [추후 필요할 때] 연관된 엔티티를 조회하는 쿼리를 날린다.
@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
    private User user;
}

위 Post 엔티티는 User와 지연 로딩(LAZY) 관계를 맺고 있다. 이 경우 Post 엔티티를 조회할 때는 User 엔티티를 바로 로드하지 않고, getUser() 메서드를 호출해서 User 정보를 실제로 사용할 때 DB에서 쿼리가 실행되어 데이터를 로드한다. 장점으로는 연관된 엔티티가 많아도 불필요한 데이터는 로드하지 않아 성능을 향상시킬 수 있다. 하지만 엔티티에 접근할 때마다 추가적인 데이터베이스 쿼리가 발생할 수 있다. 이를 잘못 사용할 경우 N+1 문제를 일으킬 수 있다.

 

 

 

기본 로딩 전략 (디폴트)

  • @OneToMany, @ManyToMany: 기본적으로 지연 로딩(LAZY)으로 설정
  • @ManyToOne, @OneToOne: 기본적으로 즉시 로딩(EAGER)으로 설정
@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @ManyToOne // 기본 즉시 로딩 (EAGER)
    private User user;

    @OneToMany(mappedBy = "post") // 기본 지연 로딩 (LAZY)
    private List<Comment> comments;
}
728x90
728x90

JpaRepository 상속 시 메서드 이름 구성

  • 메서드 이름은 findBy, findAllBy, existsBy, deleteBy 등의 접두사로 시작한다.
  • 이 접두사는 메서드가 어떤 기능을 수행하는지를 나타낸다.
    • findBy는 조회를, existsBy는 존재 여부 확인을, deleteBy는 삭제를 의미한다.

속성 이름 사용

  • 메서드 이름에서 사용하는 속성 이름은 엔티티 클래스의 필드 이름과 일치해야 한다. 대소문자를 구분하지 않고 CamelCase로 작성한다. 
  • findByUserName(String userName)에서 userName은 User 엔티티의 userName 필드와 일치해야 한다.

조건 연산자

  • Containing, Is, Equals, Between, LessThan, GreaterThan, In, Before, After, StartingWith, EndingWith, Not, True, False, OrderBy
  • findByUserNameAndEmail(String userName, String email)은 사용자 이름과 이메일이 모두 일치하는 엔티티를 조회한다.

정렬 및 페이징

  • 메서드 이름에 OrderBy 또는 Top, First, Last 등을 사용하여 정렬 순서 또는 결과의 최대 개수를 지정할 수 있다.
  • findTop3ByOrderByCreatedAtDesc()은 생성일자를 기준으로 내림차순으로 최대 3개의 결과를 조회한다.

기타 주의 사항

  • 메서드 이름에서 예약된 키워드( And, Or, Between, LessThan, GreaterThan )를 올바르게 사용해야 한다.
  • 메서드 이름은 가능한 한 명확하고 직관적으로 작성해야 한다.

 

주요 키워드

  • Containing: SQL의 LIKE '%keyword%'와 유사. 부분 일치를 의미
    • List<Post> findByTitleContaining(String title);
  • Is, Equals: 필드 값이 정확히 일치하는 경우
    • List<User> findByUsernameIs(String username); List<User> findByUsernameEquals(String username);
  • Between: 두 값 사이에 있는 경우
    • List<Order> findByOrderDateBetween(LocalDate startDate, LocalDate endDate);
  • LessThan, LessThanEqual: 값이 특정 값보다 작거나 작은 경우
    • List<Product> findByPriceLessThan(Double price); List<Product> findByPriceLessThanEqual(Double price);
  • GreaterThan, GreaterThanEqual: 값이 특정 값보다 크거나 큰 경우
    • List<Product> findByPriceGreaterThan(Double price); List<Product> findByPriceGreaterThanEqual(Double price);
  • In: 여러 값 중 하나와 일치하는 경우
    • List<User> findByUsernameIn(List<String> usernames);
  • Before, After: 날짜가 특정 날짜 이전이거나 이후인 경우
    • List<Event> findByEventDateBefore(LocalDate date); List<Event> findByEventDateAfter(LocalDate date);
  • StartingWith, EndingWith: 문자열이 특정 접두사 또는 접미사로 시작하거나 끝나는 경우
    • List<User> findByUsernameStartingWith(String prefix); List<User> findByUsernameEndingWith(String suffix);
  • Not: 특정 조건과 일치하지 않는 경우
    • List<User> findByUsernameNot(String username);
  • True, False: Boolean 필드가 true 또는 false인 경우
    • List<User> findByActiveTrue(); List<User> findByActiveFalse();
  • OrderBy: 결과 정렬. Asc 또는 Desc를 사용하여 오름차순 또는 내림차순 지정
    • List<Post> findByUserOrderByCreatedAtDesc(User user);

 

ex) 사용자 이름으로 게시글 검색

Post 엔티티와 연관된 User 엔티티의 username 필드를 기반으로 게시글 검색

List<Post> findByUserUsernameContaining(String username);
728x90

+ Recent posts