Youtube 개발자 인큐티비의 SpringBatch 편을 참고하여 질문지를 리스트업 했습니다. 

 

  • 왜 스프링 배치를 사용하는가?
    • 대용량 데이터를 처리해야 함
    • 사용자 개입 없이 동작
    • 로깅, 통계처리, 트랜잭션 등의 비즈니스 로직 외에 배치 어플리케이션에 필요한 기능 사용 가능
    • 지정한 시간 내에 다른 어플리케이션을 방해하지 않고 수행
    • 충돌이나 중단 되었을 때 컨트롤이 가능함
  • 멱등성은 어떻게 유지하는가?
    • 멱등성 : 연산을 여러번 적용하더라도 결과가 달라지지 않는 성질
    • Spring Batch Job Parameter를 사용하여 외부에서 값을 주입받도록 하여 제어가 불가능한 코드를 제거한다.
      • ex) LocalDateTime.now()
    • 멱등성이라는 패러다임이 Spring Batch와는 적합하지 않다는 내용의 의견도 많다. (https://namocom.tistory.com/752)
      • Job Parameter를 이용해 외부에서 주입하는 것은 책임의 이동 정도로 이해하자는 의견도..
  • Spring Batch 메타 데이터 테이블은 어떤 것이 있는가?
    • (맨 하단 ERD 참고)
    • BATCH_JOB_INSTANCE (최상위)
      • Job Instance 객체의 정보를 담고 있음 
    • BATCH_JOB_EXECUTION_PARAMS
      • Job Parameter 정보를 담고 있음, Job 실행 시 사용된 파라미터 저장
      • 정규화되지 않은 형태의 테이블로 TYPE_CD 컬럼에서 저장되는 파라미터 타입을 가진다. 
    • BATCH_JOB_EXECUTION
      • Job Execution 객체의 정보를 가지고 있음, Job이 run 할 때마다 row가 추가됨
    • BATCH_STEP_EXECUTION
      • Step Execution 객체의 정보와 대응되는데 하나의 Job Execution에서 사용하는 Step 개수만큼 테이블의 row에 추가된다.
      • 배치가 돌아서 처리한 개수를 알고 싶을 때는 이 테이블을 찾아보면 된다. 
    • BATCH_JOB_EXECUTION_CONTEXT
      • 하나의 Job Execution 에 대해 하나의 Job Execution Context가 존재하며, Job레벨의 모든 데이터를 다 가지고 있다.
    • BATCH_STEP_EXECUTION_CONTEXT
      • 하나의 Step Execution 에 대해 하나의 Step Execution Context가 존재하며, 
      • JobInstance가 중단된 위치에서 다시 시작할 수 있도록 실패 후 검색되어야 하는 정보도 담고 있음
  • 배치 중간 실패하면 어떻게 처리하는지?
    • skip
      • 데이터를 처리하는 동안 설정된 Exception이 발생했을 경우, 해당 데이터 처리를 건너뛰는 기능
      • default 값은 0으로 사용을 원하는 경우 반드시 0 이상의 숫자를 입력해주고, 어떤 Exception을 skip 할 것인지 반드시 명시해주어야 한다. 
      • chunk 내부에서 이뤄지는데 Read/Process/Write 하는 과정에 설정해줄 수 있다. 
    • retry (아래 그림 참고)
      • 데이터를 Process/Write 하는 과정에서 설정된 Exception이 발생했을 경우, 지정한 정책에 따라 데이터 처리 재시도 하는 기능.
      • Read과정에서 주로 발생하는 FlatFileParseException 에 대한 문제는 대부분  Skip에서 처리가 된다.
      • 예를 들면 DeadlockLoserDataAccessException 발생 시 Retry가 일어나도록 설정할 수 있다. 다른 프로세스에서 처리중인 데이터에 새로운 프로세스가 접근하면 Locak이 걸려 있어 에러가 발생하는데 Retry를 하면 성공할 수 있을 것이다. 
  • 트랜잭션 관리 왜 청크단위로 하는가?
    • Chunk 
      • 각 커밋 사이에 처리되는 row 수
    • Chunk 지향 처리란?
      • Chunk 단위로 트랜잭션을 다루는 것 
      • Reader와 Processor에서는 1건씩 다뤄지고, Writer에선 Chunk 단위로 처리
        • Reader에서 데이터를 하나 읽어옵니다
        • 읽어온 데이터를 Processor에서 가공합니다
        • 가공된 데이터들을 별도의 공간에 모은 뒤, Chunk 단위만큼 쌓이게 되면 Writer에 전달하고 Writer는 일괄 저장합니다.
    • Why Chunk?
      • 커밋을 매번 하면 비용이 많이 듭니다. 데이터가 많은 경우라면 (Spring Batch를 사용한다면 당연하겠죠?) 매번 커밋을 하는 것은 이상적이지 않고, 각 트랜잭션에서 가능한 한 많은 항목을 처리하는 것이 바람직합니다. 이러한 이유로 한 커밋 내에서 처리하는 수를 chunk로 관리하게 된 것입니다. 
      • Chunk-oriented 프로세싱을 하게 되면 다양한 기능들을 사용할 수 있는 것이 장점인데 skip, retry, 특정 Exception에 대한 Rollback, 다양한 ItemReader 그리고Cursor, Paging 등이 대표적이다.
    • Page Size와 Chunk Size
      • PagingItemReader를 사용하면 보이는 Page Size는 한번에 조회할 Item의 양이고, Chunk Size는 한번에 처리될 트랜잭션 단위이다.
      • 보편적으로 두개의 사이즈 크기는 일치하는게 좋다. 
  • Cursor 기반 vs Paging 기반
    • Cursor는 한칸씩 커서를 옮기면서 데이터 1 Row씩을 가져온다.
      • 배치 처리가 완료될 때까지 데이터를 읽어오기 때문에 DB Connection Time이 Paging보다 길다. 
      • 모든 데이터를 메모리에 저장하기 때문에 메모리 사용량이 많다.
    • Paging은 설정한 PageSize 만큼 데이터를 가져오며 데이터 결과의 순서가 보장될 수 있도록 order by 사용 필요
      • PageSize만큼 DB Connection을 읽고 종료한다. 따라서 Cursor에 비해 상대적으로 DB Connection Time이 적다. 
      • 하지만 트랜잭션을 여러번 타야하는 단점이 있긴 하다. 
  • Multi Thread & Partitioning
    • 정해진 시간 안에 많은 데이터를 처리하기 위해 성능을 높이기 위해 사용하는 방법
    • 서비스에 적재된 데이터가 적을 경우에는 Spring Batch의 기본 기능들만 사용해도 큰 문제가 없으나, 데이터가 엄청나게 많이 쌓일시 배치 애플리케이션 역시 확장이 필요
    • Multi-threaded Step (아래 그림 참고)
      • 단일 Step을 수행할 때, 해당 Step 내의 각 Chunk를 별도의 여러 쓰레드에서 실행하는 방법
      • 정한 개수(throttleLimit)만큼의 스레드를 생성하여 수행하는데 ItemReader는 반드시 Thread-safe인지 확인해야 하며(데이터를 중복으로 읽어 오지 않게 하기 위해) 스프링 배치에서 제공하는 것중 JdbcPagingItemReader, JpaPagingItemReader가 Thread-safe하다.
    • Partitioning
      • Master가 데이터를 파티셔닝 한 다음 Slave가 개별 스레드를 통해 각 파티션을 처리하는 방식
      • 각 SlaveStep은 ItemReader / ItemProcessor / ItemWriter 등을 갖고 동작하며 작업을 독립적으로 병렬 처리합니다.
    • Multi-threaded 는 Thread-safe를 신경써야 하나 Partitioning은 Thread-safe하지 않아도 됩니다. 
  • tasklet model vs chunk (reader,processor,writer)
    • Step은 Tasklet 혹은 Chunk로 처리할 수 있다. 
    • When to use
      • Tasklet 
        • 실행할 작업이 간단하여 집계가 필요없고, 실행만 필요한 경우
      • Chunk
        • 실행할 작업이 복잡하고 청크 지향 처리를 사용하는 읽기, 처리 및 쓰기와 관련된 작업 실행이 포함된다고 가정합니다.
  • Batch 실행은 어떻게 하는지 
    • Jenkins로 
  • 모니터링은 어떻게? 
    • 예전에는 Spring Batch Admin이 있었는데 deprecated 되었고, 현재는 spring에서 Spring Cloud Data Flow 를 사용하라고 한다. 
    • 요건 직접 사용해보고 내용 추가해보겠습니다~
 
 

 

Spring Batch Meta Table


 

Spring Batch Retry


Multi-thread Step


 

AsyncItemProcessor / AsyncItemWriter


Cursor vs Paging

 


참고

https://jojoldu.tistory.com/489

https://docs.spring.io/spring-batch/docs/current/reference/html/step.html#commitInterval

https://www.egovframe.go.kr/wiki/doku.php?id=egovframework:rte2:brte:batch_core:skip_repeat_retry 

https://velog.io/@backtony/Spring-Batch-%EB%A9%80%ED%8B%B0-%EC%8A%A4%EB%A0%88%EB%93%9C-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8B%B1

 

HashMap: {1=Python, 2=JavaScript}
HashMap after put():
{1=Python, 2=JavaScript, 3=Java}
HashMap after replace():
{1=Python, 2=JavaScript}

 

put() 및 replace() 메서드의 구문은 HashMap에서 매우 유사합니다.

// syntax of put()
hashmap.put(key, value)

// syntax of replace()
hashmap.replace(key, value)

그리고 해시맵에 지정된 키에 대한 매핑이 포함되어 있으면 두 메서드 모두 지정된 키와 연결된 값을 바꿉니다. 그러나 해시맵에 지정된 키로 찾을 수 있는 값이 없는 경우

* put() 메서드는 지정된 Key와 Value에 대한 새 데이터를 삽입합니다.

* replace() 메서드는 null을 반환합니다.

import java.util.HashMap;

class Main {
  public static void main(String[] args) {

    // create an HashMap
    HashMap<Integer, String> languages1 = new HashMap<>();

    // insert entries to HashMap
    languages1.put(1, "Python");
    languages1.put(2, "JavaScript");

    // create another HashMap similar to languages1
    HashMap<Integer, String> languages2 = new HashMap<>();

    // puts all entries from languages1 to languages2
    languages2.putAll(languages1);
    System.out.println("HashMap: " + languages1);

    // use of put()
    languages2.put(3, "Java");
    System.out.println("HashMap after put():\n" + languages2);

    // use of replace()
    languages1.replace(3, "Java");
    System.out.println("HashMap after replace():\n" + languages1);

  }
}

Output

put() 메서드는 HashMap에 새 매핑(3 = Java)을 추가합니다. replace() 메서드는 어떤 작업도 수행하지 않습니다.
 (Key가 없어도 에러는 나지 않네요?)

HashMap: {1=Python, 2=JavaScript}
HashMap after put():
{1=Python, 2=JavaScript, 3=Java}
HashMap after replace():
{1=Python, 2=JavaScript}

 

 

출처

https://www.programiz.com/java-programming/library/hashmap/replace

인터페이스에서 디폴트 메소드를 작성할 수 있게 됨으로써 추상 클래스와 언뜻 보면 차이가 없어 보인다.

실제로 인터페이스에 디폴트 메소드를 작성해서 코드를 작성해보려니 추상으로 풀 수도 있는거 아냐? 라는 생각이..

다시금 추상클래스와 인터페이스와의 차이를 알아보도록 하자.

 

인터페이스와 추상 클래스는 존재 목적이 다르다.

=> 추상 클래스는 추상 클래스를 상속 받아서 기능을 이용하고, 확장하는 데에 있다.

=> 인터페이스는 큰 틀을 사전에 짜놓는 것으로써 그 틀에 맞추어 구현을 하라는 데 의의가 있다. 이를 통해 인터페이스를 구현한 객체들은 동일한 동작을 약속할 수 있게 됩니다.

  • 추상 클래스, 인터페이스 둘 다 불완전 한 것이기 때문에 인스턴스화가 불가, extends하거나 implements해야 합니다.
  • 인터페이스는 여러개 인터페이스를 함께 구현 가능, 추상클래스는 다중 상속이 불가하다.
  • 추상 클래스에는 public, protected, private 메소드를 가질 수 있습니다. 반면에 인터페이스는 public만 허용됩니다.
  • 추상 클래스에는 멤버변수 선언이 가능하지만 인터페이스는 public static final 이어야 한다. 

=> 각각 다른 추상클래스를 상속하는데 공통된 기능이 필요하다면? 해당 기능을 인터페이스로 작성해서 구현

 

 

+ 학원에서 교육 받을 때에는 아래와 같이 교육받았다... 

추상클래스는 IS - A "~이다".

인터페이스는 HAS - A "~을 할 수 있는".

 

출처

https://codechacha.com/ko/java8-default-methods/

https://myjamong.tistory.com/150

아래 강의를 보고 정리한 내용입니다. 

[우아한테크세미나] 190926 우아한스프링배치 by 우아한형제들 이동욱님  https://youtu.be/_nkJkWVH-mo

 

  • JobParameter
    • Spring Batch는 외부에서 파라미터를 주입받아 Batch 컴포넌트에서 사용 할 수 있다. 
    • 사용법 : @Value("#{jobParameters[파라미터명]}") 타입 이름
  • @JobScope
    • Step에서 사용할 수 있고, Job이 실행되는 시점에 Bean이 생성된다.
    • 사용 예
      • @JobScope라고 선언해줘야 호출한 곳에서 넘겨진 파라미터를 받을 수 있다.
      • 컴파일 에러 방지를 위해 우선은 null값을 넘겨준다.
@Bean
public Job scopeJob() {
	return jobBuilderFactory.get("scopeJob")
    			.start(scopeStep1(null))
                            .next(scopeStep2())
                            .build();
}

@Bean
@JobScope
public Step scopeStep1(@Value("#{jobParameters[requestDate]}") String requestDate) {
	return stepBuilderFactory.get("scopeStep1")
    				 .tasklet((contribution, chunkContext) -> {
                             	log.info(">>> ");
                                return RepeatStatus.FINISHED;
                             })
                             .build();
}
  • @StepScope
    • Tasklet / Reader / Processor / Writer에서 사용할 수 있다.
@Bean
pulbic Step scopeStep2() {
	return stepBuilderFactory.get("scopeStep2")
    .tasklet(scopeStep2Tasklet(null))
    .build();
}

@Bean
@StepScope
public Tasklet scopeStep2Tasklet(@Value("#{jobParameters[requestDate]}") String requestDate) {
	return (contribution, chunkContext) -> {
    	log.info(">>>");
        return RepeatStatus.FINISHED;
    }
}

 

  • Type
    • Spring Batch의 JobParameter는 Long / String / Double / Date 타입들을 지원한다.
    • Enum / LocalDate / LocalDateTime은 지원하지 않는다.
      • 그렇다면?
        • @Value의 특성을 이용하면 String을 매번 LocalDateTime으로 변경하지 않아도 된다. (아래 코드 참고)
          • setter메소드에 @Value를 선언 문자열로 받은뒤 원하는 타입으로 세팅
@Slf4j
@Getter
@NoArgsConstructor
public class CreateDateJobParameter {
	private LocalDate localDate;
    
    @Value("#{jobParameters[createDate]}")
    public void setCreateDate(String createDate) {
    	DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        this.createDate = LocalDate.parse(createDate, formatter);
    }
}

 

@JobScope로 설정해놨기 때문에 Job이 실행될 때 Bean이 실행된다. 그러면 job실행되는 시점에 jobParameter Bean이 생성되서 값이 세팅된 다음에 CreateDateJobParameter에 Injection이 된다.

public class JObParameterBatchConfiguration {
	private final CreateDateJobParameter jobParameter;
    
    @Bean(BATCH_NAME + "jobParameter")
    @JobScope
    public CreateDateJobParameter jobParameter() {
    	return new CreateDateJobParameter():
    }
}

 

그 다음부터는 아래와 같이 사용하면 된다.jobParameter.getCreateDate()만 호출하면 된다. 

@Bean(name = BATCH_NAME + "_reader")
@StepScope
public JpaPagingItemReader<Product> reader() {
	Map<String, Object> params = new HashMap<>();
    params.put("createDate", jobParameter.getCreateDate()); // 어디에서나 LocalDate을 가져올 수 있음
    
    return new JpaPagingItemReaderBuilder<Product>()
    			.name(BATCH_NAME + "_reader")
                .entityManagerFactory(entityManagerFactory)
                .pageSize(chunkSize)
                .queryString("SELECT p FROM Product p WHERE p.createDate = :createDate")
                .parameterValues(params)
                .build();
}

 

  • @JobScope, @StepScope의 특징(Late Binding)을 활용해보기
    •  @JobScope나 @StepScope가 선언된  Bean의 경우에는 Job이나 Step이 실행될 때 Bean이 생성되는 특징을 가진다. (일반 Spring처럼 애플리케이션이 로딩될 때 Bean이 생성되는 것이 아니라)
    • 그 뜻은 애플리케이션 실행 후에도 동적으로 reader / processor / writer bean 생성이 가능하다는 뜻
      • 예를 들아보자
        • 정산하는 시스템에서 ERP 연동이 2-30개가 되는데 하는 일이 비슷
        • 주문데이터 긁어와서 어디에 보내는 것~ 파라미터가 주문이냐 매출이냐 광고냐 그리고 읽어와야 될 테이블 이 값만 다르고 다른 건 전부 동일 
        • 그럴 때 마다 같은 클래스를 계속 생성할 수 없으니 LateBinding을 이용해서 파라미터로 주문으로 던지면 주문 테이블에서 읽어오는 리더로 바꿔서 배치를 돌리고, 광고면 광고 테이블에서 읽어오는 리더로 바꿔서 배치를 돌리는 것이다.
private StimpleStepBuilder<EaiEntity, EaiItem> readerAndProcessor(String dealCode, String txDateStr) {
	EaiReaderParameterDato parameterDto = createParameter(dealCode, txDateStr);
    
    EaiTaskletType readerProcessorType = EaiDatailType.findTaskletType(dealCode);
    EaiReaderFactory readerCreator = readerProcessorType.getReaderCreator();
    
    return stepBuilderFactory.get(stepName)
    		 	.<~>chunk(chunkSize)
                             .reader(readerCreator.create(chunkSize, emf, parameterDto)) 
                             .processor(eaiProcessor);
}

String 배열과 ArrayList의 성능 차이에 대한 OKKY의 질문의 답변 중 몇 가지 기억해야 해둬야 할 게 있어서 기록

https://okky.kr/article/266413


댓글 )

이미 답변이 충분히 나왔지만... 몇자 적자면 간단한 자료구조만 공부하셔도 알 수 있는 질문입니다.
배열이던 ArrayList던 자기가 사용할 공간(힙)을 얻을 때가 질문자가 말하는 성능의 포인트입니다.

배열을 사용해 5개만 사용하는 공간에 +1을 하고 싶다면 6개 공간을 만들고 앞에 5개를 복사하고 1 하는 값을 추가합니다. 즉 처음 5개 만드는 공간(성능), 두 번째 6개 만드는 공간입니다.

ArrayList라고 별반 다르지 않습니다. 위의 과정이 add에 들어 있습니다. add 할 때 공간이 부족하면 추가하고 이미 있던 것 복사하고 add 합니다. 그게 기본적으로 10(용량)이라고 초기화가 되어 있고요. 만약 arrayList에 들어갈 최소 크기를 안다면 new ArrayList(사이즈) 이렇게 하시면 입력하면서 메모리를 추가(기본 10) 하는 비용은 좀 줄겠죠... 물론 근데 저렇게까지 한다고 해도 엄청난 양을 넣지 않는 이상 밀리 세컨드도 차이 안 납니다.

그리고 remove 할 땐 그냥 해당 index 없앨 뿐입니다. 실제 할당받은 메모리를 다시 작게 하거나 하진 않습니다. 100000개 넣어 놓고 1개만 남기고 다 지우더라도 메모리 할당받은 건 유지하고 있습니다. 나중에 다시 쓸 수 있으니까요. 뭐 이런 게 많아지면 힙 메모리 없다고 나올 수 도 있겠죠...

아무튼~ 단순 배열로 해결할 수 있다면 그냥 그렇게 하시면 되구요.. 추가가 일어난다 그럼 그냥 잘 만들어진 자료구조 사용하면 됩니다.

자료구조 선택할땐 구조유형을 따져서 조회보단 in/out 자주 일어난다면 위분 말처럼 링크드리스트 쓰구요 반대로 in/out 보다 랜덤엑세스가 많다면 ArrayList쓰면 됩니다. ArrayList자체가 단순 배열과 다른거는 자료구조를 포함하고 있다는것 외에는 동일합니다.


 

정리하자면

 

1. 크기를 안다고 하면 String 배열을 이용해 지정해두면 좋겠지만 그게 성능에 크게 영향을 미치지는 않는다.

2. 배열에 add로 값을 추가하게 되면 새로 정의된 크기의 공간을 힙에 새로 할당해서 기존 값 복사하고 추가하는 것

3. 2번의 이유로 기존에 정의된 크기에서 오버되는 경우에는 성능상에 문제가 생길 수 있다. (디폴트 크기는 10)

4. remove의 경우에는 해당 index를 없앨 뿐이고, 실제 할당받은 메모리는 그대로 유지된다. (메모리 차지는 계속함)

5. 값을 추가하고 제거하는 케이스가 빈번할 경우에는 LinkedList를 쓰는 것이 낫다. 

6. 생성된 List에 단순 접근하고 작업하는 경우에는 ArrayList를 쓰는 것이 낫다.

 

+ Recent posts