├── .github └── workflows │ └── automerge.yml ├── .gitignore ├── 02주차 ├── 발표자료 │ ├── [2주차]_1,2장_스프링배치_김광훈.md │ ├── [2주차]_1,2장_스프링배치_황준호.md │ └── tmp.md └── 요약본 │ ├── [2주차]_1,2장_스프링배치_김명수.md │ ├── [2주차]_1,2장_스프링배치_김민석.md │ ├── [2주차]_1,2장_스프링배치_이호빈.md │ ├── [2주차]_1,2장_스프링배치_함호식.md │ └── tmp.md ├── 03주차 ├── 발표자료 │ ├── [3주차]_4장_스프링배치_함호식.md │ ├── [3주차]_4장_잡과스텝이해하기-1_김성모.md │ └── tmp.md └── 요약본 │ ├── [3주차]_4장_잡과스텝이해하기-1_김광훈.md │ ├── [3주차]_4장_잡과스텝이해하기-1_김명수.md │ ├── [3주차]_4장_잡과스텝이해하기-1_김민석.md │ ├── [3주차]_4장_잡과스텝이해하기-1_이호빈.md │ ├── [3주차]_4장_잡과스텝이해하기-1_황준호.md │ └── tmp.md ├── 04주차 ├── 발표자료 │ ├── [4주차]_4장_잡과스텝이해하기-2_김광훈.md │ ├── [4주차]_4장_잡과스텝이해하기-2_김명수.md │ └── tmp.md └── 요약본 │ ├── [4주차]_4장_스프링배치_함호식.md │ ├── [4주차]_4장_잡과스텝이해하기-2_김민석.md │ ├── [4주차]_4장_잡과스텝이해하기-2_김성모.md │ ├── [4주차]_4장_잡과스텝이해하기-2_이호빈.md │ ├── [4주차]_4장_잡과스텝이해하기-2_황준호.md │ └── tmp.md ├── 05주차 ├── 발표자료 │ ├── [5주차]_5장_JobRepository와_메타데이터_이호빈.md │ ├── [5주차]_5장_JobRepository와메타데이터_김민석.md │ └── tmp.md └── 요약본 │ ├── [5주차]_5장_JobRepository와_메타데이터_김광훈.md │ ├── [5주차]_5장_JobRepository와_메타데이터_황준호.md │ ├── [5주차]_5장_JobRepository와메타데이터_김명수.md │ ├── [5주차]_5장_JobRepository와메타데이터_김성모.md │ ├── [5주차]_5장_스프링배치_함호식.md │ └── tmp.md ├── 06주차 ├── 발표자료 │ ├── [6주차]_6장_잡_실행하기_김성모.md │ ├── [6주차]_6장_잡실행하기_이호빈.md │ └── tmp.md └── 요약본 │ ├── [6주차]_6장_스프링배치_함호식.md │ ├── [6주차]_6장_잡_실행하기_김광훈.md │ ├── [6주차]_6장_잡_실행하기_김민석.md │ ├── [6주차]_6장_잡_실행하기_황준호.md │ └── tmp.md ├── 07주차 ├── 발표자료 │ ├── [7주차]_7장_ItemReader(1)_김성모.md │ ├── [7주차]_7장_ItemReader_김민석.md │ └── tmp.md └── 요약본 │ ├── [7주차]_7장_ItemReader-1_김명수.md │ ├── [7주차]_7장_ItemReader-1_이호빈.md │ ├── [7주차]_7장_ItemReader-1_황준호.md │ ├── [7주차]_7장_스프링배치_함호식.md │ └── tmp.md ├── 08주차 ├── 발표자료 │ ├── [7주차]_7장_ItemReader-2_황준호.md │ ├── [8주차]_7장_ItemReader-2_김명수.md │ └── tmp.md └── 요약본 │ ├── [8주차]_7장_ItemReader(2)_김성모.md │ ├── [8주차]_7장_ItemReader-2_김광훈.md │ ├── [8주차]_7장_ItemReader-2_이호빈.md │ ├── [8주차]_7장_ItemReader_김민석.md │ ├── [8주차]_7장_스프링배치_함호식.md │ └── tmp.md ├── 09주차 ├── 발표자료 │ ├── CompositeItemProcessor.png │ ├── [9주차]_8장_ItemProcessor_김광훈.md │ ├── [9주차]_8장_스프링배치_함호식.md │ └── tmp.md └── 요약본 │ ├── [9주차]_8장_ItemProcessor_김명수.md │ ├── [9주차]_8장_ItemProcessor_김민석.md │ ├── [9주차]_8장_ItemProcessor_김성모.md │ ├── [9주차]_8장_ItemProcessor_이호빈.md │ ├── [9주차]_8장_ItemProcessor_황준호.md │ └── tmp.md ├── 10주차 ├── 발표자료 │ ├── [10주차]_9장_ItemWriter_김성모.md │ ├── [10주차]_9장_ItemWriter_황준호.md │ └── tmp.md └── 요약본 │ ├── [10주차]_9장_ItemWriter_김명수.md │ ├── [10주차]_9장_ItemWriter_김민석.md │ ├── [10주차]_9장_ItemWriter_이호빈.md │ ├── [10주차]_9장_스프링배치_함호식.md │ ├── [9주차]_9장_ItemWriter-1_김광훈.md │ └── tmp.md ├── 11주차 ├── 발표자료 │ ├── [11주차]_9장_ItemWriter_김민석.md │ ├── [11주차]_9장_ItemWriter_함호식.md │ └── tmp.md └── 요약본 │ ├── [11주차]_9장_ItemWriter_김광훈.md │ ├── [11주차]_9장_ItemWriter_김명수.md │ ├── [11주차]_9장_ItemWriter_김성모.md │ ├── [11주차]_9장_ItemWriter_이호빈.md │ ├── [11주차]_9장_ItemWriter_황준호.md │ └── tmp.md ├── 12주차 ├── 발표자료 │ ├── [12주차]_11장_확장과 튜닝_김성모.md │ ├── [12주차]_11장_확장과튜닝_김광훈.md │ └── tmp.md └── 요약본 │ ├── [12주차]_11장_확장과튜닝_김명수.md │ ├── [12주차]_11장_확장과튜닝_김민석.md │ ├── [12주차]_11장_확장과튜닝_이호빈.md │ ├── [12주차]_11장_확장과튜닝_함호식.md │ ├── images │ ├── visualVM_custome_test.png │ ├── visualVM_monitor.png │ ├── visualVM_overview.png │ ├── visualVM_plugin.png │ ├── visualVM_run_debug.png │ ├── visualVM_sampler.png │ └── visualVM_thread.png │ └── tmp.md ├── 13주차 ├── 발표자료 │ ├── [13주차]_12장_클라우드_네이티브_배치_김명수.md │ ├── [13주차]_13장_배치_처리_테스트하기_이호빈.md │ └── tmp.md └── 요약본 │ ├── [13주차]_13장_배치_처리_테스트하기_김광훈.md │ ├── [13주차]_13장_배치_처리_테스트하기_김민석.md │ ├── [13주차]_13장_배치_처리_테스트하기_함호식.md │ ├── [13주차]_13장_배치처리테스트하기_김성모.md │ └── tmp.md └── README.md /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: automerge 2 | on: 3 | schedule: 4 | - cron: '0 13 * * 5' 5 | jobs: 6 | automerge: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: automerge 10 | uses: "pascalgn/automerge-action@v0.13.1" 11 | env: 12 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 13 | MERGE_LABELS: "" 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/main/generated/ 2 | HELP.md 3 | .gradle 4 | build/ 5 | !gradle/wrapper/gradle-wrapper.jar 6 | !**/src/main/**/build/ 7 | !**/src/test/**/build/ 8 | /log 9 | 10 | ### STS ### 11 | .apt_generated 12 | .classpath 13 | .factorypath 14 | .project 15 | .settings 16 | .springBeans 17 | .sts4-cache 18 | bin/ 19 | !**/src/main/**/bin/ 20 | !**/src/test/**/bin/ 21 | 22 | ### IntelliJ IDEA ### 23 | .idea 24 | *.iws 25 | *.iml 26 | *.ipr 27 | out/ 28 | !**/src/main/**/out/ 29 | !**/src/test/**/out/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### VS Code ### 39 | .vscode/ 40 | -------------------------------------------------------------------------------- /02주차/발표자료/[2주차]_1,2장_스프링배치_황준호.md: -------------------------------------------------------------------------------- 1 | ## 1장. 배치와 스프링 2 | 3 | ### 배치 처리란? 4 | 5 | - 상호작용이나 중단 없이 유한한 양의 데이터를 처리하는 것 6 | - 일단 시작되면 아무런 개입 없이 어떤 형태로든 완료된다 7 | 8 | ### 배치 처리는 왜 필요한가? 9 | 10 | 1. 필요한 정보를 실제 처리가 시작되기 전에 미리 수집할 수 있다 11 | 2. 때로는 사업적으로 도움이 된다 12 | 3. 자원을 더 효율적으로 사용할 수 있다 13 | 14 | ### 배치 처리를 개발하는 데 기술적으로 해결해야 하는 과제들 15 | 16 | 1. (코드의) 사용성 17 | - 공통 컴포넌트를 쉽게 확장해 새로운 기능을 추가할 수 있는가? 18 | - 기존 컴포넌트를 변경할 때 시스템 전체에 미치는 영향을 알 수 있도록 단위테스트가 잘 마련돼있는가? 19 | - 잡이 실패할 때 디버깅에 오랜 시간을 소비하지 않고, 언제, 어디서, 왜 실패했는지 알 수 있는가? 20 | 2. 확장성 21 | - 배치가 처리할 수 있는 규모가 웹 애플리케이션의 규모보다 몇자리 수 이상 더 클 수 있다 22 | 3. 가용성 23 | - 필요할 때 바로 배치 처리를 수행할 수 있는가? 24 | - 허용된 시간 내에 잡을 수행함으로써 다른 시스템에 영향을 미치지 않게 할 수 있는가? 25 | 4. 보안 26 | - 민감한 데이터베이스 필드는 암호화돼 있는가? 27 | - 실수로 개인 정보를 로그로 남기지는 않는가? 28 | - 자격증명이 필요한가? 29 | 30 | ### 왜 자바로 배치를 처리하는가? 31 | 32 | 1. 유지 보수성 33 | - 스프링 프레임워크는 테스트 용이성이나 추상화같은 이점을 얻을 수 있도록 설계됐다 34 | - DI를 통해 객체간 결합을 제거할 수 있다 35 | - 테스트 도구를 활용하여 유지 보수시 발행할 수 있는 위험을 줄일 수 있다 36 | - JDBC 코드나 파일I/O API를 직접 다룰 필요 없다 37 | - 트랜잭션 및 커밋 횟수같은 것들을 제공하므로 실패시 무슨 일을 해야 하는지 관리할 필요가 없다 38 | 2. 유연성 39 | - 배치 처리가 가능한 플랫폼은 메인프레임, C++/UNIX 등이 있다 40 | - 위 방식들은 JVM의 유연성과 스프링 배치의 기능들을 제공하지 않는다 41 | - 스프링 배치는 유닉스 계열 또는 윈도우 서버, 데스크탑 등등 어디에서든 돌아간다 42 | - 웹 애플리케이션에서 이미 테스트 및 디버깅 된 서비스를 배치 처리에서 동일하게 바로 사용할 수 있다 43 | 3. 확장성 44 | - 단일 서버 내의 단일 JVM에서 배치처리를 수행할수도, 나눠서 수행할수도 있다. 45 | - 클라우드 리소스를 사용하여 배치처리할 수 있다 46 | 4. 개발 리소스 47 | - 배치 처리 코드는 수명이 길기 때문에 개발 인력을 구하는 것도 중요하다. 48 | - 스프링 개발자는 많다 49 | 5. 지원 50 | - 온라인 커뮤니티가 잘 갖춰져 있다 51 | - 소스코드에 접근할 수 있고 비용을 지불하면 기술 지원을 받을 수 있다 52 | 6. 비용 53 | - 스프링 배치는 가장 저렴한 솔루션 54 | 55 | ### 스프링 배치의 사용 사례 56 | 57 | - ETL(추출, 변환, 적재) 처리 58 | - 스프링 배치의 청크기반 처리 및 확장 기능은 ETL워크로드에 자연스럽게 들어맞는다 59 | - 데이터 마이그레이션 60 | - 잡을 기동하는데 많은 코딩이 필요없다 61 | - 마이그레이션에 필요한 커밋 횟수 측정이나 롤백 기능을 제공한다 62 | - 병렬 처리 63 | - 멀티 코어 도는 멀티 서버에 처리를 분산하는 기능을 제공한다 64 | 65 | ### 스프링 배치 프레임워크 66 | 67 | - 스프링 배치의 구조 68 | 69 | - 레이어 구조로 조립된 세개의 티어로 이뤄져 있다 70 | 71 | ![Figure 1.1: Spring Batch Layered Architecture](https://docs.spring.io/spring-batch/docs/4.3.x/reference/html/images/spring-batch-layers.png) 72 | 73 | - 애플리케이션 레이어 74 | 75 | - 코어 레이어와 상호작용하는데 대부분의 시간을 소비한다 76 | 77 | - 코어 레이어 78 | 79 | - 배치 도메인을 정의하는 모든 부분이 포함된다 80 | - `Job`, `Step`, `JobLauncher`, `JobParameters` 등이 있다 81 | 82 | - 인프라스트럭쳐 레이어 83 | 84 | - 파일, 데이터베이스 등으로부터 읽고 쓸 수 있게 한다 85 | - 잡 실패 후 재시도될 때 어떤 일을 수행할 지 다룰 수 있게 한다 86 | 87 | - 잡 config 88 | 89 | - 중단이나 상호작용 없이 처음부터 끝까지 실행되는 처리 90 | 91 | - 여러 개의 스텝이 모여 이뤄질 수 있다 92 | 93 | - 여러 방법으로 구성할 수 있지만, 아래는 자바 구성 클래스에 구성하는 방법을 보여준다 94 | 95 | ```java 96 | @Bean 97 | public AccountTasklet accountTasklet() { 98 | // 커스텀 컴포넌트. 스프링 배치는 AccountTasklet이 완료될때까지 execute메서드를 반복해서 호출한다. (각각은 새 트랜잭션으로 호출됨) 99 | return new AccountTasklet(); 100 | } 101 | 102 | @Bean 103 | public Job accountJob() { 104 | Step accountStep = this.stepBuilderFactory.get("accountStep") 105 | .tasklet(accountTasklet()) 106 | .build(); 107 | 108 | return this.jobBuilderFactory.get("accountJob") 109 | .start("accountStep") 110 | .build(); 111 | } 112 | ``` 113 | 114 | - 잡 관리 115 | 116 | - 실패해서 재실행할 때 필요한 잡의 상태 정보를 유지해준다 117 | - 실패했을 때 데이터 무결성을 유지할 수 있도록 트랜잭션을 관리해준다 118 | 119 | 120 |
121 | 122 | ## 2장. 스프링 배치 123 | 124 | ### 스텝 125 | 126 | - 잡을 구성하는 독립된 작업의 단위 127 | - 스텝은 두가지 유형이 있음 128 | 1. tasklet기반 스텝 129 | - 비교적 더 간단함 130 | - `Tasklet`을 구현하면 됨. 스텝이 중지될 때까지 `execute`메서드가 계속 반복해서 수행된다 131 | - 초기화, 저장 프로시저 실행, 알림 전송 등과 같은 잡에서 사용 132 | 2. chunk기반 스텝 133 | - 약간 더 복잡함 134 | - 아이템 기반 처리에 사용 135 | - `ItemReader`, `ItemProcessor`(필수x), `ItemWriter`으로 구성 136 | - 스텝을 분리하는 것의 이점 137 | 1. 유연성 : 재사용할 수 있도록 여러 빌더 클래스 제공 138 | 2. 유지보수성 : 각 스텝은 독립적 -> 각 스텝의 단위테스트, 디버그, 변경 등을 할 수 있음. 또 독립적이기 때문에 여러 잡에서 재사용 가능 139 | 3. 확장성 : 스텝을 병렬로 실행할 수 있는 등의 확장 가능한 기능 제공 140 | 4. 신뢰성 : 오류 처리 방법(예외 발생시 재시도, 건너뛰기 등) 제공 141 | 142 | ### `JobRepository` 143 | 144 | - 다양한 배치 수행과 관련된 수치 데이터, 잡의 상태를 유지/관리 145 | - 실행된 스텝, 현재 상태, 읽은 아이템 수, 처리된 아이템 수 등이 저장됨 146 | - 관계형 데이터베이스 사용 147 | - 스프링 배치 내의 대부분의 주요 컴포넌트가 공유 148 | 149 | ### `JobLauncher` 150 | 151 | - `Job.execute`를 실행하는 역할 152 | - 잡이 재실행 가능한지, 잡을 어떻게 실행할건지(현재 스레드에서 할지, 스레드 풀을 통해 실행할지), 파라미터 유효성 검증 등의 처리도 함 153 | 154 | ### `Job`, `JobInstance`, `JobExecution` 155 | 156 | - `JobInstance` : 배치 잡의 논리적인 실행. "잡의 이름"과 "식별 파라미터"로 식별할 수 있다. 157 | - `JobExecution` : 배치 잡의 실제 실행. 잡을 구동할때마다 새로운 `JobExecution`을 얻는다. 158 | 159 | -> 한 잡을 같은 파라미터로 2번 실행했는데 첫번째는 실패, 두번째는 성공했다면 `JobInstance`는 1개, `JobExecution`는 2개 160 | 161 | ### `StepExecution` 162 | 163 | - `JobExecution`이 잡의 실제 실행을 나타내듯이 `StepExecution`은 스텝의 실제 실행을 나타낸다 164 | - `StepInstance`라는 개념은 존재하지 않는다 165 | 166 | ### 병렬화 방법 167 | 168 | 1. 다중 스레드 스텝을 이용해 잡 나누기 169 | - 잡은 청크라는 블록 단위로 처리되도록 구성됨 170 | - 각 청크는 독립적인 트랜잭션으로 처리됨 171 | - 일반적으로 각 청크는 연속해서 처리됨 172 | - 여러 스레드를 사용하도록 변경하면 처리량을 늘릴 수 있다 173 | 2. 스텝을 병렬로 실행 174 | - 각 스텝이 연관이 없다면 병렬로 실행하는게 효율적 175 | 3. 비동기 `ItemProcessor`/`ItemWriter` 구성 176 | - `AsynchronousItemProcessor` , `AsynchronousItemWriter` 등을 사용할 수 있다 177 | 4. 원격 청킹 178 | - 메시지 브로커등을 통해 여러 JVM에서 처리를 분산할 수 있다 179 | - 단 네트워크 사용량이 매우 많아질 수 있다 180 | 5. 파티셔닝 181 | - 원격 파티셔닝과 로컬 파티셔닝을 모두 지원한다 182 | 183 | ### Hello world! 184 | 185 | ```java 186 | @EnableBatchProcessing //배치 인프라스터럭쳐를 부트스트랩하는데 사용됨 187 | @SpringBootApplication 188 | public class BatchApplication { 189 | 190 | @Autowired 191 | private JobBuilderFactory jobBuilderFactory; 192 | @Autowired 193 | private StepBuilderFactory stepBuilderFactory; 194 | 195 | @Bean 196 | public Step step() { 197 | return stepBuilderFactory.get("step") 198 | .tasklet(new Tasklet() { 199 | @Override 200 | public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) 201 | throws Exception { 202 | System.out.println("Hello, World!"); 203 | return RepeatStatus.FINISHED; //tasklet이 완료됐음을 스프링배치에게 알리는 역할 204 | } 205 | }).build(); 206 | } 207 | 208 | @Bean 209 | public Job job() { 210 | return jobBuilderFactory.get("job") 211 | .start(step()) 212 | .build(); 213 | } 214 | 215 | public static void main(String[] args) { 216 | SpringApplication.run(BatchApplication.class, args); 217 | } 218 | } 219 | ``` 220 | 221 | - 실행하면 "Hello, World!"가 출력되고 종료된다. 222 | - 실제로 일어난 일 223 | 1. `@SpringBootApplication`이 스프링부트를 부트스트랩한다 224 | 2. `ApplicationContext` 생성됨 225 | 3. 스프링배치가 클래스 경로에 있어서` JobLauncherCommandLineRunner`가 실행됨 226 | 4. 잡이 수행되어 첫번째 스텝이 실행됨(이때 트랜잭션이 시작됨) 227 | 5. `Tasklet`이 실행됨 228 | 6. 결과가 `JobRepository`에 갱신됨 -------------------------------------------------------------------------------- /02주차/발표자료/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /02주차/요약본/[2주차]_1,2장_스프링배치_김민석.md: -------------------------------------------------------------------------------- 1 | # 1장 배치와 스프링 2 | 3 | 4 | - 배치의 장점 5 | - 실제 처리가 시작되기 전에 필요한 정보를 미리 수집할 수 있다. 6 | - 사업적인 도움. 실제 처리가 시작되기 전에 최소 할 수 있는 시간적 여유를 줄 수 있다. 7 | - 자원의 효울적 사용 8 | 9 | 10 | - 배치가 직면한 과제 11 | - 품질특성 관련한 문제 12 | - 품질특성 13 | - ![image](https://user-images.githubusercontent.com/6725753/130757747-9e8e0d6f-c5c2-4077-8d70-ac82588f16f0.png) 14 | - 사용성/ 유지보수성/ 확장성 15 | 16 | 17 | - 자바(스프링)를 사용하는 이유 18 | - 유지보수성 19 | - 유연성 20 | - 확장성 21 | - 개발 리소스 22 | - 지원 23 | - 비용 24 | 25 | - 스프링 배치의 기타 사용 사례 26 | - ETL 27 | - 데이터 마이그레이션 28 | - 병렬처리 29 | - 무중단/상시 데이터 처리 30 | 31 | - 스프링 배치 프레임워크 32 | - Site 33 | - https://spring.io/projects/spring-batch 34 | - Sample 35 | - https://github.com/spring-projects/spring-batch/tree/main/spring-batch-samples 36 | - 구조 37 | - ![](https://docs.spring.io/spring-batch/docs/4.3.x/reference/html/images/spring-batch-layers.png) 38 | - Job 관리 39 | - 로컬/원격 병렬화 40 | - I/O 표준화하기 41 | - 에코시스템 42 | - 스프링 부트 43 | - 스프링 클라우드 태스크 44 | - 스프링 클라우드 데이터 플로우 45 | - 스프링 모든 기능 46 | 47 | 48 | 49 | # 2장 스프링 배치 50 | 51 | 52 | ## 배치 아키텍처 53 | - Job과 Step 54 | - Job 55 | - 상태를 수집하고 이전 상태에서 다음 상태로 전환. 56 | - State Machine 57 | - Step으로 구성 58 | - Step 59 | - Tasklet 60 | - 심플 61 | - 초기화/ 프로시저 실행/ 알림전송 같은 작업에서 사용 62 | - Chunk 63 | - 상대적으로 좀 더 복잡 64 | - ItemReader/ ItemProcessor/ ItemWriter 으로 구성 가능 65 | - 패키지 구성 66 | - ![image](https://user-images.githubusercontent.com/6725753/130740658-60dbc1f0-2251-4b8a-8598-9f8fa4cc1fb2.png) 67 | - ![image](https://user-images.githubusercontent.com/6725753/130754383-6e4eb525-f128-4ace-b795-fc0809dffa56.png) 68 | - 스텝은 각기 독립적으로 실행 69 | - 유연성/ 유지보수성/ 확장성/ 신뢰성 확보 70 | 71 | - Job 실행 72 | - JobRepository 73 | - 배치 수행과 관련된 수치 데이터 및 잡의 상태 관리 74 | - RDB 사용 75 | - ![image](https://user-images.githubusercontent.com/6725753/130741289-455685ad-b6be-4578-af1b-f8a3758471c2.png) 76 | - JobLauncher 77 | - 잡 실행 담당 78 | - Job -> JobInstance -> JobExecution 79 | - ![image](https://user-images.githubusercontent.com/6725753/130741634-39201b3b-3326-4823-915a-6718868f7694.png) 80 | 81 | 82 | ## 병렬화 83 | 84 | - 다중 스레드 스텝 85 | - 스텝 내부의 여러 청크를 각각 스레드로 실행 86 | - 병렬 스텝 87 | - 독립적인 스텝을 병렬적으로 실행 88 | - 비동기 ItemProcessor/ItemWriter 89 | - Process와 write를 Asynch하게 실행. 결과는 Future로 반환. 90 | - 원격청킹 91 | - 복수 JVM 사용 92 | - 마스터 노드에서 메세지 큐를 사용하여 원격 워커 ItemProcessor로 전송 93 | - 파티셔닝 94 | - 원격청킹과 비슷하나 마스터는 워커의 스텝 수집을 위한 컨트롤러 역할만 수행 95 | - 각 워커의 스텝은 독립적으로 실행되나 마치 로컬에서 동작하는 것처럼 보임. 96 | 97 | 98 | ## Example - HelloWorld 99 | 100 | ```java 101 | @EnableBatchProcessing 102 | @SpringBootApplication 103 | public class HelloWorldApplication { 104 | 105 | @Autowired 106 | private JobBuilderFactory jobBuilderFactory; 107 | 108 | @Autowired 109 | private StepBuilderFactory stepBuilderFactory; 110 | 111 | @Bean 112 | public Step step() { 113 | return this.stepBuilderFactory.get("step1") 114 | .tasklet(new Tasklet() { 115 | @Override 116 | public RepeatStatus execute(StepContribution contribution, 117 | ChunkContext chunkContext) { 118 | System.out.println("Hello, World!"); 119 | return RepeatStatus.FINISHED; 120 | } 121 | }).build(); 122 | } 123 | 124 | @Bean 125 | public Job job() { 126 | return this.jobBuilderFactory.get("job") 127 | .start(step()) 128 | .build(); 129 | } 130 | 131 | public static void main(String[] args) { 132 | SpringApplication.run(HelloWorldApplication.class, args); 133 | } 134 | } 135 | 136 | ``` 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /02주차/요약본/[2주차]_1,2장_스프링배치_이호빈.md: -------------------------------------------------------------------------------- 1 | # 1장 배치와 스프링 2 | 3 | > 배치 처리는 상호작용이나 중단 없이 유한한 양의 데이터를 처리하는 것으로 정의한다 4 | 5 | ### 배치를 작성할 때 중요하게 봐야할 점 6 | 7 | - 쉽게 확장해서 새로운 기능을 추가할 수 있는가? 8 | - 기존 코드를 변경했을 때 시스템에 미치는 영향을 알기 위해 테스트가 잘 작성되어있나? 9 | - Job이 실패할 때, 언제 어디서 왜 실패했는지 알 수 있는가? 10 | - 원하는 시간에 다른 시스템에 영향을 끼치지 않으면서 배치 처리를 할 수 있는가? 11 | - 데이터를 안전하게 저장할 수 있는가? 12 | 13 | ### 배치 개발을 자바로 하는 이유 14 | 15 | - 테스트하기 용이하고 추상화같은 이점을 얻을 수 있는 스프링 프레임워크 사용 가능 16 | - 어디서든 실행 가능한 JVM의 유연성 17 | - 커뮤니티도 빵빵함 18 | 19 | ### 스프링 배치 사용 사례 20 | 21 | - 청크 기반으로 추출, 변환, 적재 처리 22 | - 데이터 마이그레이션 23 | - 병렬 처리 → 멀티 코어, 멀티 서버에 처리를 분산하는 기능을 제공함 24 | 25 | ### 스프링 배치 구조 26 | 27 | > 애플리케이션 레이어 > 코어, 인프라스트럭처 레이어 28 | 29 | ![스크린샷 2021-08-24 오후 9 50 15](https://user-images.githubusercontent.com/22311557/130619214-5e57f88e-13a1-4a61-bb48-9b2062275b65.png) 30 | 31 | 32 | - 애플리케이션 레이어 33 | - 코어 레이어 34 | - 인프라스트럭처 레이어 35 | - 여기에 ItemReader, ItemWriter를 비롯해 재시작과 관련된 문제를 해결할 수 있는 클래스와 인터페이스를 제공한다. 36 | 37 | # 2장 스프링 배치 38 | 39 | ### 스프링 배치 40 | 41 | - SynchonousItemProcessor를 ItemProcessor가 호출될 때마다 동일한 스레드에서 실행되게 할 수 있다 42 | - `@EnableBatchProcessing` 애너테이션은 배치 인프라를 위한 대부분의 스프링 빈 정의를 제공한다. 43 | - JobRepository, JobLauncher 등등... 44 | - 안에 @ComponentScan과 @EnableAutoConfiguration이 결합되어 있다 45 | - 즉, Job과 Step을 만들기 위해 스프링 배치가 제공하는 두 개의 빌더(JobBuilderFactory, StepBuilderFactory)만 Autowired하면 된다 46 | - 스프링 배치는 여러 빌더 클래스를 제공한다 47 | 48 | ### Step 49 | 50 | - Step은 독립된 작업의 단위 51 | - Step에는 Tasklet과 Chunk 기반 스텝이 있음 52 | - Tasklet Step은 보통 초기화, 저장 프로시저 실행, 알림 전송 등과 같은 Job에서 사용된다 53 | - Step도 Bean이라서 재사용할 수 있다 54 | - 하나의 스텝 내에서 처리할 일을 여러 스레드에 나눠서 병렬로 처리할 수 있다 55 | - retry하거나 skip을 할 수 있다 56 | - JobRepository는 RDBMS를 사용하며 JobLauncher, Job, Step과 공유한다. 57 | - JobLauncher는 Job을 실행하는 역할 58 | - Job.execute 호출 59 | - Job의 실행 방법 60 | - Job에 쓰이는 파라미터 유효성 검증 등 61 | - Step이 실행되면 JobRepository는 실행된 Step, 현재 상태, 읽은 아이템 등 모두 JobRepository에 저장한다. 62 | - Step이 각 Chunk를 처리할 때마다 JobRepository 내 StepExecution의 스텝 상태가 업데이트 된다 63 | - Step들이 서로 관련이 없다면 병렬로 Step을 실행시킬 수도 있다 64 | 65 | ### Job 66 | 67 | - Job이 다른 파라미터로 실행될 때마다 새로운 JobInstance가 생성된다. 68 | - 만약 파라미터가 같다면 새로운 JobExecution만 얻고 JobInstance는 새로 얻지 않는다 69 | - 그래서 하나의 JobInstance는 여러 개의 JobExecution을 얻을 수 있다. 70 | - Job → JobInstance → JobExecution 71 | - 각각 1:N 관계 72 | - Chunk마다 트랜잭션 단위가 되고 스레드 단위가 돼서 병렬로 처리하면 빠르게 처리할 수 있다 73 | -------------------------------------------------------------------------------- /02주차/요약본/[2주차]_1,2장_스프링배치_함호식.md: -------------------------------------------------------------------------------- 1 | 1.배치와 스프링 2 | -- 3 | 4 | ##### 배치 정의 5 | 상호작용이나 중단 없이 유한한 양의 데이터를 처리하는 것. 6 | 배치처리가 일단 시작되면, 아무런 개입 없이 어떤 형태로든 완료된다. 7 | 8 | ##### 배치 처리의 장점 9 | 1. 실제 처리가 시작되기 전에 필요한 정보를 미리 수집할 수 있다. 10 | ex) 월말 거래 내역 조회 11 | 2. 자원의 효율적 사용 12 | * 일반적인 데이터 모델 처리 13 | 1. 모델의 생성 14 | -> 배치 처리로 생성 15 | 2. 생성된 모델을 놓고 새로운 데이터 평가 16 | -> 생성된 모델로 실시간 사용 17 | 18 | ##### 배치 특징 19 | * 사용자의 개입이 없어, 일반 애플리케이션이 가지는 문제가 대체적으로 발생하지 않음 20 | * 사용량 급증과 사용자 중심의 에러 처리 문제는 배치함 처리시 예측이 가능함 21 | -> 따라서 확실한 로그와 피드백용 알림을 사용해 신속, 정확하게 에러를 발생시킨다. 22 | 23 | --- 24 | 25 | ##### 소프트웨어 아키텍처의 공통적인 속성 26 | 1. 사용성 27 | 2. 유지 보수성 28 | 3. 확장성 29 | 30 | ##### 배치 아키텍처의 속성 31 | 1. 사용성(오류 처리 및 유지 보수성과 관련) 32 | * 단위테스트가 잘 작성되었는지 33 | * 왜 실패했는지 알 수 있는지 34 | 2. 확장성 35 | * worker를 여러개 두어 동시 실행?과 같이 큰 규모의 데이터를 다룰 수 있는 것 36 | 3. 가용성 37 | * 필요할 때 배치를 실행할 수 있는지 38 | * 허용된 시간내에 잡을 수행함으로써 다른 시스템에 영향을 끼치지 않는지 39 | 40 | ###### java를 사용하는 이유 41 | * 유지보수성 42 | * 배치 특성상 코드의 수명이 길다. 43 | -> 큰 위험 없이 쉽게 수정할 수 있어야한다. 44 | * 스프링 프레임워크의 이점을 얻을 수 있다. 45 | * 유연성 46 | * 배포를 위한 JVM의 유연성(OS) 47 | * 개발 리소스 48 | * 확장성 49 | * 비용 50 | 51 | --- 52 | ##### 스프링 배치 일반적인 사용 53 | 1. ETL(추출-Extract, 변환-Transform, 적재-Load) 54 | 2. 데이터 마이그레이션 55 | * 테스트가 가능하다. 56 | * 커밋 횟수 측정, 롤백과 같은 기능 제공 57 | 3. 병렬 처리 58 | * 단일 작업을 빨리하는 것보다, 많은 작업을 병렬로 처리하는 것이 성능의 향상의 지름길이다. 59 | 60 | --- 61 | ##### 스프링 배치 프레임워크 62 | 레이어 구조(3 tier) 63 | * 애플리케이션 레이어 64 | -> 배치 처리 구축의 코드 및 구성 65 | (코어와 인프라스트럭쳐를 감싸고 있음) 66 | * 코어 67 | * 잡 - 중단이나 상호작용 없이 처음부터 끝까지 실행되는 처리 68 | * 스텝 - 잡과 관련된 입력과 출력이 있을 수 있음 69 | +) 잡(1) : 스텝(1..N) 70 | * 인프라스트럭처 71 | -> 데이터를 읽고 씀 72 | -> 잡 수행 실패 후 재시도시 어떤일을 수행할지 선택 73 | 74 | 스프링 기반으로 구축 75 | -> 의존성 주입, AOP, 트랜잭션 관리등의 필요한 기능을 사용 가능 76 | 77 | 78 | 2.스프링 배치 79 | -- 80 | 81 | ##### 잡과 스텝 82 | 잡 - (스텝1 -> 스텝2 -> 스텝3) 83 | * 스텝 : 잡을 구성하는 독립된 작업 단위 84 | * tasklet 기반 85 | -> 스텝이 중지될 때까지 execute 메서드가 반복적 수행 86 | (execute마다 독립된 트랜잭션을 가짐) 87 | * chunk 기반 88 | -> 아이템 기반 처리에 사용 89 | * ItemReader 90 | * ItemProcessor (필수 아님) 91 | * ItemWriter 92 | 93 | 스텝을 분리함으로써 얻는 이점 94 | * 유연성 95 | * 유지 보수성 96 | -> 각 스텝의 코드는 독립적이므로 다른 스텝에 영향을 끼치지 않음 97 | * 확장성 98 | -> 각 스텝을 병렬로 실행 가능 99 | * 신뢰성 100 | -> 스텝의 여러 단계에 적용할 수 있는 강력한 오류 처리 방법 제공 101 | (재시도, 스킵) 102 | 103 | 104 | ##### 잡 실행 105 | 106 | * JobRepository 107 | -> 다양한 배치 수행과 관련된 수치 데이터와 잡의 상태를 유지 관리한다. 108 | (주로 RDB를 사용하여 관련 정보(배치의 시작&종료 시간, 상태, 읽기&쓰기 횟수)를 공유한다.) 109 | ![image](https://godekdls.github.io/images/springbatch/batch-stereotypes.png) 110 | 111 | * JobLauncher 112 | -> 잡을 실행하는 역할을 담당 113 | -> Job.execute를 호출, 재실행가능 여부, 잡의 실행 방법, 유효성 검증 등을 수행 114 | * JobInstance 115 | -> 잡의 이름과 잡의 논리적 실행을 위헤 제공되는 고유한 파라미터 식별 모음 116 | (잡이 다른 파라미터로 실행될 때마다 새로운 JobInstance 생성) 117 | * JobExecution 118 | -> 스프링 배치잡의 실제 실행을 의미 119 | -> 잡을 구동할 때마다 매번 새로운 JobExecution을 얻게됨 120 | -> 실패한 잡을 재실행시 새로운 JobInstance를 얻지 못하고 새로운 JobExecution이 생성됨 121 | * StepExecution스텝 122 | -> 실제 실행을 의미함 123 | -> 일반적으로 JobExecution은 여러 개의 StepExecution과 관계가 있음 124 | 125 | ##### 병렬화 126 | 1. 다중 스레드 스텝을 통한 작업분할 127 | -> 청크라는 블록단위로 처리되도록 구성(각각 독립적인 트랜잭션) 128 | -> 전체 레코드가 1,000개이고, 커밋단위를 100개로 설정했다면 2개의 스레드로 처리할 경우 이론상 2배의 처리량을 갖는다. 129 | 2. 전체 스텝의 병렬 실행 130 | -> 각 스텝이 영향을 끼치지 않는다면, 기다릴 필요 없이 병렬로 스텝을 처리할 수 있다. 131 | 3. 비동기 ItemProcessor, ItemWriter 구성 132 | -> 스텝 내의 ItemProcessor가 병목현상을 일으킬 수 있다. 현재 청크 내에서 Future를 반환하며 비동기 처리를 할 수 있다. 133 | 4. 원격 청킹 134 | -> 메시지 브로커(Rabbit MQ, Active MQ)를 통해 원격 청킹이 가능하다. 135 | (마스터에서 데이터를 읽고 원격 워커에 처리 후 다시 마스터로 전송하므로 네트워크 사용량이 많아질 수 있음) 136 | 5. 파티셔닝 137 | -> 마스터는 워커의 스텝 수집을 위한 컨트롤러 역할만 함 138 | -> 모든 워커의 스텝은 독립적으로 동작 139 | 140 | ##### annotation 141 | * EnableBatchProcessing 142 | -> 배치 인프라스트럭처를 위한 대부분의 스프링 빈 정의를 제공 143 | * JobRepository 144 | * JobLauncher 145 | * JobExplorer 146 | * PlatformTransactionManager 147 | * JobBuilderFactory 148 | * StepBuilderFactory -------------------------------------------------------------------------------- /02주차/요약본/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /03주차/발표자료/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /03주차/요약본/[3주차]_4장_잡과스텝이해하기-1_김광훈.md: -------------------------------------------------------------------------------- 1 | # 4장. 잡과 스텝 이해하기 2 | ## 1. Job 3 | #### 잡 러너 4 | - CommandLineRunner: 스크립트 혹은 명령행에서 직접 실행 5 | - JobRegistryBackgroundJobRunner: 부트스트랩해서 기동한 자바 프로세스 내에서 쿼츠나 JMX 후크 같은 스케줄러를 실행 --> JobRegistry 생성 6 | 7 | #### 배치스키마 8 | - 데이터베이스에 배치 스키마가 존재하지 않는다면 자동 생성 9 | 10 | #### 잡 파라미터 11 | - 잡 이름 및 잡에 전달된 식별 파라미터로 식별 12 | - 동일한 식별 파라미터를 사용해 동일한 잡을 두 번 이상 실행 못 함 13 | 14 | #### 잡 파라미터 주의사항 15 | - 잡 파라미터는 스프링 부트의 명령행 기능을 사용해 프로퍼티를 구성하는 것과 다르다 16 | - 따라서 -- 접두사를 사용해 잡 파라미터를 전달하면 안 된다. 17 | - -D 인자도 사용하면 안된다. 18 | - Late binding 못쓰게 됨 19 | 20 | #### 예제 1 21 | ```java 22 | @EnableBatchProcessing // 배치 인프라스트럭처 제공 23 | @SpringBootApplication 24 | public class HelloWorldJob { 25 | 26 | @Autowired 27 | private JobBuilderFactory jobBuilderFactory; // JobBuilder 생성 28 | 29 | @Autowired 30 | private StepBuilderFactory stepBuilderFactory; // StepBuilder 생성 31 | 32 | 33 | // 잡 파라미터 validate 로직 34 | @Bean 35 | public CompositeJobParametersValidator validator() { 36 | CompositeJobParametersValidator validator = 37 | new CompositeJobParametersValidator(); 38 | 39 | DefaultJobParametersValidator defaultJobParametersValidator = 40 | new DefaultJobParametersValidator( 41 | new String[] {"fileName"}, 42 | new String[] {"name", "currentDate"}); 43 | 44 | defaultJobParametersValidator.afterPropertiesSet(); 45 | 46 | validator.setValidators( 47 | Arrays.asList(new ParameterValidator(), 48 | defaultJobParametersValidator)); 49 | 50 | return validator; 51 | } 52 | 53 | // Tasklet 구현체 람다로 전달 54 | @Bean 55 | public Step step1() { 56 | return this.stepBuilderFactory.get("step1") 57 | .tasklet(helloWorldTasklet(null, null)) 58 | .build(); 59 | } 60 | 61 | @StepScope 62 | @Bean 63 | public Tasklet helloWorldTasklet( 64 | @Value("#{jobParameters['name']}") String name, // SPEL 을 사용해 잡 파라미터 전달 65 | @Value("#{jobParameters['fileName']}") String fileName) { 66 | 67 | return (contribution, chunkContext) -> { 68 | 69 | System.out.println(String.format("Hello, %s!", name)); 70 | System.out.println(String.format("fileName = %s", fileName)); 71 | 72 | return RepeatStatus.FINISHED; 73 | }; 74 | } 75 | 76 | } 77 | ``` 78 | 79 | #### 예제 2 80 | ```java 81 | public class JobLoggerListener { 82 | 83 | private static String START_MESSAGE = "%s is beginning execution"; 84 | private static String END_MESSAGE = "%s has completed with the status %s"; 85 | 86 | @BeforeJob // Job 실헹 전에 실행 87 | public void beforeJob(JobExecution jobExecution) { 88 | System.out.println(String.format(START_MESSAGE, 89 | jobExecution.getJobInstance().getJobName())); 90 | } 91 | 92 | @AfterJob // Job 실행 후에 실행 93 | public void afterJob(JobExecution jobExecution) { 94 | System.out.println(String.format(END_MESSAGE, 95 | jobExecution.getJobInstance().getJobName(), 96 | jobExecution.getStatus())); 97 | } 98 | } 99 | ``` -------------------------------------------------------------------------------- /03주차/요약본/[3주차]_4장_잡과스텝이해하기-1_이호빈.md: -------------------------------------------------------------------------------- 1 | # 4장 잡과 스텝 이해하기 2 | 3 | ## 잡 4 | 5 | - Bean 구성방식과 동일해서 재사용이 가능하며 여러 번 실행할 수 있다 6 | - 순서가 있는 여러 스텝을 가지고 있다 7 | - 외부 의존성 없이 실행할 수 있는 일련의 스텝이다 8 | 9 | ### 잡의 실행 10 | 11 | - JobRunner에서 잡의 실행이 이뤄진다 12 | - CommandLineJobRunner 13 | - 스프링 배치가 제공함 14 | - 스크립트를 이용하거나 명령행에서 직접 전달할 때 15 | - JobRegistryBackgroundJobRunner 16 | - 스프링 배치가 제공함 17 | - Quartz 같은 스케줄러를 사용해 잡을 실행할 때 (JobRegistry를 생성함) 18 | - JobLauncherCommandLineRunner 19 | - 스프링부트는 별도의 구성이 없을 때 얘를 실행시킴 20 | - 잡을 실행할 때 TaskExecutor 인터페이스를 사용한다 21 | - SyncTaskExecutor를 사용하면 JobLauncher와 동일한 스레드에서 실행된다 22 | - JobInstance는 Job이름과 실행 시에 사용되는 식별 파라미터로 사용된다. 23 | - JobInstance는 한 번 **성공적으로 완료되면** 다시 실행시킬 수 없다. 동일한 식별 파라미터를 사용하는 Job은 한 번만 실행할 수 있다 24 | - JobInstance의 상태를 알 수 있는 건 그 상태가 매번 DB에 저장되기 때문 25 | - JobExecution은 Job 실행의 실제 시도. 한 번에 시도하고 한 번에 성공했다면 JobInstance와 JobExecution 둘 다 하나만 존재한다. 또한 얘도 DB에 저장된다 26 | - 인메모리 DB로 JobRepository를 사용하지 않는다 27 | - key=value 형태로 파라미터를 전달한다 28 | - 특정 Job 파라미터가 식별에 사용되지 않게 하려면 -를 붙이자 29 | 30 | 이후, 실습으로 진행했습니다. 31 | 32 | [실습 링크](https://github.com/aegis1920/my-lab/tree/master/def-guide-spring-batch) 33 | -------------------------------------------------------------------------------- /03주차/요약본/[3주차]_4장_잡과스텝이해하기-1_황준호.md: -------------------------------------------------------------------------------- 1 | ## 4장. 잡과 스텝 이해하기 2 | 3 | ### 잡은... 4 | 5 | - 유일하다 6 | - 잡을 여러번 실행하려고 동일한 잡을 여러번 정의할 필요가 없다 7 | - 순서를 가진 여러 스텝의 목록이다 8 | - 잡에서 스텝의 순서는 중요하다 9 | - 모든 스텝을 논리적인 순서대로 실행할 수 있다 10 | - 처음부터 끝까지 실행 가능하다 11 | - 외부 의존성 없이 실행할 수 있다 12 | - 독립적이다 13 | - 의존성을 관리할 수 있어야 한다 14 | 15 | ### 잡의 생명주기 16 | 17 | - 잡은 생명주기대로 실행된다. 18 | - 잡의 실행은 잡 러너에서 시작된다. 19 | - 잡 러너 : 잡의 이름과 여러 파라미터를 받아들여 잡을 실행시키는 역할. 스프링배치는 두가지를 제공한다 20 | 1. `CommandLineJobRunner` 21 | - 스크립트를 이용하거나 명령행에서 직접 잡을 실행할 때 사용 22 | - 스프링을 부트스트랩하고, 전달받은 파라미터를 사용해 요청된 잡을 실행한다 23 | 2. `JobRegistryBackgroundJobRunner` 24 | - `JobRegistry`를 생성하는데 사용 25 | - `JobRegistry`란? 스케줄러를 사용해 잡을 실행한다면 생성되는것. 스프링이 부트스트랩될 때 실행 가능한 잡을 가지고 있음 26 | - 이와 별개로 스프링부트가 제공하는 `JobLauncherCommandLineRunner`도 있음. 27 | - 별도의 config가 없다면 `ApplicationContext`에 정의된 모든 잡 빈을 실행함 28 | - 이 책에선 이걸 사용 29 | - `JobInstance` 30 | - 잡 이름 + 파라미터로 식별 31 | - `BATCH_JOB_INSTANCE`와 `BATCH_JOB_EXECUTION_PARAMS` 사용 32 | - 성공적으로 완료된 `JobExecution`이 있다면 완료된 것으로 간주됨. 33 | - 한 번 성공하면 다시 실행시킬 수 없음 34 | - `JobExecution` 35 | - 잡 실행의 실제 시도 36 | - 시도할때마다 새로운 `JobExecution` 생성되고 `BATCH_JOB_EXECUTION` 테이블의 레코드로 저장됨 37 | - `JobExecution`이 실행될때의 상태는 `BATCH_JOB_EXECUTION_CONTEXT`에 저장됨 -> 오류 발생 시 그 시점부터 실행 가능 38 | 39 | ### 잡 config 40 | 41 | - `CommandLineJobRunner`에 파라미터 전달하기 : `java -jar demo.jar name=junho` 42 | 43 | - 그럼 파라미터들은 `JobParameters`와 매핑됨 (`Map`의 래퍼) 44 | - 타입 변환기능을 이용하고 싶으면 타입도 같이 넘겨준다(소문자로) : `java -jar demo.jar executionDate(date)=2020/12/27` 45 | - 전달한 파라미터는 `BATCH_JOB_EXECUTION_PARAMS` 에 저장됨 46 | - 식별에 사용하고 싶지 않는 파라미터는 `-`를 붙이면 됨 : `java -jar demo.jar executionDate(date)=2020/12/27 -name=junho` 47 | 48 | - 잡 파라미터에 접근하는 방법 49 | 50 | 1. `chunkContext` 51 | 52 | ```java 53 | @Bean 54 | public Tasklet helloWorldTasklet() { 55 | return ((contribution, chunkContext) -> { 56 | String name = (String) chunkContext.getStepContext() 57 | .getJobParameters() 58 | .get("name"); 59 | System.out.println(String.format("Hello, %s!", name)); 60 | return RepeatStatus.FINISHED; 61 | }); 62 | } 63 | ``` 64 | 65 | - `StepContribution contribution` : 아직 커밋되지 않은 현재 트랜잭션에 대한 정보(쓰기수, 읽기수 등) 66 | - `ChunkContext chunkContext` : 실행 시점의 잡 상태를 제공. 테스크릿 내에서는 처리중인 청크와 관련된 정보도 갖고있음 67 | 68 | 2. 늦은 바인딩 69 | 70 | ```java 71 | @StepScope 72 | @Bean 73 | public Tasklet helloWorldTasklet(@Value("#{jobParameters['name']}") String name) { 74 | return ((contribution, chunkContext) -> { 75 | System.out.println(String.format("Hello, %s!", name)); 76 | return RepeatStatus.FINISHED; 77 | }); 78 | } 79 | ``` 80 | 81 | - 스코프 기능을 사용하면 늦은 바인딩을 쉽게 사용할 수 있다 82 | - 스텝 스코프, 잡 스코프 : 스텝, 잡의 실행범위에 들어갈 때까지 빈 생성을 지연시키는 기능 83 | 84 | - 스프링 배치의 파라미터 특화 기능 85 | 86 | 1. 파라미터 유효성 검증 기능 87 | 88 | - `JobParametersValidator` 인터페이스를 구현하고 잡 config에 넣어주면 됨 89 | - 필수 파라미터와 옵션 파라미터에 대한 검증만 하고 싶으면? `DefaultJobParametersValidator` 90 | - 여러 검증 구현체를 적용하고 싶으면? `CompositeJobParametersValidator` 91 | 92 | 2. 파라미터 증가 기능 93 | 94 | ```java 95 | @Bean 96 | public Job job(){ 97 | return jobBuilderFactory.get("basicJob") 98 | .start(step1()) 99 | .validator(validator()) //CompositeJobParametersValidator 100 | .incrementer(new RunIdIncrementer()) //JobParametersIncrementer 101 | .build(); 102 | } 103 | ``` 104 | 105 | - `JobParametersIncrementer` : 파라미터를 고유하게 생성할 수 있도록 해줌 106 | - `RunIdIncrementer`를 추가하면 validator에 `run.id` 를 옵션 파라미터에 추가해야 한다 107 | - 날짜를 증가시키는 것 같이 커스텀하게 만들고 싶다면 `JobParametersIncrementer` 을 구현하고 잡 config에 넣고 옵션 파라미터에 추가하면 된다 108 | 109 | - 잡 리스너 110 | 111 | - 잡 리스너로 잡의 생명주기의 여러 시점에 로직을 추가할 수 있다 112 | 113 | - 잡 리스너 작성 방법 114 | 115 | 1. `JobExecutionListener` 인터페이스 구현 116 | 117 | - `beforeJob`과 `afterJob` 메서드가 있다 : 잡 실행 전 초기화, 잡 실행 후 정리, 알림 등등에 이용한다 118 | 119 | - 구현 후 Job config에 넣음 120 | 121 | ```java 122 | @Bean 123 | public Job job() { 124 | return jobBuilderFactory.get("basicJob") 125 | .start(step1()) 126 | .validator(validator()) 127 | .incrementer(new RunIdIncrementer()) 128 | .listener(new JobLoggerListener()) // <-- 129 | .build(); 130 | } 131 | ``` 132 | 133 | 2. `@BeforeJob`, `@AfterJob` 사용 134 | 135 | - `JobExecutionListener` 을 implements할 필요가 없다 136 | 137 | - config가 살짝 다르다 138 | 139 | ```java 140 | @Bean 141 | public Job job() { 142 | return jobBuilderFactory.get("basicJob") 143 | .start(step1()) 144 | .validator(validator()) 145 | .incrementer(new RunIdIncrementer()) 146 | .listener(JobListenerFactoryBean.getListener(new JobLoggerListener())) // <-- 147 | .build(); 148 | } 149 | ``` 150 | 151 | - 리스너는 잡 리스너 외에도 스텝, 리더, 라이터 등에도 있다 152 | 153 | - `ExecutionContext` 154 | 155 | - 잡 상태는 `JobExecution`의 `ExecutionContext`에 저장된다 156 | 157 | - `ExecutionContext`은 기본적으로 잡의 세션이다 158 | 159 | - `ExecutionContext`에 담겨있는 모든 게 `JobRepository`에 저장된다 160 | 161 | - 조작하는 방법 162 | 163 | 1. `JobExecution` 또는 `StepExecution`으로부터 가져오기 164 | 165 | ```java 166 | public class HelloWorldTasklet implements Tasklet { 167 | 168 | @Override 169 | public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { 170 | ExecutionContext jobContext = chunkContext.getStepContext() 171 | .getStepExecution() //getStepExecutionContext()는 변경사항이 반영되지 않음 172 | .getJobExecution() 173 | .getExecutionContext(); 174 | jobContext.put(...); // <-- 조작 175 | 176 | } 177 | } 178 | ``` 179 | 180 | 2. `StepExecution`의 `ExecutionContext`에 있는 키를 `JobExecution`의 `ExecutionContext`로 승격하기 181 | 182 | - ? 183 | 184 | 3. `ItemStream` 인터페이스 사용 185 | 186 | - 추후 다룸 187 | 188 | - 저장하는 법 189 | 190 | - 잡이 처리되는 동안 각 청크를 커밋하면서 잡과 스텝의 현재 `ExecutionContext`를 데이터베이스에 저장한다 191 | - `BATCH_JOB_EXECUTION_CONTEXT` 테이블 192 | - `JOB_EXECUTION_ID` 컬럼 : 관련된 `JobExecution`의 참조 193 | - `SHORT_CONTEXT` 컬럼 : `ExecutionContext`의 json 표현. 배치 처리가 진행되면서 갱신됨 194 | - `SERIALIZED_CONTEXT` 컬럼 : 직렬화된 자바 객체 -------------------------------------------------------------------------------- /03주차/요약본/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /04주차/발표자료/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /04주차/요약본/[4주차]_4장_잡과스텝이해하기-2_김민석.md: -------------------------------------------------------------------------------- 1 | # 4장 Job과 Step 이해하기 - Step 2 | 3 | 4 | ## Step 알아보기 5 | 6 | - 독립적 7 | - 순차적 8 | - 단위 작업의 조각 9 | - 자체적으로 입력/처리/출력 10 | - 트랜잭션 안에서 수행 11 | - 자유롭게 Job을 구성 12 | 13 | ### 태스크 / 청크 14 | 15 | - 청크 기반 처리 16 | ![](https://res.infoq.com/presentations/Spring-Integration-Batch/en/slides/sl38.jpg) 17 | 18 | ### 스텝 구성 19 | - 상태 머신 패러다임 20 | 21 | #### 태스크릿 스텝 22 | 23 | - MethodIbvokingTaskletAdapter 24 | - POJO를 스텝으로 활용 25 | - Tasklet 인터페이스 26 | ```java 27 | public interface Tasklet { 28 | 29 | /** 30 | * Given the current context in the form of a step contribution, do whatever 31 | * is necessary to process this unit inside a transaction. Implementations 32 | * return {@link RepeatStatus#FINISHED} if finished. If not they return 33 | * {@link RepeatStatus#CONTINUABLE}. On failure throws an exception. 34 | * 35 | * @param contribution mutable state to be passed back to update the current 36 | * step execution 37 | * @param chunkContext attributes shared between invocations but not between 38 | * restarts 39 | * @return an {@link RepeatStatus} indicating whether processing is 40 | * continuable. Returning {@code null} is interpreted as {@link RepeatStatus#FINISHED} 41 | * 42 | * @throws Exception thrown if error occurs during execution. 43 | */ 44 | @Nullable 45 | RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception; 46 | 47 | } 48 | ``` 49 | ### 그 밖의 다른 유형 태스크릿 50 | - CallableTaskletAdapter 51 | - 다른 스레드에서 실행 52 | - 스텝을 병렬적으로 실행하는 것은 아님 53 | ```java 54 | public class CallableTaskletAdapter implements Tasklet, InitializingBean { 55 | 56 | private Callable callable; 57 | 58 | /** 59 | * Public setter for the {@link Callable}. 60 | * @param callable the {@link Callable} to set 61 | */ 62 | public void setCallable(Callable callable) { 63 | this.callable = callable; 64 | } 65 | 66 | /** 67 | * Assert that the callable is set. 68 | * 69 | * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 70 | */ 71 | @Override 72 | public void afterPropertiesSet() throws Exception { 73 | Assert.notNull(callable, "A Callable is required"); 74 | } 75 | 76 | /** 77 | * Execute the provided Callable and return its {@link RepeatStatus}. Ignores 78 | * the {@link StepContribution} and the attributes. 79 | * @see Tasklet#execute(StepContribution, ChunkContext) 80 | */ 81 | @Override 82 | public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { 83 | return callable.call(); 84 | } 85 | 86 | } 87 | ``` 88 | 89 | - MethodinvokingTaskletAdaptor 90 | - 서비스의 bean과 메소드 이름을 넘겨서 기존 서비스를 실행 91 | 92 | - SystemCommandTasklet 93 | - 시스템 명령 실행 94 | 95 | #### 청크 기반 스텝 96 | - 청크 개수 지정 가능 97 | - 청크 개수에 따라 커밋 98 | - 읽기/쓰기가 청크 단위로 이루어짐 99 | 100 | #### 청크 크기 구성하기 101 | - 하드 코딩으로 소스에 정적으로 정의 가능 102 | - CompletionPolicy 인터페이스로 청크 크기 동적으로 변경 가능 103 | - 구현체 104 | - SimpleCompletionPolicy 105 | - TimeoutTerminationPolicy 106 | - CompositeCompletionPolicy 107 | 108 | #### 스텝 리스너 109 | - StepExecutionListener 110 | - ChunkListener 111 | - JobListner과 동일하게 애너테이션으로도 가능 112 | 113 | ### 스텝 플로우 114 | 스텝의 실행 순서를 구성할 수 있다 115 | 116 | #### 조건 로직 117 | - StepBuilder의 next 메서드를 통해 순서 구성 118 | - 조건별 스텝 플로우 참고 119 | - https://jojoldu.tistory.com/328 120 | ```java 121 | @EnableBatchProcessing 122 | @SpringBootApplication 123 | public class ConditionalJob { 124 | 125 | @Autowired 126 | private JobBuilderFactory jobBuilderFactory; 127 | 128 | @Autowired 129 | private StepBuilderFactory stepBuilderFactory; 130 | 131 | @Bean 132 | public Tasklet passTasklet() { 133 | return (contribution, chunkContext) -> { 134 | // return RepeatStatus.FINISHED; 135 | throw new RuntimeException("Causing a failure"); 136 | }; 137 | } 138 | 139 | @Bean 140 | public Tasklet successTasklet() { 141 | return (contribution, context) -> { 142 | System.out.println("Success!"); 143 | return RepeatStatus.FINISHED; 144 | }; 145 | } 146 | 147 | @Bean 148 | public Tasklet failTasklet() { 149 | return (contribution, context) -> { 150 | System.out.println("Failure!"); 151 | return RepeatStatus.FINISHED; 152 | }; 153 | } 154 | 155 | @Bean 156 | public Job job() { 157 | return this.jobBuilderFactory.get("conditionalJob") 158 | .start(firstStep()) 159 | .on("FAILED").stopAndRestart(successStep()) 160 | .from(firstStep()) 161 | .on("*").to(successStep()) 162 | .end() 163 | .build(); 164 | } 165 | 166 | @Bean 167 | public Step firstStep() { 168 | return this.stepBuilderFactory.get("firstStep") 169 | .tasklet(passTasklet()) 170 | .build(); 171 | } 172 | 173 | @Bean 174 | public Step successStep() { 175 | return this.stepBuilderFactory.get("successStep") 176 | .tasklet(successTasklet()) 177 | .build(); 178 | } 179 | 180 | @Bean 181 | public Step failureStep() { 182 | return this.stepBuilderFactory.get("failureStep") 183 | .tasklet(failTasklet()) 184 | .build(); 185 | } 186 | 187 | public static void main(String[] args) { 188 | SpringApplication.run(ConditionalJob.class, args); 189 | } 190 | } 191 | ``` 192 | - 로직으로 스텝 플로우 구성 가능 193 | - JobExecutionDecider 194 | ```java 195 | public class RandomDecider implements JobExecutionDecider { 196 | 197 | private Random random = new Random(); 198 | 199 | public FlowExecutionStatus decide(JobExecution jobExecution, 200 | StepExecution stepExecution) { 201 | 202 | if (random.nextBoolean()) { 203 | return new 204 | FlowExecutionStatus(FlowExecutionStatus.COMPLETED.getName()); 205 | } else { 206 | return new 207 | FlowExecutionStatus(FlowExecutionStatus.FAILED.getName()); 208 | } 209 | } 210 | } 211 | ``` 212 | 213 | #### 잡 종료하기 214 | - Job 종료 상태 215 | - Completed 216 | - 해당 JobInstance는 다시 실행 될 수 없다 217 | - JobBuilder에서 end() 218 | - Failed 219 | - 해당 JobInstance 재실행 가능 220 | - JobBuilder에서 fail() 221 | - Stopped 222 | - 중단점부터 다시 시작 가능 223 | - JobBuilder에서 stopAndRestart(step) 224 | 225 | #### 플로우 외부화하기 226 | - 독자적 플로우 227 | 228 | ```java 229 | @Bean 230 | public Flow preProcessingFlow() { 231 | return new FlowBuilder("preProcessingFlow").start(loadFileStep()) 232 | .next(loadCustomerStep()) 233 | .next(updateStartStep()) 234 | .build(); 235 | } 236 | 237 | @Bean 238 | public Job conditionalStepLogicJob() { 239 | return this.jobBuilderFactory.get("conditionalStepLogicJob") 240 | .start(preProcessingFlow()) 241 | .next(runBatch()) 242 | .build(); 243 | } 244 | ``` 245 | 246 | - 플로우 스텝 247 | ```java 248 | @Bean 249 | public Flow preProcessingFlow() { 250 | return new FlowBuilder("preProcessingFlow").start(loadFileStep()) 251 | .next(loadCustomerStep()) 252 | .next(updateStartStep()) 253 | .build(); 254 | } 255 | 256 | @Bean 257 | public Job conditionalStepLogicJob() { 258 | return this.jobBuilderFactory.get("conditionalStepLogicJob") 259 | .start(intializeBatch()) 260 | .next(runBatch()) 261 | .build(); 262 | } 263 | 264 | @Bean 265 | public Step intializeBatch() { 266 | return this.stepBuilderFactory.get("initalizeBatch") 267 | .flow(preProcessingFlow()) 268 | .build(); 269 | } 270 | ``` 271 | - 잡에서 다른 잡 호출 272 | - 가능하나 복잡도가 올라가기 때문에 사용하지 않는 것이 좋다. 273 | ```java 274 | @Bean 275 | public Job conditionalStepLogicJob() { 276 | return this.jobBuilderFactory.get("conditionalStepLogicJob") 277 | .start(intializeBatch()) 278 | .next(runBatch()) 279 | .build(); 280 | } 281 | 282 | @Bean 283 | public Step intializeBatch() { 284 | return this.stepBuilderFactory.get("initalizeBatch") 285 | .job(preProcessingJob()) 286 | .parametersExtractor(new DefaultJobParametersExtractor()) 287 | .build(); 288 | } 289 | 290 | @Bean 291 | public Job preProcessingJob() { 292 | return this.jobBuilderFactory.get("preProcessingJob") 293 | .start(loadFileStep()) 294 | .next(loadCustomerStep()) 295 | .next(updateStartStep()) 296 | .build(); 297 | } 298 | ``` -------------------------------------------------------------------------------- /04주차/요약본/[4주차]_4장_잡과스텝이해하기-2_김성모.md: -------------------------------------------------------------------------------- 1 | # 4장 잡과 스텝 이해하기 2편(스텝) 2 | 3 | ## 스텝 알아보기 4 | 5 | > 스텝이란? 독립적이고 순차적으로 배치 처리를 수행하는 배치 프로세서 6 | 7 | 목표 : 8 | 1. 스텝의 처리 방법 9 | 2. 스텝의 트랜잭션 처리 방법 10 | 3. 스텝 흐름 제어 방법 11 | 12 | 13 | ### 태스크릿 처리와 청크 처리 비교 14 | 15 | - 태스크릿 16 | 17 | - 청크 18 | 19 | 20 | ### 스탭 구성 21 | 22 | #### 태스크릿 스텝 23 | 24 | 1. MethodInvokingTaskletAdapter 25 | 2. CallableTaskletAdapter 26 | 3. SystemCommandTasklet 27 | 4. Tasklet 인터페이스 구현 28 | 29 | 30 | 31 | 1. MethodInvokingTaskletAdapter 32 | > 메소드를 다른 클래스에서 가져와 메서드 호출할 수 있게 해준다. 33 | 34 | ```java 35 | @Configuration 36 | public class MethodInvokingTaskletConfiguration { 37 | @Autowired 38 | private JobBuilderFactory jobBuilderFactory; 39 | @Autowired 40 | private StepBuilderFactory stepBuilderFactory; 41 | 42 | @Bean 43 | public Job methodInvokingJob() { 44 | return this.jobBuilderFactory.get("methodInvokingJob").start(methodInvokingStep()).build(); 45 | } 46 | 47 | @Bean 48 | public Step methodInvokingStep() { 49 | return this.stepBuilderFactory.get("methodInvokingStep").tasklet(methodInvokingTasklet()).build(); 50 | } 51 | 52 | @Bean 53 | public MethodInvokingTaskletAdapter methodInvokingTasklet() { 54 | MethodInvokingTaskletAdapter methodInvokingTaskletAdapter = new MethodInvokingTaskletAdapter(); 55 | 56 | methodInvokingTaskletAdapter.setTargetObject(service()); 57 | methodInvokingTaskletAdapter.setTargetMethod("serviceMethod"); 58 | 59 | return methodInvokingTaskletAdapter; 60 | } 61 | 62 | @Bean 63 | public CustomService service() { 64 | return new CustomService(); // 이 클래스엔 serviceMethod라는 이름을 갖는 함수가 있다. 65 | } 66 | } 67 | 68 | ``` 69 | 70 | 71 | 2. CallableTaskletAdapter 72 | > 값을 반환하고 체크 예외를 바깥으로 던질 수 있다. 73 | > 74 | > 스텝이 실행되는 스레드와 별개의 스레드에서 실행되지만 병렬 실행은 아니다. 75 | 76 | ``` java 77 | 78 | @Configuration 79 | public class CallableTaskletConfiguration { 80 | @Autowired 81 | private JobBuilderFactory jobBuilderFactory; 82 | @Autowired 83 | private StepBuilderFactory stepBuilderFactory; 84 | 85 | @Bean 86 | public Job callableJob(){ 87 | return this.jobBuilderFactory.get("callableJob").start(callableStep()).build(); 88 | } 89 | 90 | @Bean 91 | public Step callableStep() { 92 | return this.stepBuilderFactory.get("callableStep").tasklet(tasklet()).build(); 93 | } 94 | 95 | @Bean 96 | public Callable callableObject() { 97 | return () -> { 98 | System.out.println("This was executed in another thread"); 99 | return RepeatStatus.FINISHED; 100 | }; 101 | } 102 | 103 | @Bean 104 | public CallableTaskletAdapter tasklet() { 105 | CallableTaskletAdapter callableTaskletAdapter = new CallableTaskletAdapter(); 106 | 107 | callableTaskletAdapter.setCallable(callableObject()); 108 | 109 | return callableTaskletAdapter; 110 | } 111 | } 112 | 113 | 114 | public class CustomService { 115 | 116 | public void serviceMethod() { 117 | System.out.println("Service method was called"); 118 | } 119 | } 120 | 121 | ``` 122 | 123 | 잡 파라미터의 값을 사용하고싶다면 다음의 추가 작업을 진행한다. 124 | 125 | ```java 126 | 127 | @Configuration 128 | public class MethodInvokingTaskletConfiguration { 129 | @Autowired 130 | private JobBuilderFactory jobBuilderFactory; 131 | @Autowired 132 | private StepBuilderFactory stepBuilderFactory; 133 | 134 | @Bean 135 | public Job methodInvokingJob() { 136 | return this.jobBuilderFactory.get("methodInvokingJob").start(methodInvokingStep()).build(); 137 | } 138 | 139 | @Bean 140 | public Step methodInvokingStep() { 141 | return this.stepBuilderFactory.get("methodInvokingStep").tasklet(methodInvokingTasklet(null)).build(); 142 | } 143 | 144 | @StepScope 145 | @Bean 146 | public MethodInvokingTaskletAdapter methodInvokingTasklet( 147 | @Value("#{jobParameters['message']}") String message 148 | ) { 149 | MethodInvokingTaskletAdapter methodInvokingTaskletAdapter = new MethodInvokingTaskletAdapter(); 150 | 151 | methodInvokingTaskletAdapter.setTargetObject(service()); 152 | methodInvokingTaskletAdapter.setTargetMethod("serviceMethod"); 153 | methodInvokingTaskletAdapter.setArguments(new String[]{message}); 154 | 155 | return methodInvokingTaskletAdapter; 156 | } 157 | 158 | @Bean 159 | public CustomService service() { 160 | return new CustomService(); 161 | } 162 | } 163 | 164 | 165 | 166 | public class CustomService { 167 | 168 | public void serviceMethod(String message) { 169 | System.out.println("Service method was called"); 170 | System.out.println(message); 171 | } 172 | } 173 | ``` 174 | 175 | 3. SystemCommandTasklet 176 | 177 | > 시스템 명령을 사용할 때 사용한다. 178 | 179 | ```java 180 | 181 | @Configuration 182 | public class SystemCommandTaskletConfiguration { 183 | @Autowired 184 | private JobBuilderFactory jobBuilderFactory; 185 | @Autowired 186 | private StepBuilderFactory stepBuilderFactory; 187 | 188 | @Bean 189 | public Job systemCommandJob() { 190 | return this.jobBuilderFactory.get("systemCommandJob").start(systemCommandStep()).build(); 191 | } 192 | 193 | @Bean 194 | public Step systemCommandStep() { 195 | return this.stepBuilderFactory.get("systemCommandStep").tasklet(systemCommandTasklet()).build(); 196 | } 197 | 198 | @Bean 199 | public SystemCommandTasklet systemCommandTasklet() { 200 | SystemCommandTasklet systemCommandTasklet = new SystemCommandTasklet(); 201 | systemCommandTasklet.setCommand("rm -rf /tmp.txt"); 202 | systemCommandTasklet.setTimeout(5000); 203 | systemCommandTasklet.setInterruptOnCancel(true); 204 | 205 | return systemCommandTasklet; 206 | } 207 | } 208 | 209 | ``` 210 | 211 | ```java 212 | 213 | 214 | @Configuration 215 | public class AdvancedSystemCommandTaskletConfiguration { 216 | @Autowired 217 | private JobBuilderFactory jobBuilderFactory; 218 | @Autowired 219 | private StepBuilderFactory stepBuilderFactory; 220 | 221 | @Bean 222 | public Job systemCommandJob() { 223 | return this.jobBuilderFactory.get("systemCommandJob").start(systemCommandStep()).build(); 224 | } 225 | 226 | @Bean 227 | public Step systemCommandStep() { 228 | return this.stepBuilderFactory.get("systemCommandStep").tasklet(systemCommandTasklet()).build(); 229 | } 230 | 231 | @Bean 232 | public SystemCommandTasklet systemCommandTasklet() { 233 | SystemCommandTasklet systemCommandTasklet = new SystemCommandTasklet(); 234 | systemCommandTasklet.setCommand("touch tmp.txt"); 235 | systemCommandTasklet.setTimeout(5000); 236 | systemCommandTasklet.setInterruptOnCancel(true); 237 | 238 | systemCommandTasklet.setWorkingDirectory("/Userss/mminella/spring-batch"); 239 | systemCommandTasklet.setTerminationCheckInterval(5000); 240 | systemCommandTasklet.setSystemProcessExitCodeMapper(touchCodeMapper()); 241 | systemCommandTasklet.setTaskExecutor(new SimpleAsyncTaskExecutor()); 242 | systemCommandTasklet.setEnvironmentParams(new String[]{ 243 | "JAVA_HOME=/java", "BATCH_HOME=/Users/batch" 244 | }); 245 | 246 | return systemCommandTasklet; 247 | } 248 | 249 | @Bean 250 | public SimpleSystemProcessExitCodeMapper touchCodeMapper() { 251 | return new SimpleSystemProcessExitCodeMapper(); 252 | } 253 | } 254 | 255 | ``` 256 | 257 | 4. Tasklet 인터페이스 구현 258 | 259 | 260 | ### 청크 기반 스텝 261 | 262 | > 청크는 커밋 간격에 의해 정의된다. 한 번에 몇개의 처리를 한 뒤 기록할 떄 쓴다. 263 | 264 | #### 청크 크기 구성하기 265 | 청크 크기를 구성하는 두 가지 방식 266 | 1. 정적인 커밋 개수 설정 267 | ```java 268 | 269 | @Configuration 270 | public class ChunkJob { 271 | @Autowired 272 | private JobBuilderFactory jobBuilderFactory; 273 | @Autowired 274 | private StepBuilderFactory stepBuilderFactory; 275 | 276 | @Bean 277 | public Job chunkBasedJob() { 278 | return this.jobBuilderFactory.get("chunkBasedJob").start(chunkBasedStep()).build(); 279 | } 280 | 281 | @Bean 282 | public Step chunkBasedStep() { 283 | return this.stepBuilderFactory.get("chunkBasedStep") 284 | .chunk(1000) 285 | .reader(itemReader()) 286 | .writer(itemWriter()) 287 | .build(); 288 | } 289 | 290 | @Bean 291 | public ListItemReader itemReader() { 292 | List items = new ArrayList<>(100000); 293 | 294 | for (int i = 0; i < 100000; i++) { 295 | items.add(UUID.randomUUID().toString()); 296 | } 297 | 298 | return new ListItemReader<>(items); 299 | } 300 | 301 | @Bean 302 | public ItemWriter itemWriter() { 303 | return items -> { 304 | for (String item : items) { 305 | System.out.println(">> current item = " + item); 306 | } 307 | }; 308 | } 309 | } 310 | ``` 311 | 312 | 2. CompletionPolicy 구현체 사용 313 | 314 | 구현체 커스터마이징 시 순서 315 | - start 메서드 316 | - update 메서드 317 | - isComplete 메서드 318 | 319 | 320 | #### 스텝 리스너 321 | 322 | > 각 스텝 시작 전 후로 이벤트 처리 가능 323 | 324 | - StepExecutionListener 325 | - ChunkListener 326 | 327 | 328 | ### 스텝 플로우 329 | 330 | #### 조건 로직 331 | > 스텝을 줄을 세워 실행하는 게 아닌 조건부로 스텝을 나눠서 진행하는 방식 332 | 333 | #### 잡 종료하기 334 | 335 | 1. Completed : 성공적으로 종료, JobInstance도 종료 336 | 2. Failed : 잡이 성공되지 않음, JobInstance 재실행 가능 337 | 3. Stopped : 잡에 오류가 발생하지 않았지만 중단된 위치에서 다시 시작 가능 338 | 339 | 340 | #### 스텝을 플로우화하기 341 | - 플로우 만들기 342 | - 플로우 스텝 만들기 343 | - 잡 스텝만들기 344 | -------------------------------------------------------------------------------- /04주차/요약본/[4주차]_4장_잡과스텝이해하기-2_이호빈.md: -------------------------------------------------------------------------------- 1 | # 4장 잡과 스텝 이해하기 - Step 2 | 3 | - 스텝은 잡의 구성요소로 독립적이고 순차적으로 배치 처리를 수행한다 4 | - 트랜잭션은 스텝 내에서 이뤄진다 5 | - 스프링 배치는 기본적으로 각 Step이 상태로 이뤄지고, 다음 상태로 전이되는 상태 머신임 6 | 7 | ## Tasklet 모델 8 | 9 | - 단일 명령으로 실행하는 경우 10 | - 정의 방법 11 | - 여러 코드를 사용해서 사용자 코드를 Tasklet Step으로 정의할 수 있음 12 | - CallableTaskletAdapter 13 | - MethodInvokigTaskletAdapter 14 | - SystemCommandTasklet 15 | - Tasklet 인터페이스를 구현해서도 가능 16 | - 반환 값은 RepeatStatus.CONTINUABLE or RepeatStatus.FINISHED 17 | - CONTINUEABLE은 어떤 조건이 충족될 때까지 반복해서 해당 Tasklet을 실행시키고 싶을 때 사용 18 | 19 | ### SystemCommandTasklet (실습 X) 20 | 21 | - 시스템 명령을 실행할 때 사용 22 | - `rm -rf tmp.txt` 등... 23 | - 지정한 시스템 명령은 비동기로 실행된다. 24 | - 그래서 timeout 값이 중요하다 25 | - 해당 명령의 완료 여부를 terminateCheckInterval 속성을 통해 주기적으로 확인할 수 있다 26 | - interruptOnCancel 속성으로 Job이 비 정상적으로 종료될 때 시스템 프로세스와 관련된 스레드를 강제로 종료할 지 스프링 배치에게 알려준다. 27 | 28 | ## Chunk 모델 29 | 30 | - ItemReader, ItemProcessor, ItemWriter로 구성됨 31 | - ItemWriter에서 물리적 쓰기를 일괄로 처리한다 32 | - commit interval이라고 불리는 커밋 간격이 중요하다 33 | - 10개라고 설정했을 때 9개의 아이템을 처리하고 오류가 발생하면, 스프링 배치는 현재 Chunk를 롤백하고 잡이 실패했다고 표시한다. 34 | - CompletionPolicy 인터페이스는 청크의 완료 여부를 결정할 수 있는 로직을 구현할 수 있게 해준다. 35 | - 기본적으로 SimpleCompletionPolicy를 사용한다 36 | - 처리된 아이템 개수를 세다가 임계값에 도달하면 청크 완료로 표시한다. 37 | - TimeoutTerminationPolicy도 있는데 얘를 사용하면 청크 내에서 처리 시간이 해당 시간이 넘으면 해당 청크가 완료된 것으로 간주되고 모든 트랜잭션 처리가 정상으로 계속된다 38 | - CompositeCompletionPolicy를 통해 여러 Policy를 함께 구성한다 39 | 40 | ## 그 외 41 | 42 | ### StepExecutionListener, ChunkListener 43 | 44 | - StepExecutionListener, ChunkListener 인터페이스는 각각 스텝과 청크의 시작과 끝에서 특정 로직을 처리할 수 있게 해준다 45 | - afterStep 메서드는 ExitStatus를 반환하는데 Listener가 ExitStatus를 Job에 반환하기 전에 수정할 수 있기 때문 46 | - 얘들로 기본적인 무결성 검사를 할 수 있다 47 | - @BeforeStep, @AfterStep, @BeforeChunk, @AfterChunk 등을 사용할 수 있다 48 | 49 | ### JobExecutionDecider 50 | 51 | - on 메서드는 Step의 ExitStatus를 평가해 조건으로 분기할 수 있다 52 | - JobExecutionDecider 인터페이스를 구현해서 결정해줄 수 있다. 53 | - FlowExecutionStatus는 BatchStatus, ExitStatus 쌍을 래핑한 래퍼 객체다 54 | 55 | ### ExitStatus vs BatchStatus 56 | 57 | - BatchStatus 58 | - Job이나 Step의 현재 상태를 식별하는 JobExecution이나 StepExecution의 속성이다. 59 | - StepExecution이나 JobExecution 내에 보관된다 60 | - ExitStatus 61 | - Job이나 Step 종료 시 스프링 배치로 반환되는 값이다. 이는 문자열이다 62 | - Step, Chunk, Job 에서 반환될 수 있다 63 | - 64 | 65 | ### Job의 3가지 종료 상태 66 | 67 | - Completed 68 | - 성공적으로 종료. 동일한 파라미터로 다시 실행할 수 없다 69 | - builder에서 end()로 끝내버리면 Completed로 종료된다 70 | - Failed 71 | - 실패로 종료. 동일한 파라미터로 다시 실행할 수 있다 72 | - Stopped 73 | - 중단된 위치에서 Job을 다시 시작된다 74 | - 사용자가 미리 구성해둔 스텝부터 시작된다 75 | - 오류가 발생하지 않아도 중단 가능하다. 76 | 77 | ### 여러 Step을 하나로 만드는 방법 : Flow로 만들기 vs Flow Step으로 만들기 vs 외부화하지 않기 78 | 79 | - Flow로 만들기 80 | - JobRepository를 살펴보면 Job에서 Step을 구성하는 것과 차이가 없다. 81 | - 예시 82 | 83 | ```java 84 | @Bean 85 | public Flow preProcessingFlow() { 86 | return new FlowBuilder("preProcessingFlow").start(loadFileStep()) 87 | .step(loadCustomStep()) 88 | .step(updateStartStep()) 89 | .build() 90 | } 91 | ``` 92 | 93 | - Flow Step으로 만들기 94 | - Flow를 Step으로 래핑한다. 95 | - JobRepository를 살펴보면 추가적인 항목이 더해진다 96 | - 해당 플로우가 담긴 스텝을 하나의 스텝처럼 기록한다. 97 | - 플로우의 영향을 전체적으로 볼 수 있다 98 | - 예시 99 | 100 | ```java 101 | @Bean 102 | public Step intializeBatch() { 103 | return this.stepBuilderFactory.get("initalizeBatch") 104 | .flow(preProcessingFlow()) 105 | .build(); 106 | } 107 | ``` 108 | 109 | - 플로우를 작성하지 않고 Job 내에서 다른 Job을 호출하기 110 | - 개별 Job을 만들어 함께 묶으면 실행 처리를 제어하는데 큰 제약이 있을 수 있다. 111 | - Job과 Job을 최대한 연결시키지 말자. 112 | 113 | > [실습 링크](https://github.com/aegis1920/my-lab/tree/master/def-guide-spring-batch) 114 | -------------------------------------------------------------------------------- /04주차/요약본/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /05주차/발표자료/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /05주차/요약본/[5주차]_5장_JobRepository와_메타데이터_김광훈.md: -------------------------------------------------------------------------------- 1 | # 6장 JobRepository 와 메타데이터 2 | ## 1. JobRepository ?? 3 | ### 1.1 기능 4 | #### (1) 스프링 배치는 잡이 실행될 때 잡의 상태를 JobRepository 에 저장한다. 5 | #### (2) 모니터링으로 사용할 수 있다. 6 | 7 | ### 1.2 정의 8 | #### (1) Interface 9 | #### (2) 데이터 저장소 10 | 11 | ### 1.3 RDB 12 | #### (1) BATCH_JOB_INSTANCE 13 | - 스키마의 시작점 14 | - 잡을 식별하는 고유 정보가 포함된 잡 파라미터로 잡을 처음 실행하면 생성 15 | 16 | #### (2) BATCH_JOB_EXECUTION 17 | - 배치 잡의 실제 실행 기록 저장 18 | 19 | #### (3) BATCH_JOB_EXECUTION_PARAMS 20 | - 잡이 매번 실행될 때마다 사용된 잡 파라미터 저장 21 | 22 | #### (4) BATCH_JOB_EXECUTION_CONTEXT 23 | - Execution context 관련 정보 저장 24 | - 잡의 실행 정보 25 | 26 | #### (5) BATCH_STEP_EXECUTION 27 | - 스텝의 시작, 완료, 상태에 대한 메타데이터 저장 28 | 29 | #### (6) BATCH_STEP_EXECUTION_CONTEXT 30 | = 스텝 수준에서 컴포넌틍 상태를 저장하는 데 사용 31 | 32 | ## 2. 배치 인프라스트럭처 구성 33 | ### 2.1 BatchConfigure 인터페이스 34 | #### (1) BatchConfigurer 구현체에서 빈을 생성 35 | #### (2) 그다음 SimpleBatchConfiguration 에서 스프링의 ApplicationContext 에 생성한 빈 등록 36 | 37 | ### 2.2 TransactionManager 38 | #### DefaultBatchConfigurer 가 기본적으로 setDataSource 수정자 내에서 DataSourceTransactionManager 자동 생성 39 | 40 | ### 2.3 JobExplorer 41 | #### JobExplorer 는 JobRepository 가 다루는 데이터와 동일한 데이터를 읽기 전용으로만 보는 뷰 42 | #### 기본적으로 데이터 접근 계층은 JobRepository 와 JobExplorer 간에 공유되는 동일한 공통 DAO 집합이다. 43 | 44 | ### 2.4 JobLauncher 45 | #### JobLauncher 는 스프링 배치 잡을 실행하는 진입점 46 | #### 스프링 부트는 기본적으로 스프링 배치가 제공하는 SimpleJobLauncher 를 사용한다. 47 | -------------------------------------------------------------------------------- /05주차/요약본/[5주차]_5장_JobRepository와_메타데이터_황준호.md: -------------------------------------------------------------------------------- 1 | ## 5장. `JobRepository`와 메타데이터 2 | 3 | ### JobRepository란? 4 | 5 | - 스프링 배치 내에서 `JobRepository`를 말한 땐 둘 중 하나다 6 | 7 | 1. `JobRepository` 인터페이스 8 | 2. `JobRepository` 인터페이스를 구현해 데이터를 저장하는 데 사용되는 데이터 저장소 <- 이 절에선 주로 이걸 가리킨다 9 | 10 | - 배치 잡 내부에서 바로 사용할 수 있는 데이터 저장소 2가지 11 | 12 | 1. 관계형 데이터베이스 13 | 14 | ![Spring Batch Meta-Data ERD](https://docs.spring.io/spring-batch/docs/4.3.x/reference/html/images/meta-data-erd.png) 15 | 16 | - `BATCH_JOB_INSTANCE` : 잡의 논리적 실행 17 | - `BATCH_JOB_EXECUTION` : 잡의 실제 실행 기록 18 | - `BATCH_JOB_EXECUTION_PARAMS` : 잡이 매번 실행될때마다 사용된 잡 파라미터 기록 19 | - `BATCH_JOB_EXECUTION_CONTEXT` : `JobExecution`의 `ExecutionContext` 기록 20 | - `BATCH_STEP_EXECUTION` : 스텝의 시작, 완료, 상태에 대한 메타데이터 기록 21 | - `BATCH_STEP_EXECUTION_CONTEXT` : `StepExecution`의 `ExecutionContext` 기록 22 | 23 | 2. 인메모리 저장소 24 | 25 | - 개발 단계나 단위테스트를 수행할 때 사용할 수 있게 `Map`기반 인메모리 데이터베이스를 제공한다. 다음 절에서 다룬다. 26 | 27 | ### 배치 인프라스트럭쳐 config 28 | 29 | `@EnableBatchProcessing`을 사용하면 `JobRepository`를 사용할 수 있다. 30 | 31 | `JobRepository`를 비롯한 모든 스프링배치 인프라스트럭쳐를 커스터마이징을 하고 싶다면 `BatchConfigurer` 인터페이스를 사용하면 된다 32 | 33 | - `BatchConfigurer` 인터페이스 34 | 35 | - 스프링 배치 인프라스트럭쳐 컴포넌트 구성을 커스터마이징하는데 사용되는 전략 인터페이스이다. (일반적으론 `DefaultBatchConfigurer` 을 상속해서 커스터마이징한다.) 36 | - `@EnableBatchProcessing`을 사용했을때 빈이 추가되는 과정 37 | 1. `BatchConfigurer` 구현체에서 빈을 생성한다 38 | 2. `SimpleBatchConfiguration`에서 스프링의 `ApplicationContext`에 생성한 빈을 등록한다 39 | 40 | - `JobRepository`커스터마이징하기 41 | 42 | - 보통 `ApplicationContext`에 두 개 이상의 데이터소스가 존재할때 커스터마이징한다. 43 | - 예) 업무 데이터 용도의 데이터소스와 `JobRepository`용 데이터소스가 별도로 존재할때 44 | 45 | ```java 46 | public class CustomBatchConfigurer extends DefaultBatchConfigurer { 47 | 48 | @Autowired 49 | @Qualifier("repositoryDataSource") //"repositoryDataSource"가 어딘가 있다고 가정. 50 | private DataSource dataSource; 51 | 52 | @Override 53 | protected JobRepository createJobRepository() throws Exception { 54 | JobRepositoryFactoryBean factoryBean = new JobRepositoryFactoryBean(); 55 | factoryBean.setDatabaseType(DatabaseType.MYSQL.getProductName()); 56 | // 접두어 "BATCH_" -> "FOO_" 57 | factoryBean.setTablePrefix("FOO_"); 58 | // 데이터 생성시 트랜잭션 격리레벨 "ISOLATION_SERIALIZED" -> "ISOLATION_REPEATABLE_READ" 59 | factoryBean.setIsolationLevelForCreate("ISOLATION_REPEATABLE_READ"); 60 | factoryBean.setDataSource(this.dataSource); 61 | // 보통 아래의 두 메서드는 스프링 컨테이너가 호출해준다. 근데 create...메서드 모두는 스프링 컨테이너에 노출되지 않는다. 그래서 개발자가 직접 호출해줘야 한다. 62 | factoryBean.afterPropertiesSet(); 63 | return factoryBean.getObject(); 64 | } 65 | } 66 | ``` 67 | 68 | - `TransactionManager`커스터마이징하기 69 | 70 | ```java 71 | public class CustomBatchConfigurer extends DefaultBatchConfigurer { 72 | 73 | @Autowired 74 | @Qualifier("batchTransactionManager") //"batchTransactionManager"가 어딘가 있다고 가정. 75 | private PlatformTransactionManager transactionManager; 76 | 77 | @Override 78 | public PlatformTransactionManager getTransactionManager() { 79 | return this.transactionManager; 80 | } 81 | } 82 | ``` 83 | 84 | - `JobExplorer`커스터마이징하기 85 | 86 | - 배치 메타데이터를 읽기 전용으로 제공하고 싶을 때 87 | 88 | ```java 89 | public class CustomBatchConfigurer extends DefaultBatchConfigurer { 90 | 91 | @Autowired 92 | @Qualifier("repositoryDataSource") 93 | private DataSource dataSource; 94 | 95 | //JobExplorer와 JobRepository는 동일한 데이터 저장소를 사용하므로 함께 커스터마이징을 하는 게 좋다 96 | @Override 97 | protected JobRepository createJobRepository() throws Exception { 98 | JobRepositoryFactoryBean factoryBean = new JobRepositoryFactoryBean(); 99 | factoryBean.setDatabaseType(DatabaseType.MYSQL.getProductName()); 100 | factoryBean.setTablePrefix("FOO_"); 101 | factoryBean.setIsolationLevelForCreate("ISOLATION_REPEATABLE_READ"); 102 | factoryBean.setDataSource(this.dataSource); 103 | factoryBean.afterPropertiesSet(); 104 | return factoryBean.getObject(); 105 | } 106 | 107 | @Override 108 | protected JobExplorer createJobExplorer() throws Exception { 109 | JobExplorerFactoryBean factoryBean = new JobExplorerFactoryBean(); 110 | factoryBean.setDataSource(this.dataSource); 111 | factoryBean.setTablePrefix("FOO_"); 112 | factoryBean.afterPropertiesSet(); 113 | return factoryBean.getObject(); 114 | } 115 | } 116 | ``` 117 | 118 | - `JobLauncher`커스터마이징하기 119 | 120 | - 스프링 배치가 기본 제공하는 `SimpleJobLauncher` 외의 방식으로 커스터마이징하고 싶을때 (예 : 컨트롤러를 통해 잡 실행하려 할때) 121 | 122 | - 데이터베이스 config 123 | 124 | ```yaml 125 | spring: 126 | datasource: 127 | driverClassName: ... 128 | url: ... 129 | username: ... 130 | password: ... 131 | batch: 132 | initialize-schema: ... 133 | ``` 134 | 135 | - `initialize-schema` : 스프링부트가 스프링배치 스키마 스크립트를 실행하도록 지시 136 | - `always` : 애플리케이션을 실행할때마다 스크립트 실행. 개발 환경일때 사용하기 쉬움 137 | - `never` : 스크립트를 실행하지 않음 138 | - `embedded` : 내장 데이터베이스를 사용할때, 각 실행 시마다 데이터가 초기화된 데이터베이스 인스턴스를 사용한다는 가정으로 스크립트를 실행 139 | 140 | ### 잡 메타데이터 사용하기 141 | 142 | 어떻게 `JobRepository` 내의 정보를 얻을 수 있을까? 주로 `JobExplorer`을 사용한다. 143 | 144 | - `JobExplorer` 145 | 146 | - `JobExplorer`은 `JobRepository`의 데이터에 접근하는 시작점이다. 147 | 148 | - 대부분의 배치 프레임워크 컴포넌트가 `JobRepository`를 사용해 잡 관련 정보에 접근하지만 `JobExplorer`은 데이터베이스에 직접 접근한다. 149 | 150 | - 예시 : 잡의 `JobInstance`가 얼마나 많이 실행됐는지, 각 `JobInstance`당 얼마나 많은 실제 실행이 있었는지 확인하는 config 151 | 152 | - ```java 153 | public class ExploringTasklet implements Tasklet { 154 | 155 | private JobExplorer explorer; 156 | 157 | public ExploringTasklet(JobExplorer explorer) { 158 | this.explorer = explorer; 159 | } 160 | 161 | @Override 162 | public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception { 163 | String jobName = chunkContext.getStepContext().getJobName(); 164 | List instances = explorer.getJobInstances(jobName, 0, Integer.MAX_VALUE); 165 | System.out.printf("%s 잡에는 %d개의 잡 인스턴스가 존재합니다.", jobName, instances.size()); 166 | 167 | System.out.println("********************* 결과 *********************"); 168 | for (JobInstance instance : instances) { 169 | List jobExecutions = explorer.getJobExecutions(instance); 170 | System.out.printf("%d 인스턴스에는 %d개의 execution이 존재합니다.", 171 | instance.getInstanceId(), jobExecutions.size()); 172 | for (JobExecution jobExecution : jobExecutions) { 173 | System.out.printf("\t%d execution의 ExitStatus 결과는 %s입니다.", 174 | jobExecution.getId(), jobExecution.getExitStatus()); 175 | } 176 | } 177 | return RepeatStatus.FINISHED; 178 | } 179 | } 180 | ``` 181 | 182 | ```java 183 | @Autowired 184 | private JobExplorer jobExplorer; 185 | 186 | @Bean 187 | public Job explorerJob() { 188 | return jobBuilderFactory.get("explorerJob") 189 | .start(explorerStep()) 190 | .build(); 191 | } 192 | 193 | @Bean 194 | public Step explorerStep() { 195 | return stepBuilderFactory.get("explorerStep") 196 | .tasklet(explorerTasklet()) 197 | .build(); 198 | } 199 | 200 | @Bean 201 | public Tasklet explorerTasklet() { 202 | return new ExploringTasklet(jobExplorer); 203 | } 204 | ``` 205 | -------------------------------------------------------------------------------- /05주차/요약본/[5주차]_5장_JobRepository와메타데이터_김명수.md: -------------------------------------------------------------------------------- 1 | # 배치 인프라스트럭처 구성하기 2 | 3 | - [배치 인프라스트럭처 구성하기](#배치-인프라스트럭처-구성하기) 4 | - [BatchConfigurer 인터페이스](#BatchConfigurer-인터페이스) 5 | - [JobRepository 커스터마이징하기](#JobRepository-커스터마이징하기) 6 | - [TransactionManager 커스터마이징하기](#TransactionManager-커스터마이징하기) 7 | - [JobExplorer 커스터마이징하기](#JobExplorer-커스터마이징하기) 8 | - [JobLauncher 커스터마이징하기](#JobLauncher-커스터마이징하기) 9 | - [데이터베이스 구성하기](#데이터베이스-구성하기) 10 | - [잡 메타데이터 사용하기](#잡-메타데이터-사용하기) 11 | 12 | --- 13 | 14 | # 배치 인프라스트럭처 구성하기 15 | 16 | ## BatchConfigurer 인터페이스 17 | 18 | - `BatchConfigurer` 인터페이스는 스프링 배치 인프라스트럭처 컴포넌트의 구성을 커스터마이징하는 데 사용되는 전략 인터페이스(`strategy interace`)이다. 19 | - `BatchConfigurer` -> `SimpleBatchConfiguration` 으로 빈을 등록하는데 보통 `BatchConfigurer`를 통해 커스터마이징 빈을 등록한다. 20 | - `DefaultBatchConfigurer` 를 통해 필요한 빈만 재정의한다. 21 | 22 | ```java 23 | public interface BatchConfigurer { 24 | 25 | JobRepository getJobRepository() throws Exception; 26 | 27 | PlatformTransactionManager getTransactionManager() throws Exception; 28 | 29 | JobLauncher getJobLauncher() throws Exception; 30 | 31 | JobExplorer getJobExplorer() throws Exception; 32 | } 33 | ``` 34 | 35 | --- 36 | 37 | ## JobRepository 커스터마이징하기 38 | 39 | p.190의 표를 살펴보면 `JobRepositoryFactoryBean`의 여러가지 커스터마이징 옵션이 존재한다. 40 | 41 | `DefaultBatchConfigurer`를 상속해서 아래와 같이 `JobRepository`를 재정의할 수 있다. 42 | 43 | ```java 44 | @Slf4j 45 | @Profile("job-repository-custom") 46 | @Configuration 47 | public class CustomBatchConfigurer extends DefaultBatchConfigurer { 48 | 49 | @Autowired 50 | @Qualifier("repositoryDataSource") 51 | private DataSource dataSource; 52 | 53 | @Override 54 | protected JobRepository createJobRepository() throws Exception { 55 | final JobRepositoryFactoryBean factoryBean = new JobRepositoryFactoryBean(); 56 | 57 | factoryBean.setDatabaseType(DatabaseType.MYSQL.getProductName()); 58 | factoryBean.setTablePrefix("FOO_"); 59 | factoryBean.setIsolationLevelForCreate("ISOLATION_REPEATABLE_READ"); 60 | factoryBean.setDataSource(dataSource); 61 | 62 | // BatchConfigurer의 메소드는 스프링에서 직접 노출되지 않으므로 63 | // InitilizerBean.afterPropertiesSet() 및 FactoryBean.getObject() 메소드를 호출해야한다. 64 | factoryBean.afterPropertiesSet(); 65 | 66 | return factoryBean.getObject(); 67 | } 68 | } 69 | ``` 70 | 71 | --- 72 | 73 | ## TransactionManager 커스터마이징하기 74 | 75 | `TransactionManager`의 모든 구성 옵션을 다루는 대신 이미 정의된 빈을 기반으로 어떻게 커스터마이징 하는지 살펴보자. 76 | 77 | ```java 78 | @Slf4j 79 | @Profile("customize-configurer") 80 | @Configuration 81 | public class CustomBatchConfigurer extends DefaultBatchConfigurer { 82 | 83 | @Autowired 84 | @Qualifier("batchTransactionManager") 85 | private PlatformTransactionManager transactionManager; 86 | 87 | @Override 88 | public PlatformTransactionManager getTransactionManager() { 89 | return transactionManager; 90 | } 91 | } 92 | ``` 93 | 94 | --- 95 | 96 | ## JobExplorer 커스터마이징하기 97 | 98 | `JobExplorer`는 배치 메타데이터를 읽기 전용으로 제공한다. 99 | 100 | ```java 101 | @Slf4j 102 | @Profile("customize-configurer") 103 | @Configuration 104 | public class CustomBatchConfigurer extends DefaultBatchConfigurer { 105 | 106 | @Autowired 107 | @Qualifier("repositoryDataSource") 108 | private DataSource dataSource; 109 | 110 | @Override 111 | protected JobExplorer createJobExplorer() throws Exception { 112 | final JobExplorerFactoryBean factoryBean = new JobExplorerFactoryBean(); 113 | 114 | factoryBean.setDataSource(dataSource); 115 | factoryBean.setTablePrefix("FOO_"); 116 | 117 | // BatchConfigurer의 메소드는 스프링에서 직접 노출되지 않으므로 118 | // InitilizerBean.afterPropertiesSet() 및 FactoryBean.getObject() 메소드를 호출해야한다. 119 | factoryBean.afterPropertiesSet(); 120 | 121 | return factoryBean.getObject(); 122 | } 123 | } 124 | ``` 125 | 126 | --- 127 | 128 | ## JobLauncher 커스터마이징하기 129 | 130 | - `JobLauncher`는 스프링 배치 잡을 실행하는 진입점이며 `SimpleJobLauncher`를 기본으로 사용한다. 131 | - 스프링 부트의 기본 메커니즘으로 잡을 실행할 때는 대부분 JobLauncher를 커스터마이징 할 필요가 없다. 132 | - 스프링 MVC 애플리케이션의 일부로써 특정 API 호출 시 잡을 실행하고 싶을 때 커스터마이징이 필요하다. 133 | 134 | ```java 135 | public class SimpleJobLauncher implements JobLauncher, InitializingBean { 136 | 137 | protected static final Log logger = LogFactory.getLog(SimpleJobLauncher.class); 138 | 139 | private JobRepository jobRepository; 140 | private TaskExecutor taskExecutor; 141 | 142 | ... 143 | 144 | // 사용할 JobRepository를 지정한다. 145 | public void setJobRepository(JobRepository jobRepository) { 146 | this.jobRepository = jobRepository; 147 | } 148 | 149 | // TaskExecutor를 지정한다. 기본적으로 SyncTaskExecutor가 설정된다.(아래 afterPropertiesSet()) 150 | public void setTaskExecutor(TaskExecutor taskExecutor) { 151 | this.taskExecutor = taskExecutor; 152 | } 153 | 154 | @Override 155 | public void afterPropertiesSet() throws Exception { 156 | Assert.state(jobRepository != null, "A JobRepository has not been set."); 157 | if (taskExecutor == null) { 158 | logger.info("No TaskExecutor has been set, defaulting to synchronous executor."); 159 | taskExecutor = new SyncTaskExecutor(); 160 | } 161 | } 162 | } 163 | ``` 164 | 165 | --- 166 | 167 | ## 데이터베이스 구성하기 168 | 169 | 아래와 같은 설정으로 데이터 베이스를 구성할 수 있다. 170 | 171 | ```yaml 172 | spring: 173 | datasource: 174 | driver-class-name: com.mysql.cj.jdbc.Driver 175 | url: jdbc:mysql://localhost:53306/spring_batch 176 | username: root 177 | password: p@ssw0rd 178 | batch: 179 | jdbc: 180 | # ["always", "never", "embedded"] 181 | initialize-schema: always 182 | ``` 183 | 184 | --- 185 | 186 | # 잡 메타데이터 사용하기 187 | 188 | 스프링 배치는 내부적으로 여러 DAO를 사용해 `JobRepository` 테이블에 접근하지만 189 | 프레임워크 개발자가 사용할 수 있도록 `JobExplorer`를 통해 훨씬 실용적인 API를 제공한다. 190 | 191 | (p198에 JobExplorer에서 제공하는 여러 메소드에 대한 설명이 있다.) 192 | 193 | **1. ExploringTasklet** 194 | 195 | ```java 196 | @Slf4j 197 | public class ExploringTasklet implements Tasklet { 198 | 199 | private final JobExplorer explorer; 200 | 201 | public ExploringTasklet(JobExplorer explorer) { 202 | this.explorer = explorer; 203 | } 204 | 205 | @Override 206 | public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { 207 | final String jobName = chunkContext.getStepContext().getJobName(); 208 | final List instances = explorer.getJobInstances(jobName, 0, Integer.MAX_VALUE); 209 | 210 | logger.info("## There are {} job instances for the job {}", instances.size(), jobName); 211 | logger.info("They have had the following results"); 212 | logger.info("****************************************************"); 213 | for (JobInstance instance : instances) { 214 | final List executions = explorer.getJobExecutions(instance); 215 | logger.info("> Instance {} had {} executions", instance.getInstanceId(), executions.size()); 216 | 217 | for (JobExecution execution : executions) { 218 | logger.info("\tExecution {} resulted in ExitStatus {}", execution.getId(), execution.getExitStatus()); 219 | } 220 | } 221 | 222 | return RepeatStatus.FINISHED; 223 | } 224 | } 225 | ``` 226 | 227 | **2. Job 구성** 228 | 229 | ```java 230 | @Slf4j 231 | @EnableBatchProcessing 232 | @SpringBootApplication 233 | public class DemoApplication { 234 | 235 | @Autowired 236 | private JobBuilderFactory jobBuilderFactory; 237 | 238 | @Autowired 239 | private StepBuilderFactory stepBuilderFactory; 240 | 241 | @Autowired 242 | private JobExplorer jobExplorer; 243 | 244 | @Bean 245 | public Tasklet explorerTasklet() { 246 | return new ExploringTasklet(jobExplorer); 247 | } 248 | 249 | @Bean 250 | public Step explorerStep() { 251 | return stepBuilderFactory.get("explorerStep") 252 | .tasklet(explorerTasklet()) 253 | .build(); 254 | } 255 | 256 | @Bean 257 | public Job explorerJob() { 258 | return jobBuilderFactory.get("explorerJob") 259 | .start(explorerStep()) 260 | .incrementer(new RunIdIncrementer()) 261 | .build(); 262 | } 263 | } 264 | ``` -------------------------------------------------------------------------------- /05주차/요약본/[5주차]_5장_JobRepository와메타데이터_김성모.md: -------------------------------------------------------------------------------- 1 | # 5장 JobRepository와 메타데이터 2 | 3 | ## JobRepository란? 4 | 5 | > 스프링 배치 내에서 JobRepository 인터페이스를 구현해 데이터를 저장하는 데 사용되는 데이터 저장소이다. 6 | 7 | ### 관계형 데이터베이스 사용하기 8 | 9 | 관계형 데이터베이스는 스프링 배치에서 기본적으로 제공하는 JobRepository 이다. 10 | 11 | ![JobRepository - schema](https://user-images.githubusercontent.com/48056463/133590242-b2e5628c-0046-454d-a0dd-6a8d6f27f2de.png) 12 | 13 | 14 | 1. 테이블 설명 15 | 16 | - 시작점인 `BATCH_JOB_INSTANCE` 17 | 18 | ```sql 19 | CREATE TABLE BATCH_JOB_INSTANCE ( 20 | JOB_INSTANCE_ID BIGINT PRIMARY KEY , 21 | VERSION BIGINT, 22 | JOB_NAME VARCHAR(100) NOT NULL , 23 | JOB_KEY VARCHAR(2500) 24 | ); 25 | ``` 26 | 27 | - 배치 잡의 실제 실행 기록 `BAATCH_JOB_EXECUTION` 28 | 29 | > 잡이 실행될 대 마다 새 레코드 생성되고 잡이 진행되는 동안 주기적으로 업데이트 된다. 30 | 31 | ```sql 32 | CREATE TABLE BATCH_JOB_EXECUTION ( 33 | JOB_EXECUTION_ID BIGINT PRIMARY KEY , 34 | VERSION BIGINT, 35 | JOB_INSTANCE_ID BIGINT NOT NULL, 36 | CREATE_TIME TIMESTAMP NOT NULL, 37 | START_TIME TIMESTAMP DEFAULT NULL, 38 | END_TIME TIMESTAMP DEFAULT NULL, 39 | STATUS VARCHAR(10), 40 | EXIT_CODE VARCHAR(20), 41 | EXIT_MESSAGE VARCHAR(2500), 42 | LAST_UPDATED TIMESTAMP, 43 | JOB_CONFIGURATION_LOCATION VARCHAR(2500) NULL, 44 | constraint JOB_INSTANCE_EXECUTION_FK foreign key (JOB_INSTANCE_ID) 45 | references BATCH_JOB_INSTANCE(JOB_INSTANCE_ID) 46 | ) ; 47 | ``` 48 | 49 | 위 테이블과 연관된 세 개의 테이블 50 | 51 | - 연관 1. BATCH_JOB_EXECUTION_CONTEXT 52 | 53 | > 잡이 여러 번 실행되는 상황에서 관련 정보를 보관하는 장소 54 | 55 | ```sql 56 | CREATE TABLE BATCH_JOB_EXECUTION_CONTEXT ( 57 | JOB_EXECUTION_ID BIGINT PRIMARY KEY, 58 | SHORT_CONTEXT VARCHAR(2500) NOT NULL, 59 | SERIALIZED_CONTEXT CLOB, 60 | constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID) 61 | references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) 62 | ) ; 63 | ``` 64 | 65 | 66 | - 연관 2. BATCH_JOB_EXECUTION_PARAMS 67 | 68 | > 매번 실행될 때 마다 사용된 잡 파라미터를 보관하는 테이블 69 | 70 | ```sql 71 | CREATE TABLE BATCH_JOB_EXECUTION_PARAMS ( 72 | JOB_EXECUTION_ID BIGINT NOT NULL , 73 | TYPE_CD VARCHAR(6) NOT NULL , 74 | KEY_NAME VARCHAR(100) NOT NULL , 75 | STRING_VAL VARCHAR(250) , 76 | DATE_VAL DATETIME DEFAULT NULL , 77 | LONG_VAL BIGINT , 78 | DOUBLE_VAL DOUBLE PRECISION , 79 | IDENTIFYING CHAR(1) NOT NULL , 80 | constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID) 81 | references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) 82 | ); 83 | ``` 84 | 85 | - 연관 3. BATCH_STEP_EXCUTION_CONTEXT 86 | 87 | ```sql 88 | CREATE TABLE BATCH_STEP_EXECUTION_CONTEXT ( 89 | STEP_EXECUTION_ID BIGINT PRIMARY KEY, 90 | SHORT_CONTEXT VARCHAR(2500) NOT NULL, 91 | SERIALIZED_CONTEXT CLOB, 92 | constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID) 93 | references BATCH_STEP_EXECUTION(STEP_EXECUTION_ID) 94 | ) ; 95 | ``` 96 | 97 | 98 | - 잡에서와 마찬가지로 스텝의 상태를 저장하는 BATCH_STEP_EXECUTION 99 | 100 | ```sql 101 | CREATE TABLE BATCH_STEP_EXECUTION ( 102 | STEP_EXECUTION_ID BIGINT PRIMARY KEY , 103 | VERSION BIGINT NOT NULL, 104 | STEP_NAME VARCHAR(100) NOT NULL, 105 | JOB_EXECUTION_ID BIGINT NOT NULL, 106 | START_TIME TIMESTAMP NOT NULL , 107 | END_TIME TIMESTAMP DEFAULT NULL, 108 | STATUS VARCHAR(10), 109 | COMMIT_COUNT BIGINT , 110 | READ_COUNT BIGINT , 111 | FILTER_COUNT BIGINT , 112 | WRITE_COUNT BIGINT , 113 | READ_SKIP_COUNT BIGINT , 114 | WRITE_SKIP_COUNT BIGINT , 115 | PROCESS_SKIP_COUNT BIGINT , 116 | ROLLBACK_COUNT BIGINT , 117 | EXIT_CODE VARCHAR(20) , 118 | EXIT_MESSAGE VARCHAR(2500) , 119 | LAST_UPDATED TIMESTAMP, 120 | constraint JOB_EXECUTION_STEP_FK foreign key (JOB_EXECUTION_ID) 121 | references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID) 122 | ) ; 123 | ``` 124 | 125 | - BATCH_STEP_EXECUTION_CONTEXT 126 | 127 | ```sql 128 | CREATE TABLE BATCH_STEP_EXECUTION_CONTEXT ( 129 | STEP_EXECUTION_ID BIGINT PRIMARY KEY, 130 | SHORT_CONTEXT VARCHAR(2500) NOT NULL, 131 | SERIALIZED_CONTEXT CLOB, 132 | constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID) 133 | references BATCH_STEP_EXECUTION(STEP_EXECUTION_ID) 134 | ) ; 135 | ``` 136 | 137 | 138 | ## 배치 인프라스터럭쳐 구성하기 139 | 140 | > @EnableBatchProcessing 을 사용하면 기본적으로 추가 구성없이 배치가 제공하는 JobRepository를 사용할 수 있다. 141 | 142 | ### BatchConfigurer 만들기 143 | ```java 144 | public interface BatchConfigurer { 145 | 146 | JobRepository getJobRepository() throws Exception; 147 | 148 | PlatformTransactionManager getTransactionManager() throws Exception; 149 | 150 | JobLauncher getJobLauncher() throws Exception; 151 | 152 | JobExplorer getJobExplorer() throws Exception; 153 | } 154 | ``` 155 | 156 | 위의 인터페이스를 구현한 구현체에서 빈을 생성하고 `SimpleBatchConfiguration`에서 스프링 `ApplicationContext`에 생성한 빈을 등록한다. 157 | 158 | 기본적으로 스프링배치가 제공하는 `DefaultBatchConfigurer`를 사용하면 모든 기본 옵션이 제공된다. 따라서 몇 개의 구성만 재정의한다면 `DefaultBatchConfigurer` 를 상속해 적절한 메소드를 재정의하는 것이 더 쉽다. 159 | 160 | 161 | ### JoRepository 커스터마이징하기 162 | 163 | `JobRepositoryFactoryBean` 이라는 `FactoryBean` 을 통해서 `JobRepository`는 생성된다. 164 | 165 | `JobRepositoryFactoryBean` 에서 제공하는 커스터마이징을 set으로 변경하여 커스터마이징할 수 있다. 166 | 167 | ### TransactionManager 커스터마이징하기 168 | 169 | 170 | 위에서 `DefaultBatchConfigurer` 를 상속해 커스터마이징을 진행하면 쉽다고 말하였는데, 이를 위하여 transaction bean 을 가져와서 getTransactionMnager()에 @Override 해주면 된다. 171 | 172 | ```java 173 | public class CustomBatchConfigurer extends DefaultBatchConfigurer { 174 | 175 | @Autowired 176 | @Qualifier("batchTransactionManager") 177 | private PlatformTransactionManager transactionManager; 178 | 179 | @Override 180 | public PlatformTransactionManager getTransactionManager() { 181 | return this.transactionManager; 182 | } 183 | } 184 | ``` 185 | 186 | ### JobExplorer 커스터마이징하기 187 | 188 | ```java 189 | public class CustomBatchConfigurer extends DefaultBatchConfigurer { 190 | 191 | @Autowired 192 | @Qualifier("batchTransactionManager") 193 | private DataSource dataSource; 194 | 195 | @Override 196 | protected JobExplorer createJobExplorer() throws Exception { 197 | JobExplorerFactoryBean factoryBean = new JobExplorerFactoryBean(); 198 | factoryBean.setDataSource(this.dataSource); 199 | factoryBean.setTablePrefix("FOO_"); 200 | factoryBean.afterPropertiesSet(); 201 | return factoryBean.getObject(); 202 | } 203 | } 204 | ``` 205 | 206 | 207 | ### JobLauncher 커스터마이징하기 208 | 209 | 스프링 배치 잡을 실행하는 진입점을 변경해야할 때 구성한다. 210 | 211 | 212 | ## 잡 메타데이터 사용하기 213 | 214 | -------------------------------------------------------------------------------- /05주차/요약본/[5주차]_5장_스프링배치_함호식.md: -------------------------------------------------------------------------------- 1 | 4.JobRepository와 메타데이터 2 | -- 3 | 4 | ##### JobRepository 활용 5 | * 배치 잡 실행 중에 오류발생시 복구 6 | * 실행중 오류 발생시 어떤 처리를 하고 있었는지 확인 7 | * 잡이 다시 실행될 수 있는지 여부 8 | * 잡이 처리되는데 걸리는 시간 9 | * 오류로 인해 재시도된 아이템 수 확인 10 |

11 | 12 | --- 13 | 14 | ##### JobRepository란? 15 | 16 | 2가지 중에 하나를 의미한다. 17 | 1. JobRepository 인터페이스 18 | 2. JobRepository를 구현해 데이터를 저장하는데 사용되는 데이터 저장소 19 | 20 | 스프링 배치는 배치 잡 내에서 바로 사용할 수 있는 두가지 데이터 저장소를 제공한다. 21 | 1. 인메모리 저장소 22 | : 외부 데이터베이스를 구성하는 것이 실제 얻는 이익보다 많은 문제를 발생시킬 수 있다. 23 | 이런 이유로 java.util.Map 객체를 데이터 저장소로 사용하는 JobRepository 구현체를 제공한다. 24 | 2. 관계형 데이터베이스 25 | ![spring batch erd](https://docs.spring.io/spring-batch/docs/3.0.x/reference/html/images/meta-data-erd.png) 26 | * BATCH_JOB_INSTANCE 27 | -> 잡의 논리적 실행을 나타낸다. 28 | (처음 실행시 단일 레코드가 등록된다.) 29 | 30 | * BATCH_JOB_EXECUTION 31 | -> 배치잡의 실제 실행 기록을 나타낸다. 32 | (잡이 진행되는 동안 주기적으로 업데이트 된다.) 33 | 34 | * BATCH_JOB_EXECUTION_PARAMS 35 | -> 잡이 매번 실행될 때마다 사용된 잡 파라미터를 저장한다. 36 | 37 | * BATCH_JOB_EXECUTION_CONTEXT 38 | -> JoBExecution의 ExecutionContext를 저장한다. 39 | 40 | * BATCH_STEP_EXECUTION 41 | -> 스텝의 시작, 완료, 상태에 대한 메타데이터를 저장한다. 42 | -> 스텝 분석이 가능하도록 다양한 횟수 값을 저장한다. 43 | (읽기, 처리, 쓰기, 건너뛰기 횟수) 44 | 45 | * BATCH_STEP_EXECUTION_CONTEXT 46 | -> 스텝 수준에서 컴포넌트의 상태를 저장하는데 사용된다. 47 |

48 | 49 | --- 50 | 51 | ##### 배치 인프라스트럭처 구성하기 52 | 53 | @EnableBatchProcessing 애너테이션을 이용하면, 스프링 배치가 제공하는 JobRepository를 사용할 수 있다. 54 | 55 | 애너테이션 적용시 빈이 추가되는 과정 56 | 1. BatchConfigurer 구현체에서 빈을 생성한다. 57 | 2. SimpleBatchConfiguration에서 ApplicationContext에 생성한 빈을 등록한다. 58 | 59 | ##### BatchConfigurer 인터페이스 60 | 스프링 배치 인프라스트럭처 컴포넌트의 구성을 커스터마이징하는데 사용되는 전략 인터페이스이다. 61 | 62 | ```java 63 | public interface BatchConfigurer { 64 | 65 | JobRepository getJobRepository() throws Exception; 66 | 67 | // 프레임워크가 제공하는 모든 트랜잭션을 관리할 때 사용 68 | PlatformTransactionManager getTransactionManager() throws Exception; 69 | 70 | JobLauncher getJobLauncher() throws Exception; 71 | 72 | JobExplorer getJobExplorer() throws Exception; 73 | } 74 | ``` 75 | 76 | 스프링 배치가 제공하는 DefaultBatchConfigurer를 사용하면 기본 컴포넌트가 제공된다. 77 | 일반적인 시나리오라면 여러 컴포넌트 중에 일부만 재정의하므로, 78 | BatchConfigurer를 구현하는 것보다 DefaultBatchConfigurer를 상속해, 79 | 적절한 메서드를 재정의하는 것이 더 간편하다. 80 | ```java 81 | public class DefaultBatchConfigurer implements BatchConfigurer { 82 | ... 83 | 84 | private DataSource dataSource; 85 | private PlatformTransactionManager transactionManager; 86 | private JobRepository jobRepository; 87 | private JobLauncher jobLauncher; 88 | private JobExplorer jobExplorer; 89 | 90 | ... 91 | 92 | @PostConstruct 93 | public void initialize() { 94 | try { 95 | if(dataSource == null) { 96 | logger.warn("No datasource was provided...using a Map based JobRepository"); 97 | 98 | if(getTransactionManager() == null) { 99 | logger.warn("No transaction manager was provided, using a ResourcelessTransactionManager"); 100 | this.transactionManager = new ResourcelessTransactionManager(); 101 | } 102 | 103 | MapJobRepositoryFactoryBean jobRepositoryFactory = new MapJobRepositoryFactoryBean(getTransactionManager()); 104 | jobRepositoryFactory.afterPropertiesSet(); 105 | this.jobRepository = jobRepositoryFactory.getObject(); 106 | 107 | MapJobExplorerFactoryBean jobExplorerFactory = new MapJobExplorerFactoryBean(jobRepositoryFactory); 108 | jobExplorerFactory.afterPropertiesSet(); 109 | this.jobExplorer = jobExplorerFactory.getObject(); 110 | } else { 111 | this.jobRepository = createJobRepository(); 112 | this.jobExplorer = createJobExplorer(); 113 | } 114 | 115 | this.jobLauncher = createJobLauncher(); 116 | } catch (Exception e) { 117 | throw new BatchConfigurationException(e); 118 | } 119 | } 120 | ... 121 | } 122 | ``` 123 | 124 | 125 | JobExplorer는 JobRepository가 다루는 데이터와 `동일한 데이터`를 `읽기 전용`으로만 보는 뷰이다. 126 | -> JobExplorer와 JobRepository는 동일한 데이터 저장소를 사용하므로 동기화시키는 것이 좋다. 127 |

128 | ```java 129 | public class SimpleJobExplorer implements JobExplorer { 130 | 131 | @Override 132 | public Set findRunningJobExecutions(@Nullable String jobName) { 133 | ... 134 | } 135 | 136 | @Override 137 | public JobExecution getJobExecution(@Nullable Long executionId) { 138 | ... 139 | } 140 | 141 | @Override 142 | public List getJobNames() { 143 | return jobInstanceDao.getJobNames(); 144 | } 145 | ... 146 | } 147 | ``` 148 | 149 | --- 150 | ##### JobLauncher 커스터마이징 151 | JobLauncher: 스프링 배치 잡을 실행하는 진입점 152 | -> 스프링 부트는 기본적으로 스프링 배치가 제공하는 SimpleJobLauncher를 사용한다. 153 | 그러므로 대부분 JobLauncher를 커스터마이징할 필요가 없다. 154 | +) 그러나 스프링 MVC의 일부분으로 배치가 존재하며, 155 | 컨트롤러를 통해 해당 잡이 실행된다면 SimpleJobLauncher의 동작 방식을 조정할 수 있다. 156 | 157 | ```java 158 | public class SimpleJobLauncher implements JobLauncher, InitializingBean { 159 | ... 160 | // 사용할 Repository 지정 161 | public void setJobRepository(JobRepository jobRepository) { 162 | this.jobRepository = jobRepository; 163 | } 164 | 165 | // JobLauncher에 사용할 TaskExecutor를 설정한다. 166 | // default = SyncTaskExecutor 167 | public void setTaskExecutor(TaskExecutor taskExecutor) { 168 | this.taskExecutor = taskExecutor; 169 | } 170 | ... 171 | } 172 | ``` 173 |

174 | 175 | --- 176 | 177 | ##### 데이터베이스 구성하기 178 | 179 | ```yaml 180 | # database 181 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 182 | spring.datasource.url=jdbc:mysql://localhost:3306/mysql 183 | spring.datasource.username= 184 | spring.datasource.password= 185 | 186 | # batch 187 | spring.batch.jdbc.initialize-schema=always 188 | ``` 189 | 190 | spring.batch.jdbc.initialize-schema 191 | * always (개발환경에서 자주 쓰임) 192 | -> 애플케이션을 실행할 때마다 스크립트가 실행된다. 193 | -> 스프링 배치 SQL 파일에 drop문이 없고 오류 발생시 무시 194 | * never 195 | -> 스크립트를 실행하지않는다. 196 | * embedded 197 | -> 내장 데이터베이스 사용할 때, 각 실행시마다 초기화된 데이터베이스 인스턴스를 사용한다고 가정한다. 198 |

199 | 200 | --- 201 | 202 | ##### 요약 203 | 스프링 배치가 `잡과 관련된 메타데이터를 관리`하거나 204 | `실행 중인 잡의 상태를 오류 처리에 사용할 수 있도록 저장하는 기능`은 205 | 스프링 배치를 사용하는 주요 이유중에 하나다. -------------------------------------------------------------------------------- /05주차/요약본/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /06주차/발표자료/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /06주차/요약본/[6주차]_6장_스프링배치_함호식.md: -------------------------------------------------------------------------------- 1 | 6.잡 실행하기 2 | -- 3 | 4 | ##### 스프링 부트로 배치 잡 시작시키기 5 | * 스프링 부트는 CommandLineRunner와 ApplicationRunner라는 두 가지 메커니즘을 이용해 실행 시 로직을 수행한다. 6 | 두 인터페이스는 ApplicationContext가 리프레시 되고, 애플리케이션이 실행할 준비가 된 후 호출되는 하나의 메서드를 가지고 있다. 7 | 스프링 부트를 스프링 배치와 함께 사용할 때는 JobLauncherCommandLineRunner라는 특별한 CommandLineRunner가 실행된다. 8 | 9 |
10 | 11 | * 스프링 부트가 ApplicationContext 내에 구성된 CommandLineRunner를 실행할 때, 12 | 클래스패스에 spring-boot-starter-batch가 존재한다면 jobLauncherCommandLineRunner는 13 | 컨텍스트 내에서 찾아낸 모든 잡을 실행한다. 14 | 15 | 기동 시에 어떤 잡도 실행되지 않도록 설정하는 방법 16 | ```xml 17 | # application.properties 18 | spring.batch.job.enabled=false 19 | ``` 20 | 21 | ```java 22 | // java code 23 | public static void main(String[] args) { 24 | SpringApplication application = new SpringApplication(NoRunJob.class); 25 | 26 | Properties properties = new Properties(); 27 | properties.put("spring.batch.job.enabled", false); 28 | application.setDefaultProperties(properties); 29 | 30 | application.run(args); 31 | } 32 | ``` 33 | 34 | 기동 시에 특정한 잡만 실행하는 방법 35 | ```xml 36 | # application.properties 37 | # 쉼표로 구분된 잡 목록을 가져와 순서대로 실행한다. 38 | spring.batch.job.names=JobName1, JobName2 39 | ``` 40 | 41 | --- 42 | 43 | ##### REST 방식으로 잡 실행하기 44 | 45 | JobLauncher 인터페이스는 실행할 잡과 전달할 잡 파라미터를 argument로 전달 받는다. 46 | ```java 47 | // JobLauncher 인터페이스 48 | public interface JobLauncher { 49 | public JobExecution run(Job job, JobParameters jobParameters) throws JobExecutionAlreadyRunningException, 50 | JobRestartException, JobInstanceAlreadyCompleteException, JobParametersInvalidException; 51 | } 52 | ``` 53 | 54 | 배치 잡을 즉시 실행할 수 있는 REST API가 존재하는 것이 아니라, 55 | 직접 개발해야한다. 56 | ```java 57 | @PostMapping("/run") 58 | public ExitStatus runJob(@RequestBody JobLauncherRequest request) { 59 | Job job = this.context.getBean(request.getName(), Job.class); 60 | 61 | return this.jobLauncher.run(job, request.getJobParameters()).getExitStatus(); 62 | } 63 | ``` 64 | * 애플리케이션 기동시 배치 잡이 수행되지 않도록 `spring.batch.job.enabled=false`가 작성되어야 하고, 65 | @EnableBatchProcessing 애너테이션을 적용하면 스프링 배치가 제공하는 SimpleJobLauncher를 바로 사용할 수 있다. 66 | * 기본적으로 JobLauncher는 동기식을 실행하므로 ExitStatus를 반환할 수 있다. 67 | 대부분 배치 잡은 처리량이 많으므로 비동기 방식으로 실행하고, 비동기 방식으로 실행하면, 68 | JobExecutionID만 반환한다. 69 | 70 | ```java 71 | @PostMapping("/run") 72 | public ExitStatus runJob(@RequestBody JobLauncherRequest request) { 73 | Job job = this.context.getBean(request.getName(), Job.class); 74 | 75 | JobParameters jobParameters = 76 | new JobParametersBuilder(request.getJobParameters(), this.jobExploror) 77 | .getNextJobParameters(job) 78 | .toJobParameters(); 79 | 80 | return this.jobLauncher.run(job, jobParameters).getExitStatus(); 81 | } 82 | ``` 83 | 84 | * `JobParametersBuilder.getNextJobParameters`를 호출하면 run.id라는 파라미터가 추가된 새로운 JobParameters 인스턴스가 생성된다. 85 | getNextJobParameters는 Job이 JobParametersIncrementer를 가지고 있는지 해당 job을 확인 후 판별한다. 86 | 87 | --- 88 | ##### 잡 중지하기 89 | 90 | 프로그래밍적으로 중지하기 91 | * 중지 트랜지션 사용하기 92 | ```java 93 | @AfterStep 94 | public ExitStatus afterStep(StepExecution stepExecution) { 95 | if (condition) { 96 | return execution.getExitStatus(); 97 | } else { 98 | return ExitStatus.STOPPED; 99 | } 100 | } 101 | ``` 102 | -> 조건이 유효하지 않다면, STOPPED를 반환하여 잡을 중지할 수 있다. 103 | 104 | ```java 105 | @Bean 106 | public Job transactionJob() { 107 | return this.jobBuilderFactory.get("transactionJob") 108 | .start(importTransactionFileStep()) 109 | .on("STOPPED").stopAndRestart(importTransactionFileStep()) 110 | .from(importTransactionFileStep()).on("*").to(appliTransactionStep()) 111 | .from(appliTransactionStep()).next(generateAccountSummaryStep()) 112 | .end() 113 | .build(); 114 | } 115 | ``` 116 | -> `importTransactionFileStep`가 가장 먼저 시작된다. 117 | 이때 ExitStatus가 STOPPED라면, 재시작시 해당 스텝부터 다시 시작한다. 118 | ExitStatus가 STOPPED가 아니라면, `appliTransactionStep`으로 넘어간다. 119 | -> 잡이 중지되면 변경중이던 청크의 내용을 롤백한다. 120 | * StepExecution을 사용해 중지하기 121 | AfterStep 대신 BeforeStep을 사용하도록 변경해 StepExecution을 사용한다. 122 | -> 이렇게 하면 StepExecution에 접근할 수 있으므로 StepExecution.setTerminateOnly() 메서드를 호출할 수 있다. 123 | ```java 124 | private void SomeProcess () { 125 | if (condition) { 126 | 127 | } else { 128 | this.stepExecution.setTerminateOnly(); 129 | } 130 | } 131 | 132 | @BeforeStep 133 | public void beforeStep(StepExecution stepExecution) { 134 | this.stepExecution = execution; 135 | } 136 | ``` 137 | 138 | --- 139 | 140 | ##### 오류 처리 141 | 142 | * 잡 실패 143 | -> 스프링 배치는 예외가 발생하면 기본적으로 스텝 및 잡이 실패한 것으로 간주한다. 144 | 145 | | 방식 | ExitStatus | 146 | | --- | --- | 147 | | StepExecution을 사용해 잡을 중지를 하는 방식 | STOPPED | 148 | | 예외를 발생시켜 잡을 중지하는 방식 | FAILED | 149 | 150 | `스텝이 FAILED로 식별되면 스프링 배치는 해당 스텝을 처음부터 다시 시작하지 않는다.` 151 | 152 | --- 153 | 154 | ##### 재시작 제어하기 155 | 156 | * 잡의 재시작 방지하기 157 | -> jobBuilder의 preventRestart()를 호출해 잡을 다시 시작할 수 없도록 구성할 수 있다. 158 | ```java 159 | @Bean 160 | public Job someJob() { 161 | return this.jobBuilderFactory.get("someJob") 162 | .preventRestart() 163 | .start(xxx()) 164 | .build(); 165 | } 166 | ``` 167 | -> 잡 실패 후 다시 실행하려고 시도하면 `JobRestartException`이 발생한다. 168 | * 재시작 횟수를 제한하도록 구성하기 169 | -> 스프링 배치는 이 기능을 잡 대시 `스텝` 수준에서 제공한다. 170 | ```java 171 | @Bean 172 | public Step someStep() { 173 | return this.stepBuilderFactory.get("someStep") 174 | .startLimit(2) 175 | chunk(100) 176 | .build(); 177 | } 178 | ``` 179 | -> 이 잡은 두 번까지만 실행 가능하다. 180 | 재실행 횟추 제한 초과시 `StartLimitExceededException`이 발생한다. 181 | 182 | * 완료된 스텝 재실행하기 183 | -> 스프링 배치 특징 중 하나는 동일한 파라미터로 잡을 한 번만 성공적으로 실행할 수 있다. 184 | `이 문제를 해결할 방법은 없다.` 185 | 그러나 스텝에는 이 규칙이 반드시 적용되는 것은 아니다. 186 | 187 | 프레임워크의 기본 구성을 재정의함으로써 완료된 스텝을 두 번 이상 실행할 수 있다. 188 | 스텝이 잘 완료됐더라도 다시 실행할 수 있어야 한다는 것을 프레임워크에게 알리려면 189 | StepBuilder의 `allowStartIfComplete()` 메서드를 사용하면 된다. 190 | ```java 191 | @Bean 192 | public Step someStep() { 193 | return this.stepBuilderFactory.get("someStep") 194 | .allowStartIfComplete(true) 195 | chunk(100) 196 | .build(); 197 | } 198 | ``` 199 | -> 이 예제는 이전 실행에서 실패했거나 중지된 잡 내의 스텝이 다시 실행된다. 200 | 해당 스텝은 이전 실행 시에 완료 상태로 종료되므로 재시작할 중간 단계가 없어 처음부터 다시 시작된다. 201 | 202 | > 잡의 ExitStatus가 COMPLETED라면 모든 스텝에 `allowStartIfComplete(true);`를 적용해 구성하더라도 203 | > 이와 관계없이 잡 인스턴스를 다시 실행할 수 없다. -------------------------------------------------------------------------------- /06주차/요약본/[6주차]_6장_잡_실행하기_김광훈.md: -------------------------------------------------------------------------------- 1 | # 잡 실행하기 2 | ## 1. 스프링 부트로 잡 실행하기 3 | #### - 스프링 부트에서는 CommandLineRunner 와 ApplicationRunner 라는 두 메커니즘을 이용해 실행 시 로직 수행 4 | #### - 스프링 부트를 스프링 배치와 함께 사용할 때는 JobLauncherCommandLineRunner 라는 CommandLineRunner 가 사용된다. 5 | #### - JobLauncherCommandLineRunner 는 스프링 배치의 JobLauncher 를 사용해 모든 잡을 실행한다. 6 | #### - 만약 특정한 잡만 실행시키고 싶다면 spring.batch.job.names 프로퍼티를 사용해 애플리케이션 기동 시에 실행할 잡을 구성할 수 있다. 7 | 8 |
9 | 10 | ## 2. REST 방식으로 잡 실행하기 11 | #### - JobLauncher 를 사용해서 API 를 실행시킬 수 있다. 12 | #### - JobLauncher 인터페이스에는 run 메서드 하나만 존재 13 | #### - 스프링 배치는 기본적으로 유일한 JobLauncher 구현체인 SimpleJobLauncher 제공 14 | #### - SimpleJobLauncher 는 전달 받은 JobParameters 조작 지원 X 15 | #### - SimpleJobLauncher 는 기본적으로 동기식이다. 16 | #### - 예시 17 | ```java 18 | @PostMappint("/run") 19 | public ExitStatus runJob(@RequestBody JobLauncherRequest request) { 20 | Job job = this.context.getBean(request.getName(), Job.class); 21 | 22 | return this.jobLauncher.run(job, request.getJobParameters()) 23 | .getExitStatus(); 24 | } 25 | 26 | ``` 27 | 28 | #### - JobParametersIncrementer 를 사용할 떄 파라미터 변경 사항을 적용하는 JobLauncher 가 수행한다. 29 | 30 |
31 | 32 | ## 3. 쿼츠를 사용해 스케줄링하기 33 | #### - 쿼츠는 스케줄러, 잡, 트리거 세 가지 주요 컴포넌트를 가진다. 34 | - 스케줄러: 트리거 저장소 기능, 잡을 실행하는 역할 35 | - 잡: 실행할 작업의 단위 36 | - 트리거: 작업 실행 시점 정의 37 | 38 | #### - QuartzJobBean 클래스 상속해서 사용 39 | 40 | 41 | ## 4. 잡 중지 42 | #### 자연스러운 완료 43 | - 각각의 잡은 스텝이 COMPLETED 상태를 반환할 때까지 스텝을 실행했으며 모든 스텝이 완료되면 잡 자신도 COMPLETED 종료 코드를 반환 44 | - 동일한 파라미터 값으로 잡이 한 번 정상적으로 실행됐다면 또 다시 실행시킬 수 없다. 45 | 46 | #### 프로그래밍적으로 종료 (중지 트랜잭션) 47 | - 중지 트랜잭션으로 프로그래밍 방식으로 잡을 중지할 수 있다. 48 | 49 | #### StepExecution 을 사용해 중지하기 50 | - AfterStep 대신 BeforeStep 을 사용하도록 변경해 StepExecution 을 가져온다. 51 | - 이렇게 하면 StepExecution 에 접근할 수 있으므로 푸터 레코드를 읽을 때 StepExecution.setTerminateOnly() 메서드를 호출할 수 있다. 52 | - setTerminateOnly() 메서드는 스텝이 완료된 후 스프링 배치가 종료되도록 지시하는 플래그를 설정한다. 53 | - 예시 54 | ```java 55 | private Transaction process(FieldSet fieldSet) { 56 | 57 | if(this.recordCount == 25) { 58 | throw new ParseException("This isn't what I hoped to happen"); 59 | } 60 | 61 | Transaction result = null; 62 | 63 | if(fieldSet != null) { 64 | if(fieldSet.getFieldCount() > 1) { 65 | result = new Transaction(); 66 | result.setAccountNumber(fieldSet.readString(0)); 67 | result.setTimestamp( 68 | fieldSet.readDate(1, "yyyy-MM-DD HH:mm:ss")); 69 | result.setAmount(fieldSet.readDouble(2)); 70 | 71 | recordCount++; 72 | } else { 73 | expectedRecordCount = fieldSet.readInt(0); 74 | 75 | if(expectedRecordCount != this.recordCount) { 76 | this.stepExecution.setTerminateOnly(); 77 | } 78 | } 79 | } 80 | 81 | return result; 82 | } 83 | ``` 84 | 85 | ## 5. 재시작 제어하기 86 | #### 잡의 재시작 방지하기 87 | - preventRestart() 사용 88 | 89 | #### 재시작 횟수 제한하기 90 | - startLimit() 사용 91 | 92 | #### 완료된 스텝 재실행하기 93 | - allowStartComplete() 사용 -------------------------------------------------------------------------------- /06주차/요약본/[6주차]_6장_잡_실행하기_황준호.md: -------------------------------------------------------------------------------- 1 | ## 6장. 잡 실행하기 2 | 3 | ### 스프링 부트로 배치 잡 시작시키기 4 | 5 | - 스프링부트는 `CommandLineRunner`와 `ApplicationRunner`라는 두가지 메커니즘을 이용해 실행시 로직을 수행한다 6 | 7 | - 두 인터페이스는 한개의 메서드(`run()` : 애플리케이션이 코드를 실행할 준비가 되면 호출됨)를 가지고있다 8 | - 스프링부트를 스프링배치와 함께 사용할땐 `JobLauncherCommandLineRunner`가 사용된다 9 | - 스프링부트가 `ApplicationContext`내에 구성된 모든 `CommandLineRunner`를 실행할 때 클래스패스에 `spring-boot-starter-batch`가 존재한다면 `JobLauncherCommandLineRunner`는 컨텍스트 내에서 찾아낸 모든 잡을 실행한다. 10 | - 이전 예제는 모두 이런식으로 작동했음 11 | 12 | - 애플리케이션 기동 시에 실행할 잡 정의하는 법 13 | 14 | - 애플리케이션이 기동될때 잡이 실행되지 않도록 `spring.batch.job.enabled`를 false로 설정해야 한다 (기본값은 true) 15 | 16 | ```java 17 | public static void main(String[] args) { 18 | SpringApplication application = new SpringApplication(BatchApplication.class); 19 | Properties properties = new Properties(); 20 | properties.put("spring.batch.job.enable", false); 21 | application.setDefaultProperties(properties); 22 | application.run(args); 23 | } 24 | ``` 25 | 26 | - 컨텍스트의 여러 잡 중 특정 잡만 실행하는 법 27 | 28 | - `spring.batch.job.names` 프로퍼티 사용 29 | 30 | ### REST 방식으로 잡 실행하기 31 | 32 | - 컨트롤러와 `JobLauncher.run(Job, JobParameters)`을 연결시키면 된다 33 | 34 | - 스프링 배치는 기본적으로 `JobLauncher`의 구현체인 `SimpleJobLauncher`을 제공한다 35 | 36 | - `SimpleJobLauncher`은 `JobParameters`에 대한 조작을 제공하지 않으므로 전달되기 전에 조작해야 한다. 37 | 38 | - `JobLauncher`가 사용하는 `TaskExecutor`을 구성하여 동기식/비동기식을 선택할 수 있다. 39 | 40 | - `SimpleJobLauncher`은 기본적으로 동기식 (호출자와 동일한 스레드에서 잡이 수행됨) 41 | - 잡이 오래 걸리는경우 비동기가 적절할 수도 있음 (이럴땐 `JobExecution`의 id만 반환됨) 42 | 43 | - 잡 파라미터를 자동으로 증가시키고 싶다면? `JobParametersBuilder.getNextJobParameters` 이용. 44 | 45 | ```java 46 | // inputJobParameters은 입력받은 잡 파라미터, JobExplorer은 autowired 47 | JobParameters jobParameters = new JobParametersBuilder(inputJobParameters, jobExplorer) 48 | .getNextJobParameters(job) //입력받은 job 정보로 찾은 job(incrementer config가 되어 있어야 함) 49 | .toJobParameters(); 50 | return jobLauncher.run(job, jobParameters).getExitStatus(); 51 | ``` 52 | 53 | - 쿼츠로 스케줄링하기 54 | 55 | - 쿼츠의 컴포넌트 56 | 57 | - 스케줄러 : 연관된 트리거가 작동할 때 잡을 실행하는 역할 58 | - `SchedulerFactory`를 통해 가져올 수 있음 59 | - `JobDetails` 및 트리거의 저장소 기능 60 | - 트리거 : 작업 실행 시점 61 | - 트리거가 작동돼 쿼츠에게 잡을 실행하도록 지시하면 잡의 개별 실행을 정의하는 `JobDetails` 객체가 생성됨 62 | - 잡 : 실행할 작업의 단위 63 | 64 | - config 65 | 66 | ```java 67 | //배치 잡을 기동하는 쿼츠 잡 68 | //일정 이벤트가 발생할 때 잡을 실행 69 | public class BatchScheduledJob extends QuartzJobBean { 70 | 71 | @Autowired 72 | private Job job; 73 | 74 | @Autowired 75 | private JobExplorer jobExplorer; 76 | 77 | @Autowired 78 | private JobLauncher jobLauncher; 79 | 80 | @Override 81 | protected void executeInternal(JobExecutionContext context) { 82 | JobParameters jobParameters = new JobParametersBuilder(this.jobExplorer) 83 | .getNextJobParameters(this.job) 84 | .toJobParameters(); 85 | 86 | try { 87 | this.jobLauncher.run(this.job, jobParameters); 88 | } catch (Exception e) { 89 | e.printStackTrace(); 90 | } 91 | } 92 | } 93 | ``` 94 | 95 | ```java 96 | @Configuration 97 | public class QuartzConfig { 98 | 99 | // Trigger: 스케줄과 JobDetail을 연결함 100 | @Bean 101 | public Trigger jobTrigger() { 102 | SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule() 103 | .withIntervalInSeconds(5).withRepeatCount(4); // 5초마다 실행, 4번 반복 (총 5번 실행됨) 104 | 105 | return TriggerBuilder.newTrigger() 106 | .forJob(quartzJobDetail()) 107 | .withSchedule(scheduleBuilder) 108 | .build(); 109 | } 110 | 111 | // JobDetail: 실행할 쿼츠 잡 수행 시에 사용되는 메타데이터 112 | @Bean 113 | public JobDetail quartzJobDetail() { 114 | return JobBuilder.newJob(BatchScheduledJob.class) 115 | .storeDurably() 116 | .build(); 117 | } 118 | } 119 | ``` 120 | 121 | ### 잡 중지하기 122 | 123 | 잡을 종료하는 방법엔 여러가지가 있다 124 | 125 | 1. 자연스럽게 완료되어 종료 126 | 127 | - 모든 스텝이 `COMPLETED` 상태가 되어 잡도 `COMPLETED` 상태가 됨 128 | 129 | 2. 프로그래밍적으로 종료 130 | 131 | - 중지 트랜지션으로 종료 132 | 133 | ```java 134 | @Bean 135 | public Job job() { 136 | return this.jobBuilderFactory.get("job") 137 | .start(step1()) 138 | .on("STOPPED").stopAndRestart(step1()) //재시작시 step1부터 다시시작 139 | .from(step1()) 140 | .on("*").to(step2()) 141 | .from(step2()) 142 | .next(step3()).end() 143 | .build(); 144 | } 145 | 146 | private Step step1() { 147 | return this.stepBuilderFactory.get("step1") 148 | .chunk(100) 149 | .reader(customReader()) 150 | .writer(customWriter()) 151 | .allowStartIfComplete(true) // 잡이 재시작되면 이 스텝이 다시 실행되도록 152 | .listener(customReader()) //customReader의 @AfterStep를 붙인 메서드에서 특정 조건일때 STOPPED를 반환 153 | .build(); 154 | } 155 | 156 | private Step step2() { 157 | ... 158 | } 159 | 160 | private Step step3() { 161 | ... 162 | } 163 | ``` 164 | 165 | - `StepExecution.setTermimateOnly()`를 이용해 종료 (위 예보다 더 효율적, config가 더 깔끔해짐) 166 | 167 | ```java 168 | @Bean 169 | public Job job() { 170 | return this.jobBuilderFactory.get("job") //깔끔해짐 171 | .start(step1()) 172 | .next(step2()) 173 | .next(step3()) 174 | .build(); 175 | } 176 | 177 | private Step step1() { 178 | return this.stepBuilderFactory.get("step1") 179 | .chunk(100) 180 | .reader(customReader()) 181 | .writer(customWriter()) 182 | .allowStartIfComplete(true) 183 | //customReader의 @BeforeStep붙인 메서드에서 StepExecution을 가져와서 필드에 저장하고, 184 | //특정 조건일때 this.stepExecution.setTerminateOnly(); 하면 됨. 185 | .listener(customReader()) 186 | .build(); 187 | } 188 | 189 | private Step step2() { 190 | ... 191 | } 192 | 193 | private Step step3() { 194 | ... 195 | } 196 | ``` 197 | 198 | 3. 오류를 던저 종료 199 | 200 | - 스프링 배치는 예외가 발생하면 기본적으로 스텝 및 잡이 실패한 것으로 간주한다 201 | - `StepExecution`을 사용해 잡을 중지하는 것과 예외를 발생시켜 잡을 중지하는것의 차이 : 잡의 상태 202 | - 전자는 `ExitStatus.STOPPED` 상태로 스텝이 완료된 후 잡 중지 203 | - 후자는 스텝이 완료되지 않음. 스텝과 잡에 `ExitStatus.FAILED` 레이블이 지정됨 204 | - 스텝이 `FAILED`로 식별되면 스텝을 처음부터 다시 실행하는 게 아니라 실패한 지점부터 실행한다 205 | - `[청크1: 아이템1,2,3], [청크2: 아이템4,5,6], [청크3: 아이템7,8,9]`일때, 아이템5에서 예외가 발생한 경우, 아이템4,5는 롤백된다. 재시작하면 청크1은 건너뛰고 청크2부터 다시 시작된다. 206 | 207 | ### 재시작 제어하기 208 | 209 | 스텝1에서 성공적이었다면 스텝2에서 실패하더라도 스텝1을 재시작하고 싶지 않을수도 있다. 210 | 211 | - 잡의 재시작 방지하기 212 | 213 | ```java 214 | @Bean 215 | public Job job() { 216 | return this.jobBuilderFactory.get("job") 217 | .preventRestart() // 잡이 실패하거나 어떤 이유로든 중지된 경우에 다시 실행할 수 없도록 함 218 | .start(step1()) 219 | .next(step2()) 220 | .next(step3()) 221 | .build(); 222 | } 223 | ``` 224 | 225 | 다시 실행하려고 시도하면 `JobRestartException`이 발생함 226 | 227 | - 재시작 횟수를 제한하도록 구성하기 228 | 229 | - step1을 두번만 시도하도록 구성 230 | 231 | ```java 232 | private Step step1() { 233 | return this.stepBuilderFactory.get("step1") 234 | .startLimit(2) // 두번까지만 실행 가능 235 | .chunk(100) 236 | .reader(customReader()) 237 | .writer(customWriter()) 238 | .allowStartIfComplete(true) // 배치 잡 재실행시 수행할 일 결정 239 | .listener(customReader()) 240 | .build(); 241 | } 242 | ``` 243 | 244 | 2번 넘게 실행되면 `StartLimitExceededException` 발생함 245 | 246 | - 완료된 스텝 재실행하기 247 | 248 | - 잡은 동일 파라미터로 성공하면 다시 실행 못한다 249 | - 스텝은 재정의로 이 규칙을 피할 수 있다 250 | - `StepBuilder.allowStartIfComplete()`를 사용하면 된다 -------------------------------------------------------------------------------- /06주차/요약본/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /07주차/발표자료/[7주차]_7장_ItemReader(1)_김성모.md: -------------------------------------------------------------------------------- 1 | # 7장 ItemReader 2 | 3 | ## ItemReader 인터페이스 4 | 5 | ```java 6 | public interface ItemReader { 7 | 8 | @Nullable 9 | T read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException; 10 | 11 | } 12 | ``` 13 | 14 | 스텝에 입력을 제공할 때, `ItemReader` 인터페이스의 `read()` 메소드를 정의한다. 15 | 16 | 스프링 배치는 여러 입력 유형에 맞는 여러 구현체를 제공한다. 17 | 18 | 제공하는 입력 유형은 플랫 파일, 여러 데이터베이스, JMS 리소스, 기타 입력 소스 등 다양하다. 19 | 20 | ## 파일 입력 21 | 22 | 다양한 입력 유형을 처리할 때 어떤 파일 입력을 처리하는 지 방법에 대해 알아보자. 23 | 24 | ### 플랫 파일 25 | 26 | `XML` 과 `JSON` 과는 다르게 데이터 포맷이나 의미를 정의하는 메타데이터가 없는 파일을 `플랫 파일`이라고 한다. 27 | 28 | 스프링 배치에서는 `FlatFileItemReader` 클래스를 사용한다. 29 | 30 | `FlatFileItemReader` 는 메인 컴포넌트 두 개로 이뤄진다. 31 | 32 | - Resource(읽을 대상 파일) 33 | - LineMapper(ResultSet을 객체로 맵핑) 34 | 35 | `LineMapper` 는 인터페이스로 구성되어 여러 구현체가 있는데, 그 중 가장 많이 사용하는 구현체는 `DefaultLineMapper` 이다. 36 | 37 | `DefaultLineMapper` 는 파일에서 읽은 Raw String 을 두 단계 처리를 거쳐 도메인 객체로 변환한다. 38 | 39 | 1. LineTokenizer : 파일의 레코드 한 줄씩 파싱해 `FieldSet` 으로 만든다. 40 | 2. FieldSetMapper : `FieldSet` 을 도메인 객체로 매핑한다. 41 | 42 | #### 고정 너비 파일 43 | 44 | 위에서 말했듯, 파일을 읽어서 레코드를 한 줄씩 파싱 후 도메인 객체에 매핑할 예정이다 예시를 위하여 고객 정보를 담은 resource 파일과 Customer 도메인 객체를 생성하자 45 | 46 | ```text 47 | Aimee CHoover 7341Vel Avenue Mobile AL35928 48 | Jonas UGilbert 8852In St. Saint Paul MN57321 49 | Regan MBaxter 4851Nec Av. Gulfport MS33193 50 | Octavius TJohnson 7418Cum Road Houston TX51507 51 | Sydnee NRobinson 894 Ornare. Ave Olathe KS25606 52 | Stuart KMckenzie 5529Orci Av. Nampa ID18562 53 | Petra ZLara 8401Et St. Georgia GA70323 54 | Cherokee TLara 8516Mauris St. Seattle WA28720 55 | Athena YBurt 4951Mollis Rd. Newark DE41034 56 | Kaitlin MMacias 5715Velit St. Chandler AZ86176 57 | Leroy XCherry 7810Vulputate St. Seattle WA37703 58 | Connor WMontoya 4122Mauris Av. College AK99743 59 | Byron XMedina 7875At Road Rock Springs WY37733 60 | Penelope YSandoval 2643Fringilla Av. College AK99557 61 | Rashad VOchoa 6587Lacus Street Flint MI96640 62 | Jordan UOneil 170 Mattis Ave Bellevue WA44941 63 | Caesar ODaugherty 7483Libero Ave Frankfort KY56493 64 | Wynne DRoth 8086Erat Street Owensboro KY50476 65 | Robin IRoberson 8014Pellentesque Street Casper WY84633 66 | Adrienne CCarpenter 8141Aliquam Avenue Tucson AZ85057 67 | ``` 68 | 69 | ```java 70 | 71 | @Getter 72 | @Setter 73 | public class Customer { 74 | private String firstName; 75 | private String middleInitial; 76 | private String lastName; 77 | private String addressNumber; 78 | private String street; 79 | private String address; 80 | private String city; 81 | private String state; 82 | private String zipCode; 83 | 84 | public Customer() { 85 | } 86 | 87 | public Customer(String firstName, String middleName, String lastName, String addressNumber, String street, String city, String state, String zipCode) { 88 | this.firstName = firstName; 89 | this.middleInitial = middleName; 90 | this.lastName = lastName; 91 | this.addressNumber = addressNumber; 92 | this.street = street; 93 | this.city = city; 94 | this.state = state; 95 | this.zipCode = zipCode; 96 | } 97 | 98 | @Override 99 | public String toString() { 100 | return "Customer{" + 101 | "firstName='" + firstName + '\'' + 102 | ", middleInitial='" + middleInitial + '\'' + 103 | ", lastName='" + lastName + '\'' + 104 | ", address='" + address + '\'' + 105 | ", addressNumber='" + addressNumber + '\'' + 106 | ", street='" + street + '\'' + 107 | ", city='" + city + '\'' + 108 | ", state='" + state + '\'' + 109 | ", zipCode='" + zipCode + '\'' + 110 | '}'; 111 | } 112 | } 113 | ``` 114 | 115 | 읽어들일 파일과 레코드를 읽어 매핑할 객체가 준비가 되었다면 간단한 ItemReader, ItemWriter, Step, Job 을 구성해보자 116 | 117 | ```java 118 | 119 | @Configuration 120 | public class FixedWidthJobConfiguration { 121 | @Autowired 122 | private JobBuilderFactory jobBuilderFactory; 123 | 124 | @Autowired 125 | private StepBuilderFactory stepBuilderFactory; 126 | 127 | @Bean 128 | @StepScope 129 | public FlatFileItemReader fixedWidthItemReader( 130 | @Value("#{jobParameters['customerFile']}") Resource inputFile) { 131 | 132 | return new FlatFileItemReaderBuilder() 133 | .name("fixedWidthItemReader") 134 | .resource(inputFile) 135 | .fixedLength() 136 | .columns(new Range[]{new Range(1, 11), new Range(12, 12), new Range(13, 22), 137 | new Range(23, 26), new Range(27, 46), new Range(47, 62), new Range(63, 64), 138 | new Range(65, 69)}) 139 | .names(new String[]{"firstName", "middleInitial", "lastName", 140 | "addressNumber", "street", "city", "state", "zipCode"}) 141 | .targetType(Customer.class) 142 | .build(); 143 | } 144 | 145 | @Bean 146 | public ItemWriter fixedWidthItemWriter() { 147 | return (items) -> items.forEach(System.out::println); 148 | } 149 | 150 | @Bean 151 | public Step fixedWidthStep() { 152 | return this.stepBuilderFactory.get("fixedWidthStep") 153 | .chunk(10) 154 | .reader(fixedWidthItemReader(null)) 155 | .writer(fixedWidthItemWriter()) 156 | .build(); 157 | } 158 | 159 | @Bean 160 | public Job fixedWidthJob() { 161 | return this.jobBuilderFactory.get("fixedWidthJob") 162 | .start(fixedWidthStep()) 163 | .build(); 164 | } 165 | } 166 | ``` 167 | 168 | #### 구분자로 구분된 파일 169 | 170 | #### 커스텀 레코드 파싱 171 | 172 | #### 여러 가지 레코드 포맷 173 | 174 | #### 여러 줄에 걸친 레코드 175 | 176 | #### 여러 개의 소스 177 | 178 | ### XML 179 | 180 | ### JSON -------------------------------------------------------------------------------- /07주차/발표자료/[7주차]_7장_ItemReader_김민석.md: -------------------------------------------------------------------------------- 1 | # 7장 ItemReader - 1부 (~json) 2 | 3 | 청크 기반 스텝은 ItemReader - ItemProcessor - ItemWriter 로 구성. 4 | 5 | 스프링 배치는 거의 모든 유형의 입력 데이터를 읽을 수 있는 기반 코드를 제공함과 동시에 커스터마이징 할 수 있는 기능도 지원함. 6 | 7 | 7장에서는 ItemReader가 제공하는 다양한 기능을 알아본다. 8 | 9 | ## ItemReader 인터페이스 10 | 11 | ```java 12 | public interface ItemReader { 13 | 14 | @Nullable 15 | T read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException; 16 | 17 | } 18 | ``` 19 | 20 | - 스프링 배치는 처리할 입력 유형에 맞는 ItemReader의 여러 구현체 제공 21 | ![image](https://user-images.githubusercontent.com/6725753/135055056-aaac8995-8e5a-4b0d-98a3-08fdca72d1bb.png) 22 | ![image](https://user-images.githubusercontent.com/6725753/135055276-8d4d2882-cc7c-441a-86eb-79ce775aba7c.png) 23 | 24 | - 스텝 내에서 동작 방식 25 | - 스프링 배치가 read 메서드 호출 26 | - 아이템 한개 반환 27 | - 청크의 개수 만큼 반복 28 | - 청크 개수 만큼 확보되면 ItemProcessor -> ItemWriter로 청크 전달 29 | 30 | 31 | ## 파일 입력 32 | 33 | 스프링 배치는 파일을 고성능 IO 로 읽어 올 수 있는 선언적 방식의 리더를 제공 34 | 35 | ### 플랫 파일 36 | 37 | 플랫 파일 : 구분자가 없는 한개 이상의 레코드로 구성된 텍스트 파일 38 | 39 | FlatFileItemReader 사용. 40 | 41 | ![image](https://user-images.githubusercontent.com/6725753/135234066-acfeeb76-de9d-4373-ae14-7b87e4caeba6.png) 42 | 43 | FlatFileItemReader은 내부적으로 Resource(파일)와 LineMapper(DefaultLineMapper) 구현체로 구성. 44 | 45 | LineMapper는 LineTokenizer와 FieldSetMapper로 구성. 46 | 47 | FlatFileItemReader는 빌더를 사용하여 여러 옵션을 줄 수 있다. 48 | 49 | - 동작방식 50 | - 레코드 한 개에 해당하는 문자열이 LineMapper로 전달 51 | - LineTokenizer가 원시 문자열을 파싱해서 FieldSet으로 만든다 52 | - FieldSetMapper가 FieldSet을 토대로 도메인 객체로 매핑 53 | 54 | 55 | #### 고정 너비 파일 56 | 57 | 고정 너비 파일 : 레코드의 각 항목이 고정된 길이로 표현 58 | 59 | ```text 60 | Aimee CHoover 7341Vel Avenue Mobile AL35928 61 | Jonas UGilbert 8852In St. Saint Paul MN57321 62 | Regan MBaxter 4851Nec Av. Gulfport MS33193 63 | Octavius TJohnson 7418Cum Road Houston TX51507 64 | Sydnee NRobinson 894 Ornare. Ave Olathe KS25606 65 | Stuart KMckenzie 5529Orci Av. Nampa ID18562 66 | ``` 67 | 68 | - LineMapper : DefaultLineMapper 69 | - LineTokenizer : FixedLengthTokenizer 70 | - FieldSetMapper : BeanWrapperFieldSetMapper 71 | 72 | `` 73 | 74 | #### 필드가 구분자로 구분된 파일 75 | 76 | delimited file : 레코드의 각 항목이 특정한 딜리미터로 구분되어 있는 텍스트 파일 77 | 78 | ```text 79 | Aimee,C,Hoover,7341,Vel Avenue,Mobile,AL,35928 80 | Jonas,U,Gilbert,8852,In St.,Saint Paul,MN,57321 81 | Regan,M,Baxter,4851,Nec Av.,Gulfport,MS,33193 82 | Octavius,T,Johnson,7418,Cum Road,Houston,TX,51507 83 | Sydnee,N,Robinson,894,Ornare. Ave,Olathe,KS,25606 84 | Stuart,K,Mckenzie,5529,Orci Av.,Nampa,ID,18562 85 | ``` 86 | 87 | - LineMapper : DefaultLineMapper 88 | - LineTokenizer : DelimitedLineTokenizer 89 | - FieldSetMapper : BeanWrapperFieldSetMapper 90 | 91 | `` 92 | 93 | 건물번호와 거리명을 붙여서 단일 필드로 매핑 94 | 95 | FieldSetMapper의 커스텀 구현체 사용. 96 | 97 | FieldSet에서는 여러 타입의 값을 읽어올 수 있는 다양한 메서드 제공. 98 | (예제에서는 DefaultFieldSet class) 99 | 100 | - LineMapper : DefaultLineMapper 101 | - LineTokenizer : DelimitedLineTokenizer 102 | - FieldSetMapper : Custom 103 | 104 | `` 105 | 106 | #### 커스텀 레코드 파싱 107 | 108 | 건물번호와 거리명을 합칠 때 커스텀 LineTokenizer 구현체 사용. 109 | 110 | ```java 111 | public interface LineTokenizer { 112 | 113 | FieldSet tokenize(String line); 114 | } 115 | ``` 116 | 117 | 커스텀 LineTokenizer는 아래와 같은 경우에도 사용 118 | - 특이한 파일 포맷 파싱 119 | - 서드파티 파일 포맷 파싱 120 | - 특수한 타입 변환 요구 조건 처리 121 | 122 | - LineMapper : DefaultLineMapper 123 | - LineTokenizer : Custom 124 | - FieldSetMapper : BeanWrapperFieldSetMapper 125 | 126 | 127 | `` 128 | 129 | #### 여러 가지 레코드 포맷 130 | 131 | 한 파일 안에 여러 레코드가 섞여 있을 경우 PatternMatchingCompositeLineMapper 사용. 132 | 133 | 기존 DefaultLineMapper는 단일 필드매퍼와 토크나이저를 사용했지만 PatternMatchingCompositeLineMapper는 Map을 통해서 패턴 매칭을 사용하여 다양한 매퍼와 토크나이저 사용 가능. 134 | 135 | ```text 136 | CUST,Warren,Q,Darrow,8272 4th Street,New York,IL,76091 137 | TRANS,1165965,2011-01-22 00:13:29,51.43 138 | CUST,Ann,V,Gates,9247 Infinite Loop Drive,Hollywood,NE,37612 139 | CUST,Erica,I,Jobs,8875 Farnam Street,Aurora,IL,36314 140 | TRANS,8116369,2011-01-21 20:40:52,-14.83 141 | TRANS,8116369,2011-01-21 15:50:17,-45.45 142 | TRANS,8116369,2011-01-21 16:52:46,-74.6 143 | TRANS,8116369,2011-01-22 13:51:05,48.55 144 | TRANS,8116369,2011-01-21 16:51:59,98.53 145 | ``` 146 | 147 | - LineMapper : PatternMatchingCompositeLineMapper 148 | - LineTokenizer(CUST) : DelimitedLineTokenizer 149 | - FieldSetMapper(CUST) : BeanWrapperFieldSetMapper 150 | - LineTokenizer(TRANS) : DelimitedLineTokenizer 151 | - FieldSetMapper(TRANS) : Custom 152 | 153 | `` 154 | 155 | 156 | #### 여러 줄에 걸친 레코드 157 | 158 | 하나의 커스터머에 여러개의 트랜잭션을 가진 도메인 객체로 변환하고 싶을때는? 159 | 160 | - LineMapper : PatternMatchingCompositeLineMapper 161 | - LineTokenizer(CUST) : DelimitedLineTokenizer 162 | - FieldSetMapper(CUST) : BeanWrapperFieldSetMapper 163 | - LineTokenizer(TRANS) : DelimitedLineTokenizer 164 | - FieldSetMapper(TRANS) : Custom 165 | 166 | 167 | `` 168 | 169 | #### 여러 개의 소스 170 | 171 | 여러 개의 파일에서 읽어야 하는 경우라면? 172 | 173 | ![image](https://user-images.githubusercontent.com/6725753/135420475-dd242b57-4c8c-4edd-b1f4-b6330ee74598.png) 174 | 175 | `` 176 | 177 | 178 | ### XML 179 | 180 | 파일 포맷의 차이는 메타데이터의 차이. 181 | 182 | XML은 태그라는 메타데이터를 사용하여 데이터를 완전히 설명 함. 183 | 184 | XML 파서는 대표적으로 DOM과 SAX가 있다. DOM은 한번에 읽어 들이는 방식으로 효율적이지 않기 때문에 이벤트 기반의 SAX 사용. 185 | 186 | 스프링 배치에서는 문서 내 각 섹션을 독립적으로 파싱하는 StAX 파서 사용. 187 | 188 | ```text 189 | StAX(Streaming API for XML)는 XML 문서를 처리하는 자바 API로서, 기존 DOM 및 SAX에 추가된 API이다. 190 | 191 | 기존 XML API는 2가지 방식이었다. 192 | 193 | 트리 기반 : 문서 전체를 트리(Tree) 구조로 메모리로 읽어서 랜덤하게 접근이 가능하다. 194 | 이벤트 기반 : 문서의 한 항목식 이벤트가 발생하여 응용 프로그램에서 처리한다. 195 | 2가지 방식은 각각 보완작용을 한다. 트리 기반(DOM)은 문서의 구조 해석이 가능하고, 이벤트 기반(SAX)은 메모리를 적게 사용하면서 신속한 작동이 가능하다. 196 | 197 | StAX는 이러한 방식의 중간 방식으로 설계되었다. StAX 방식은 프로그램의 동작점, 즉 문서의 한지점을 가리키는 커서가 있는 방식이다. 이러한 이유로 응용 프로그램은 필요에 따라 정보를 추출할 수 있게 된다. (pull 형) 이것이 기존 SAX와 같은 이벤트 기반 API와 다른 점이다. SAX는 파서에서 응용 프로그램으로 데이터를 보내는 방식이다. (push형) 198 | 199 | https://ko.wikipedia.org/wiki/StAX 200 | ``` 201 | 202 | ItemReader 구현체는 StaxEventItemReader 사용 203 | 204 | XML 바인딩은 JAXB 사용. 205 | 206 | `` 207 | 208 | ### JSON 209 | 210 | XML과 방식은 거의 유사. 211 | 212 | 매퍼는 Jackson이나 Gson 사용. 213 | 214 | `` -------------------------------------------------------------------------------- /07주차/발표자료/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /07주차/요약본/[7주차]_7장_ItemReader-1_이호빈.md: -------------------------------------------------------------------------------- 1 | # 7-1장 ItemReader 2 | 3 | - 스프링 배치는 거의 모든 유형의 입력 데이터를 처리할 수 있는 표준 방법을 제공한다 4 | 5 | ### ItemReader 인터페이스 6 | 7 | - 스프링 배치의 ItemReader와 javax(JSR)의 ItemReader는 다르다. 8 | - JSR의 ItemReader는 ItemSteam과 ItemReader를 조합한 것 9 | - ItemReader의 read()를 호출하면 Step 내에서 처리할 Item 한 개를 반환한다. 10 | 11 | ### 파일 입력 12 | 13 | - 플랫파일 14 | - 한 개 이상의 레코드가 포함된 파일. 메타데이터가 없다 15 | - 파일 형태에 여러 경우가 있을 수 있다. 16 | - 고정 너비로 있는 경우, 구분자로 구분된 경우(csv), 여러 줄로 구성된 레코드를 읽는 경우, 다중 파일을 읽는 경우 17 | - FlatFileItemReader 18 | - 읽어들일 대상 파일을 나타내는 Resource, 필드의 묶음을 나타내는 LineMapper 인터페이스가 있다 19 | - LineMapper 구현체인 DefaultLineMapper를 통해서 읽을 수 있다. 20 | - String을 대상으로 도메인 객체로 변환해줄 수 있다 21 | - LineTokenizer로 해당 줄을 파싱하고, FieldSetMapper로 도메인 객체를 매핑한다 22 | - 커스텀 LineTokenizer, 커스텀 FieldSetMapper로 만들어줄 수 있다 23 | - 파싱 후, 원하는 대로 도메인 필드에 넣어줄 수 있다. (e.g. street + address = address 혹은 조건을 추가한다든지 등...) 24 | 25 | > 각각 입력 파일에 대한 구현체들을 제공한다. 26 | > 27 | 28 | ### ItemReader 인터페이스 vs ItemStreamReader 인터페이스 29 | 30 | - ItemReader 31 | - 데이터를 제공하고 다음으로 가게 해주는 인터페이스 32 | - read() 밖에 없다. 33 | - ItemStreamReader 34 | - ItemReader와 ItemStream을 합친 것 35 | - ItemStream 36 | - 에러가 발생했을 때 상태를 ExecutionContext에 저장하거나 ExecutionContext에 있는 상태를 꺼내서 복원하기 위함 37 | - 그래서 open, update, close 등이 있음 38 | 39 | ### XML 40 | 41 | - XML 파일을 입력 받기 위해서 JAXB 라는 구현체를 이용한다. (여러 구현체가 있음) 42 | 43 | [실습 링크](https://github.com/aegis1920/my-lab/tree/master/def-guide-spring-batch) 44 | -------------------------------------------------------------------------------- /07주차/요약본/[7주차]_7장_ItemReader-1_황준호.md: -------------------------------------------------------------------------------- 1 | ## 7장. ItemReader 2 | 3 | ### ItemReader 인터페이스 4 | 5 | ```java 6 | public interface ItemReader { 7 | T read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException; 8 | } 9 | ``` 10 | 11 | - `ItemReader` 인터페이스는 전략 인터페이스다 12 | - 스프링 배치는 플랫 파일, 여러 데이터베이스, JMS 리소스 등등 유형의 구현체를 제공한다 13 | - `ItemReader`나 `ItemReader` 하위 인터페이스를 구현해서 커스텀하게 사용할수도 있다 14 | 15 | ### 파일 입력 16 | 17 | #### 플랫 파일 18 | 19 | - 가장 많이 사용하는 구현체인 `DefaultLineMapper`는 다음 두 개가 처리를 담당한다 20 | 21 | 1. `LineTokenizer` : 파일의 한 줄 -> `FieldSet` 22 | 2. `FieldSetMapper` : `FieldSet` -> 객체 23 | 24 | - 고정 너비 파일 25 | 26 | ```java 27 | @Bean 28 | @StepScope 29 | public FlatFileItemReader customerItemReader(@Value("#{jobParameters['customerFile']})") Resource inputFile) { 30 | return new FlatFileItemReaderBuilder() 31 | .name("customerItemReader") 32 | .resource(inputFile) 33 | .fixedLength() 34 | .columns( 35 | new Range[]{new Range(1, 11), new Range(12, 12), new Range(13, 22), new Range(23, 26), 36 | new Range(27, 46), new Range(47, 62), new Range(63, 64), new Range(65, 69)} 37 | ) 38 | .names( 39 | new String[]{"firstName", "middleInitial", "lastName", "addressNumber", "street", 40 | "city", "state", "zipCode"} 41 | ) 42 | .targetType(Customer.class) 43 | .build(); 44 | } 45 | ``` 46 | 47 | - 필드가 구분자로 구분된 파일 48 | 49 | ```java 50 | @Bean 51 | @StepScope 52 | public FlatFileItemReader customerItemReader( 53 | @Value("#{jobParameters['customerFile']})") Resource inputFile) { 54 | return new FlatFileItemReaderBuilder() 55 | .name("customerItemReader") 56 | .delimited() 57 | .names(new String[]{"firstName", "middleInitial", "lastName", "addressNumber", "street", 58 | "city", "state", "zipCode"}) 59 | .targetType(Customer.class) 60 | .resource(inputFile) 61 | .build(); 62 | } 63 | ``` 64 | 65 | 매핑을 커스텀하게 하고 싶으면? 66 | 67 | ```java 68 | @Bean 69 | @StepScope 70 | public FlatFileItemReader customerItemReader( 71 | @Value("#{jobParameters['customerFile']})") Resource inputFile) { 72 | return new FlatFileItemReaderBuilder() 73 | .name("customerItemReader") 74 | .delimited() 75 | .names(new String[]{"firstName", "middleInitial", "lastName", "addressNumber", "street", 76 | "city", "state", "zipCode"}) 77 | .fieldSetMapper(new CustomFieldSetMapper()) 78 | .resource(inputFile) 79 | .build(); 80 | } 81 | ``` 82 | 83 | - `LineTokenizer`직접 구현 84 | 85 | - `LineTokenizer`을 implements하여 `FlatFileItemReader` config에서 설정한다 86 | 87 | ```java 88 | public interface LineTokenizer { 89 | FieldSet tokenizer(String line); 90 | } 91 | ``` 92 | 93 | - 예 94 | 95 | ```java 96 | @Bean 97 | @StepScope 98 | public FlatFileItemReader customerItemReader( 99 | @Value("#{jobParameters['customerFile']})") Resource inputFile) { 100 | return new FlatFileItemReaderBuilder() 101 | .name("customerItemReader") 102 | .lineTokenizer(new CustomerFileLineTokenizer()) // <-- 103 | .resource(inputFile) 104 | .build(); 105 | } 106 | ``` 107 | 108 | - 여러 형식이 섞여 있는 경우 109 | 110 | - 예 : 접두사로 고객데이터, 거래데이터가 섞여 있는 경우 111 | 112 | ``` 113 | CUST,Warren,Q,Darrow,8272 4th Street,New York,IL,76091 114 | TRANS,1165965,2011-01-22 00:13:29,51.43 115 | CUST,Ann,V,Gates,9247 Infinite Loop Drive,Hollywood,NE,37612 116 | CUST,Erica,I,Jobs,8875 Farnam Street,Aurora,IL,36314 117 | TRANS,8116369,2011-01-21 20:40:52,-14.83 118 | TRANS,8116369,2011-01-21 15:50:17,-45.45 119 | TRANS,8116369,2011-01-21 16:52:46,-74.6 120 | TRANS,8116369,2011-01-22 13:51:05,48.55 121 | TRANS,8116369,2011-01-21 16:51:59,98.53 122 | ``` 123 | 124 | - ```java 125 | @Bean 126 | public PatternMatchingCompositeLineMapper lineTokenizer() { 127 | Map lineTokenizers = new HashMap<>(2); 128 | lineTokenizers.put("CUST*", customerLineTokenizer()); 129 | lineTokenizers.put("TRANS*", transactionLineTokenizer()); 130 | 131 | Map fieldSetMappers = new HashMap<>(2); 132 | BeanWrapperFieldSetMapper customerFieldSetMapper = new BeanWrapperFieldSetMapper<>(); 133 | customerFieldSetMapper.setTargetType(Customer.class); 134 | fieldSetMappers.put("CUST*", customerFieldSetMapper); 135 | fieldSetMappers.put("TRANS*", new TransactionFieldSetMapper()); 136 | 137 | PatternMatchingCompositeLineMapper lineMappers = new PatternMatchingCompositeLineMapper(); 138 | lineMappers.setTokenizers(lineTokenizers); 139 | lineMappers.setFieldSetMappers(fieldSetMappers); 140 | return lineMappers; 141 | } 142 | 143 | @Bean 144 | public DelimitedLineTokenizer transactionLineTokenizer() { 145 | DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer(); 146 | lineTokenizer.setNames("prefix", "accountNumber", "transactionDate", "amount"); 147 | return lineTokenizer; 148 | } 149 | 150 | @Bean 151 | public DelimitedLineTokenizer customerLineTokenizer() { 152 | DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer(); 153 | lineTokenizer.setNames( 154 | "firstName", "middleInitial", "lastName", "address", "city", "state", "zipCode" 155 | ); 156 | lineTokenizer.setIncludedFields(1, 2, 3, 4, 5, 6, 7); 157 | return lineTokenizer; 158 | } 159 | ``` 160 | 161 | ```java 162 | public class TransactionFieldSetMapper implements FieldSetMapper { 163 | public Transaction mapFieldSet(FieldSet fieldSet) { 164 | Transaction trans = new Transaction(); 165 | trans.setAccountNumber(fieldSet.readString("accountNumber")); 166 | trans.setAmount(fieldSet.readDouble("amount")); 167 | trans.setTransactionDate(fieldSet.readDate("transactionDate", "yyyy-MM-dd HH:mm:ss")); 168 | return trans; 169 | } 170 | } 171 | ``` 172 | 173 | - 이후 예제 생략 174 | 175 | #### XML 176 | 177 | - XML파서로는 DOM파서와 SAX파서를 많이 이용한다 178 | - DOM파서는 부하가 커서 배치 처리엔 적합하지 않아 SAX파서를 자주 사용한다 179 | - SAX파서 : 특정 엘리먼트를 만나면 이벤트를 발생시키는 이벤트 기반 파서 180 | - 스프링 배치에서는 StAX파서를 사용한다 181 | - 해야하는 것들 182 | - oxm관련 의존성 추가 183 | - 도메인 객체에 JAXB 애너테이션 추가 184 | - @XmlRootElement, @XmlElementWrapper, @XmlElement ... 185 | - 마샬러 설정 186 | - 아이템리더 설정 187 | 188 | ### JSON 189 | 190 | - JsonItemReader는 3가지 의존성이 필요하다 191 | 192 | 1. 배치를 재시작할때 사용하는 배치의 이름 193 | 2. 파싱에 사용할 `JsonObjectReader` 194 | 3. 읽어들일 리소스 195 | 196 | ```java 197 | @Bean 198 | @StepScope 199 | public JsonItemReader customerFileReader( 200 | @Value("#{jobParameters['customerFile']}") Resource inputFile) { 201 | 202 | ObjectMapper objectMapper = new ObjectMapper(); 203 | ObjectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")); 204 | 205 | JacksonJsonObjectReader jsonObjectReader = new JacksonJsonObjectReader<>(Customer.class); 206 | jsonObjectReader.setMapper(objectMapper); 207 | 208 | return new JsonItemReaderBuilder() 209 | .name("customerFileReader") // <-- 1 210 | .jsonObjectReader(jsonObjectReader) // <-- 2 211 | .resource(inputFile) // <-- 3 212 | .build(); 213 | } 214 | ``` 215 | -------------------------------------------------------------------------------- /07주차/요약본/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /08주차/발표자료/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /08주차/요약본/[8주차]_7장_ItemReader(2)_김성모.md: -------------------------------------------------------------------------------- 1 | # 7장 ItemReader(2) 2 | 3 | ## 데이터베이스 입력 4 | 5 | ### JDBC 6 | 7 | `Cursor` 와 `Paging` 기법을 통해 JDBC에서 수백 만건의 데이터를 모두 적재하지 않고 가져올 수 있다. 8 | 9 | - Cursor 10 | 11 | `RowMapper` 인터페이스를 구현하여 도메인 객체에 매핑시킨 후 `Cursor` 를 열어 스트리밍하게 데이터를 가져올 수 있다. 12 | 13 | ```java 14 | public JdbcCursorItemReader customerItemReader(Datasource datasource){ 15 | return new JdbcCursorItemReaderBuilder() 16 | .name("blah") 17 | .dataSource(dataSource) 18 | .sql("select * from customer") 19 | .rowMapper(new CustomerRowMapper()) 20 | .build(); 21 | } 22 | ``` 23 | 24 | 이제 `JdbcCursorItemReader`의 read 메소드를 호출하게 되면 데이터베이스는 로우 하나를 반환한다. 25 | 26 | - Paging 처리 27 | 28 | `JdbcPagingItemReader` 를 itemReader에서 구현해주면 된다. 29 | 또한 `SqlPaingQueryProviderFactoryBean` 으로 sql 을 작성한 뒤 위의 객체의 세팅값으로 30 | queuryProvider에 주입해주면 된다. 31 | 32 | 33 | ### 하이버네이트 34 | 35 | 자에의 ORM 기술인 하이버네이트를 활용해 모델 객체를 데이터베이스 테이블로 매핑할 수 있다. 36 | 37 | 배치 처리에서 하이버네이트를 사용하기 위해서는 스테이트풀 세션을 주의해야만 한다. (OOM 발생할 수 있음) 38 | 39 | 스프링배치가 제공하는 ItemReader는 이런 점을 해결하도록 개발되었다. 40 | 41 | #### 하이버네이트 커서로 처리하기 42 | 43 | 사전 작업으로는 HibernateBatchConfigurer를 DefaultBatchConfigurer를 상속받아 44 | getTransactionManager()를 오버라이딩 시켜줄 수 있도록 해야한다. 45 | 46 | transactionManager에 쓰일 객체 = new HibernateTransactionManager(); 47 | 48 | 그 후 `HibernateCursorItemReader`를 구현하면 된다. 49 | 50 | 51 | #### 하이버네이트 페이징 기법 52 | 53 | `HibernatePagingItemReader` 를 구현하면 된다. 54 | 55 | ### JPA 56 | 57 | 위의 하이버네이트는 JPA 를 구현한 구현체이다. 58 | 59 | 페이징 처리는 `JpaPagingItemReader` 를 구현하게 되며 위 하이버네이트와 동일한 방식이다. 60 | 61 | ### 저장 프로시저 62 | 63 | 데이터베이스는 목적에 맞춘 저장 프로시저가 포함되어 있을 수 있다. 스프링 배치에서는 `StoredProcudereItemReader` 컴포넌트를 사용하여 구성할 수 있다. 64 | 65 | 프로시저는 데이터베이스에서 만들어놓고 이를 배치에서 사용하게끔 호출한다고 보면 된다. 66 | 67 | `StoredProcedureItemReader` 를 사용하며 프로시저명을 지정해주면 된다. 68 | 69 | ### 스프링 데이터 70 | 71 | #### 몽고 DB 72 | 73 | 몽고 DB는 `MongoItemReader`로 페이지 기반이다. 74 | 75 | #### 스프링데이터 레포지토리 76 | 77 | `RepositoryItemReader`를 활용해 일관된 방식으로 레포지토리를 운용할 수 있다. 78 | 79 | ## 기존 서비스 80 | 81 | 기존 어플리케이션의 코드를 사용하기 위해서는 `ItemReaderAdapter` 를 활용하여 처리할 수 있다 82 | 83 | ## 커스텀 입력 84 | 85 | 위와 같이 여러 제공에도 불구하고 커스텀으로 처리해야하는 상황이 발생할 수 있다. 86 | 이땐 `ItemReader ` 인터페이스를 구현한 Custom ItemReader 를 만들 수 있다. 87 | 88 | 중요 포인트는 `ItemReader` 의 read 메소드를 구현해주기만 하면 된다. 89 | 90 | 만약 리더의 상태를 저장해서 이 전에 종료된 지점부터 리더를 다시 시작하려면 `ItemStream` 91 | 인터페이스를 구현해야한다. `ItemStreamSuport` 라는 추상클래스도 함께 구현할 때 넣고 92 | read 메소드를 구현하게되면 Custom ItemReader 를 만들 수 있다. 93 | 94 | ## 에러 처리 95 | 96 | - 레코드 건너뛰기 : 에러가 발생했을 때 레코드를 건너뛰도록 step에서 설정할 수 있다. 97 | 98 | - 잘못된 레코드 로그 남기기 : ItemListener를 만들어서 `@OnReadError` 를 처리하도록 작업한다. 99 | 100 | - 입력이 없을 때 처리 : `@AfterStep` 을 구현하여 처리한다. -------------------------------------------------------------------------------- /08주차/요약본/[8주차]_7장_ItemReader-2_김광훈.md: -------------------------------------------------------------------------------- 1 | # 7-2장 ItemReader 2 | ## 하이버네이트 3 | #### 배치에서 사용하면 스테이프풀 세션 구현체를 사용 4 | #### 하이버네이트는 캐싱을 하기 때문에 OOM 발생할 수 있음 5 | #### 그래서 스트링 배치의 ItemReader 사용하면 Commit 할 때 세션을 플러시하여 이 문제를 해결해줌 6 | #### 스프링 배치는 DataSourceTransactionManager 를 사용함 7 | #### 그렇기 때문에 만약 하이버네이트를 사용한다면 HibernateTransactionManager 로 Config 수정을 해야함 8 | 9 | ## JPA 10 | #### JpaTransactionManager 를 사용한다. 11 | #### ItemReader 는 커서는 지원하지 않지만 페이징은 지원 12 | #### 쿼리는 JPQL 을 사용하는 것 같음 13 | 14 | ## 스프링 데이터 리포지터리 15 | #### 스프링 데이터 리포지터리는 Repository 추상화를 사용하여 스프링 데이터가 제공하는 인터페이스 중 하나를 상속하는 인터페이스를 정의하면 기본적인 CRUD 작업을 지원해준다. 16 | #### 스프링 배치와 스프링 데이터는 호환성이 좋은데, 그 이유는 스프링 배치가 스프링 데이터의 PagingAndSortingRepository 를 활용하기 때문이다. 17 | #### 예제를 살펴보면 ItemReader 에서 methodName 으로 익숙한 "findByCity" 와 같은 쿼리를 사용할 수 있다. 18 | 19 | ## 예외 처리 20 | #### 레코드 건너뛰기 21 | - skipLimit( ) 으로 레코드 건너뛰기 가능 -> 10회까지 건너뛰기 가능 22 | - noSkip( ) 으로 특정 예외 건너뛸 수 있음 23 | #### 로그 남기기 24 | - ItemListener 를 사용해서 잘못된 레코드를 기록할 수 있음 25 | - ItemListenerSupport 를 사용하고 onReadError 메서드를 오버라이드해서 발생한 에러 기록 26 | - @OnReadError 애노테이션 사용 가능 27 | 28 | -------------------------------------------------------------------------------- /08주차/요약본/[8주차]_7장_ItemReader-2_이호빈.md: -------------------------------------------------------------------------------- 1 | # 7-2장 ItemReader (데이터베이스 입력) 2 | 3 | 대부분의 내용은 [실습 코드](https://github.com/aegis1920/my-lab/tree/master/def-guide-spring-batch)에 있습니다. 4 | 5 | - DB는 배치 처리에서 훌륭한 입력소스다 6 | - DB에 내장된 트랜잭션 7 | - 파일보다 확장성이 좋으며 복구 기능이 좋다 8 | 9 | ## JDBC 10 | 11 | - 스프링이 제공하는 JdbcTemplate을 사용하면 12 | - 전체 ResultSet을 한 row씩 순서대로 가져오면서 필요한 도메인 객체로 변환해 메모리에 저장한다 13 | - 전체 데이터를 한 번에 메모리에 저장하지 않기 위해 스프링 배치는 Cursor와 Paging을 제공한다. 14 | 15 | ### Cursor 16 | 17 | - DB에서 하나의 레코드를 한 번에 가져옴(항상 1 row) 18 | - next()를 호출할 때마다 실행됨. 그리고 다음 레코드로 커서를 옮김 19 | 20 | ### Paging 21 | 22 | - DB에서 Chunk 크기(e.g. 10 row)만큼의 레코드를 한 번에 가져옴 23 | - 고유한 SQL 쿼리를 통해 생성됨 24 | 25 | ### JDBC Cursor 처리 26 | 27 | - JdbcTemplate이 ResultSet을 Domain 객체로 매핑하는데 RowMapper 구현체가 필요하다 28 | - RowMapper 구현체는 Spring Framework Core가 제공하는 JDBC 지원 표준 컴포넌트다 29 | - JdbcCursorItemReader는 ResultSet을 생성하면서 Cursor를 연 다음, 스프링 배치가 read()를 호출할 때마다 Domain 객체로 매핑할 row를 가져온다. 30 | - ResultSet은 ThreadSafe 하지 않으므로 다중 스레드 환경에서는 사용할 수 없다. 31 | 32 | ### JDBC Paging 처리 33 | 34 | - 스프링 배치가 Page라는 청크로 결과 목록을 반환한다 35 | - 한 번에 SQL 쿼리 하나를 실행해 레코드를 하나씩 가져오는 대신, 각 페이지마다 새로운 쿼리를 실행한 뒤 쿼리 결과를 한 번에 메모리에 적재한다?? 36 | - 페이지 크기(반환될 레코드 개수)와 페이지 번호(현재 처리중인 페이지 번호)를 가지고 쿼리를 할 수 있어야 한다. 37 | - PagingQueryProvider 인터페이스의 구현체를 제공해야 한다. DB마다 그 구현체가 다른데 SqlPagingQueryProviderFactoryBean을 사용하면 사용하는 DB가 어떤 건지 감지할 수 있다 38 | - Paging 기법은 각 페이지의 쿼리를 실행할 때마다 동일한 레코드 정렬 순서를 보장하려면 순서가 중요하다. 그래서 sortKey가 있다. 39 | - SqlPagingQueryProviderFactoryBean에서 DataSource를 받는 이유는 이 DataSource를 사용해 DB 타입을 결정하기 때문 40 | 41 | ### Hibernate 42 | 43 | - XML 파일 또는 애너테이션을 사용해서 객체를 데이터베이스 테이블로 매핑하는 구성을 한다 44 | - 그래서 DB 구조를 알지 못해도 객체 구조 기반으로 쿼리를 작성할 수 있다 45 | - 하이버네이트의 기본 세션은 stateful이다. 즉, 아이템 백만 건을 조회하고 사용한다면 해당 아이템들을 모두 캐시에 쌓으면서 OOME가 발생한다. 46 | - 스프링 배치가 제공하는 하이버네이트 기반 ItemReader는 Commit할 때 세션을 Flush해서 배치처리를 잘할 수 있도록 한다. 47 | 48 | ### Hibernate Cursor 처리 49 | 50 | - sessionFactory, Customer 엔티티, HibernateCursorItemReader가 필요하다 51 | - pageSize()만 추가하면 끝 52 | 53 | ### JPA 54 | 55 | - Cursor 기반 방법을 제공하지 않는다. 56 | - 커스텀 BatchConfigurer 구현체를 생성할 필요도 없다. JpaTransactionManger 구성을 알아서 처리하기 때문 57 | 58 | ### 저장 프로시저 59 | 60 | - DB에 저장되는 해당 DB 전용 함수 61 | - StoredProcedureItemReader로 프로시저를 만들어 SQL로 정의된 함수를 실행시켜줄 수 있다 62 | 63 | ### 몽고 DB 64 | 65 | - 기본으로 레플리케이션, 샤딩을 제공한다 66 | - MongoItemReader는 page 기반 ItemReader다 67 | - `spring-boot-starter-data-mongodb` 의존성을 추가한다 68 | - application.yml에 DB 설정 : `spring.data.mongodb.database: tweets` 69 | 70 | ```sql 71 | @Bean 72 | @StepScope 73 | public MongoItemReader tweetsItemReader(MongoOperations mongoTemplate, @Value("#{jobParameters['hashTag']}") String hashtag) { 74 | return new MongoItemReaderBuilder() 75 | .name("tweetsItemReader") 76 | .targetType(Map.class) 77 | .jsonQuery("{ \"entities.hashtags.text\": { $eq: ?0 }}") 78 | .collection("tweets_collection") 79 | .parameterValues(Collections.singletonList(hashtag)) 80 | .pageSize(10) 81 | .sorts(Collections.singletonMap("created_at", Sort.Direction.ASC)) 82 | .template(mongoTemplate) 83 | .build(); 84 | } 85 | 86 | @Bean 87 | public Step copyFileStep() { 88 | return this.stepBuilderFactory.get("copyFileStep") 89 | .chunk(10) 90 | .reader(tweetsItemReader(null, null)) 91 | .writer(itemWriter()) 92 | .build(); 93 | 94 | } 95 | 96 | ``` 97 | 98 | ### Spring Data Repository 99 | 100 | - 일관성을 위해 Repository 추상화를 했다. 101 | - 그래서 인터페이스를 상속하기만 하면 기본적인 CRUD를 할 수 있다 102 | - 스프링 배치와 스프링 데이터가 호환성이 좋은 이유는 둘 다 PagingAndSortingRepository를 활용하기 때문이다 103 | - RepositoryItemReader를 사용하면 어떤 DB 건 해당 DB에 질의할 수 있다 104 | - Pageable은 한 페이지만큼 요청하는데 필요한 파라미터를 캡슐화한다. 105 | 106 | ### 기존 서비스를 이용해 ItemReader에 데이터를 공급하는 법 107 | 108 | - ItemReaderAdapter를 사용한다. 109 | - 매번 호출할 때마다 반환되는 객체는 ItemReader가 반환하는 객체다. 110 | - 만약, 기존 서비스가 어떤 객체의 컬렉션을 반환한다면 단일 아이템으로 만들어야 하므로 직접 컬렉션 내 객체를 하나씩 꺼내면서 처리해야된다. 111 | - 입력 데이터를 모두 처리하면 서비스 메서드는 반드시 null을 반환해야 한다. null을 반환하면 해당 Step의 입력을 모두 소비했음을 알린다 112 | 113 | ### 커스텀 입력 114 | 115 | - 커스텀 ItemReader는 Job을 실행할 때마다 목록의 처음부터 재시작한다. 이는 좋지 않은 게 레코드 100만개 중 50만개만 처리하다 에러가 발생했을 때는 에러가 발생한 Chunk부터 다시 시작하도록 해야한다. 116 | - 이를 구현하려면 ItemStream을 구현해야한다. 117 | - ItemStream 인터페이스 118 | - open() 119 | - 필요한 상태를 초기화한다. (Job을 재시작할 때 이전 상태를 복원한다) 120 | - 복원한다는 건 그만큼 레코드 개수를 건너뛴다는 뜻 121 | - 특정 파일을 열거나 DB 연결을 한다 122 | - ExecutionContext에 접근할 수 있다 123 | - Reader의 현재 상태를 알려준다 124 | - update() 125 | - Job의 상태를 갱신할 때 사용한다. 126 | - ExecutionContext에 접근할 수 있다 127 | - close() 128 | - 리소스를 닫는다 129 | 130 | ### 에러 처리 131 | 132 | - 에러가 발생했을 때 할 수 있는 처리 133 | - 예외를 던져 배치를 멈춤 134 | - 특정 예외가 발생했을 때 레코드를 건너뛰게 할 수 있음 135 | - 몇 번까지 예외를 허용할지도 설정해줄 수 있음 136 | - 리스너로 입력이 없을 때 처리하거나 잘못된 레코드에 로그를 남겨줄 수 있다. 137 | -------------------------------------------------------------------------------- /08주차/요약본/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /09주차/발표자료/CompositeItemProcessor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/book-spring-batch/294c684e3b1895e12b0b15700b9e819fb3693293/09주차/발표자료/CompositeItemProcessor.png -------------------------------------------------------------------------------- /09주차/발표자료/[9주차]_8장_ItemProcessor_김광훈.md: -------------------------------------------------------------------------------- 1 | # 8장 ItemProcessor 2 | ## 1. ItemProcessor 란 ? 3 | #### o ItemProcessor 는 Spring Batch 내에서 입력 데이터를 이용하여 어떤 작업을 수행하는 컴포넌트이다. 4 | #### o ItemReader 로 읽은 데이터를 사용하여 특정 작업을 수행하도록 할 때 사용한다. 5 | #### o ItemProcessor 는 멱등(idempotent) 이여야 한다. 6 | #### o Work Flow 7 | 8 | 9 |
10 | 11 | #### o ItemReader Interface 12 | 13 | 14 | - 사용 방법 15 | ```java 16 | // 람다 사용하지 않은 예제 17 | @Bean 18 | public ItemProcessor processor() { 19 | return new ItemProcessor() { 20 | @Override 21 | public String process(Teacher teacher) throws Exception { 22 | return teacher.getName(); 23 | } 24 | }; 25 | } 26 | 27 | // 람다를 사용한 예제 28 | @Bean 29 | public ItemProcessor processor() { 30 | return teacher -> teacher.getName(); 31 | } 32 | ``` 33 | - 인터페이스에 추상 메소드가 process 하나만 있기 떼문에 위와 같이 람다식을 사용할 수 있다. 34 | 35 | 36 | - 입력 아이템과 리턴 아이템의 타입이 같지 않아도 된다. 37 | - ItemProcessor 가 반환하는 타입은 ItemWriter 가 사용하는 타입이여야 한다. 38 | - ItemProcessor 가 null 을 반환하면 해당 아이템의 이후 모든 처리가 중지된다. 39 | - 즉, null 을 반환하면 ItemWriter 에 전달되지 않는다. 40 | - 공식문서 41 | 42 |
43 | 44 | 45 | 46 |
47 | 48 | #### o ItemReader Interface Implements 49 | 50 | 51 |
52 | 53 | 54 | ## 2. ItemProcessor 사용하기 예제 (ValidatingItemProcessor) 55 | #### o 유효성 검증은 Reader/Writer 가 아닌 Processor 에서 처리하는 것이 좋다. 56 | #### o Validator Interface 57 | 58 | 59 | #### o Validator Interface Implements 60 | 61 | 62 | - Flow: check validator support ---> validate ---> if has error ---> throw exception 63 | 64 | #### o BeanValidatingItemProcessor 65 | 66 | 67 | 68 | #### o Custom Validator 69 | ```java 70 | public class UniqueLastNameValidator extends ItemStreamSupport implements Validator { 71 | 72 | private Set lastNames = new HashSet<>(); 73 | 74 | @Override 75 | public void validate(Customer value) throws ValidationException { 76 | if(lastNames.contains(value.getLastName())) { 77 | throw new ValidationException("Duplicate last name was found: " + value.getLastName()); 78 | } 79 | 80 | this.lastNames.add(value.getLastName()); 81 | } 82 | 83 | @Override 84 | public void update(ExecutionContext executionContext) { 85 | executionContext.put(getExecutionContextKey("lastNames"), this.lastNames); 86 | } 87 | 88 | @Override 89 | public void open(ExecutionContext executionContext) { 90 | String lastNames = getExecutionContextKey("lastNames"); 91 | 92 | if(executionContext.containsKey(lastNames)) { 93 | this.lastNames = (Set) executionContext.get(lastNames); 94 | } 95 | } 96 | } 97 | ``` 98 | 99 |
100 | 101 | 102 | ## 3. ItemProcessorAdapter 103 | #### o ItemProcessorAdapter 를 사용하여 이미 개발된 다양한 서비스가 ItemProcessor 역할을 하도록 만들 수 있다. 104 | #### o 예제 105 | - UpperCaseNameService 106 | ```java 107 | @Service 108 | public class UpperCaseNameService { 109 | 110 | public Customer upperCase(Customer customer) { 111 | Customer newCustomer = new Customer(customer); 112 | 113 | newCustomer.setFirstName(newCustomer.getFirstName().toUpperCase()); 114 | newCustomer.setMiddleInitial(newCustomer.getMiddleInitial().toUpperCase()); 115 | newCustomer.setLastName(newCustomer.getLastName().toUpperCase()); 116 | 117 | return newCustomer; 118 | } 119 | 120 | } 121 | ``` 122 | 123 | - ItemProcessorAdapter 124 | ```java 125 | @Bean 126 | public ItemProcessorAdapter itemProcessor(UpperCaseNameService service) { 127 | ItemProcessorAdapter adapter = new ItemProcessorAdapter<>(); 128 | 129 | adapter.setTargetObject(service); // Instance 지정 130 | adapter.setTargetMethod("upperCase"); // Instance 의 method 지정 131 | 132 | return adapter; 133 | } 134 | ``` 135 | #### o ItemProcessorAdapter 136 | 137 | 138 | 139 | 140 | #### o AbstractMethodInvokingDelegator 141 | 142 | 143 | 144 |
145 | 146 | 147 | #### o AbstractMethodInvokingDelegator extends 148 | 149 | 150 | - ItemProcessor 이외에 Reader, Writer Adapter 에도 사용 151 | 152 |
153 | 154 | ## 4. ScriptItemProcessor 155 | #### o Java Script 를 ItemProcessor 에 사용하여 유연하게 배치 잡에 사용할 수 있다. 156 | #### o 예제 157 | ```java 158 | @Bean 159 | @StepScope 160 | public ScriptItemProcessor itemProcessor(@Value("#{jobParameters['script']}") Resource script) { 161 | ScriptItemProcessor itemProcessor = new ScriptItemProcessor<>(); 162 | 163 | itemProcessor.setScript(script); 164 | 165 | return itemProcessor; 166 | } 167 | ``` 168 | 169 |
170 | 171 | ## 5. CompositeItemProcessor 172 | #### o 스텝 내에서 여러 ItemProcessor 를 체인처럼 연결하여 책임을 분담하는 역할을 한다. 173 | 174 | 175 | #### o 예제 176 | ```java 177 | @Bean 178 | public CompositeItemProcessor itemProcessor() { 179 | CompositeItemProcessor itemProcessor = 180 | new CompositeItemProcessor<>(); 181 | 182 | itemProcessor.setDelegates(Arrays.asList( 183 | customerValidatingItemProcessor(), 184 | upperCaseItemProcessor(null), 185 | lowerCaseItemProcessor(null))); 186 | 187 | return itemProcessor; 188 | } 189 | ``` 190 | - customerValidatingItemProcessor() 191 | - 잘못된 레코드를 필터링하도록 입력 데이터의 유효성 검증을 수행 192 | 193 | 194 | - upperCaseItemProcessor() 195 | - 사용자 이름을 대문자로 변경 196 | 197 | 198 | - lowerCaseItemProcessor() 199 | - 스크립트 파일을 사용하여 address, city, state 피드 값을 소문자로 변경 200 | 201 | 202 |
203 | 204 | ## 6. CustomItemProcessor 205 | #### o ItemProcessor 는 처리할 비즈니스 로직이 담기는 곳이다. 206 | #### o 그러므로 사실상 항상 직접 구현해야 한다. ValidatingItemProcessor, ItemProcessorAdapter 는 사실상 거의 사용하지 않고 CompositeItemProcessor 는 가끔 사용한다. (조졸두님 블로그 참고) 207 | #### o 예제 208 | - 홀수 짝수를 구분하는 로직을 살펴본다. 209 | - 홀수일 때만 긹하고 짝수는 필터링하는 ItemProcessor 를 살펴보자. 210 | ```java 211 | public class EvenFilteringItemProcessor implements ItemProcessor { 212 | 213 | @Override 214 | public Customer process(Customer item) { 215 | return Integer.parseInt(item.getZip()) % 2 == 0 ? null: item; 216 | } 217 | } 218 | ``` 219 | 220 | - ItemProcessor 를 구현하려면 ItemProcessor Interface 를 구현하는 클래스를 만들어야 한다. 221 | - ItemProcessor 가 null 을 반환하기만 하면 해당 아이템이 필터링되도록 함으로써 과정을 단순화한다. 222 | - null 을 던지면 그 이후에 수행되는 ItemProcessor / ItemWriter 에게 전달되지 않는다. -------------------------------------------------------------------------------- /09주차/발표자료/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /09주차/요약본/[9주차]_8장_ItemProcessor_김성모.md: -------------------------------------------------------------------------------- 1 | 2 | # ItemProcessor 3 | 4 | ItemReader에 의해 읽힌 데이터를 필터링하여 ItemWriter가 진행될 수 있도록 하기 위한 단계이다. 5 | 6 | ## ItemProcessor 소개 7 | 8 | 스프링 배치는 읽기, 처리, 쓰기 간 고려해야하는 문제를 여러 부분으로 분리하여 몇 가지 고유 작업을 수행할 수 있도록 하였다. 9 | 10 | - 입력의 유효성 검증 : ValidatingItemReader 사용 11 | - 기존 서비스의 재사용 : ItemProcessorAdapter 제공 12 | - 스크립트 실행 : ScriptItemProcessor 사용 13 | - ItemProcessor 체이닝 14 | 15 | 위의 모든 기능을 제공하는 것은 ItemProcessor 인터페이스이다. 16 | 17 | ## 스프링 배치의 ItemProcessor 사용하기 18 | 19 | ### ValidatingItemProcessor 20 | 21 | 데이터를 읽은 후 비즈니스 규칙에 따른 유효성 검증을 할 때 사용할 수 있다. 22 | 23 | 입력 데이터의 유효성 검증은 스프링 배치 Validator 인터페이스 구현체를 사용할 수 있으며 24 | 검증에 실패한 경우 `ValidationException` 이 발생해 일반적인 스프링 배치 오류 처리가 절차대로 진행된다. 25 | 26 | #### 입력 데이터의 유효성 검증 27 | 객체의 매핑 시 유효성 검증을 위한 방법으로는 JSR303 인 빈 유효성 검증을 위한 자바 사양을 통해 검증되는데 28 | @NotNull, @Pattern, @Size 등의 어노테이션을 속성에 넣어주어 검증 규칙을 정의할 수 있다. 29 | 30 | 이 기능을 동작하기 위하여 검증 메커니즘을 제공해야 하는데, BeanValidatingItemProcessor 가 이 일을 할 것이다. 31 | 32 | 스프링 배치에서 제공하는 기능이 아닌 직접 검증기를 구현해야하는 경우 33 | ValidatingItemProcessor 를 구현하여 직접 만든 34 | ItemStreamSupport를 상속해 ItemStream 인터페이스를 구현함으로써 Validator 인터페이스 구현체를 주입할 것이다. 35 | 36 | 이렇게 만들어진 구현체는 @Bean으로 등록하여 사용한다. 37 | 38 | ### ItemProcessorAdapter 39 | 기존 서비스를 사용해서 검증을 하기 위한 작업을 진행하자. 40 | ItemProcessorAdapter 를 만들어 service를 주입해주고 @Bean으로 만들어준다. 41 | 42 | ### ScriptItemProcessor 43 | 44 | 스크립트 언어는 수정이 용이해서 자주 변경되는 컴포넌트의 경우 유연성을 제공할 수 있다. 45 | 따라서 스프링 배치에서 스크립트를 사용해 유연하게 잡에 주입할 수 있다. 46 | 47 | Resource로 스크립트 파일을 파라미터로 넣어주며 ItemProcessor에서 사용할 스크립트를 정해준다. 48 | 49 | ### CompositeItemProcessor 50 | 51 | 읽은 아이템들을 처리할 대 여러 단계에 걸쳐서 처리할 수 있을텐데 이를 효율적으로 재사용하며 52 | 복합처리를 하기 위한 방법으로 사용한다. 53 | 이는 체이닝을 통해서 이루어지는데 이때 사용하는 것이 `CompositeItemProcessor` 이다. 54 | `.setDelegates()` 함수를 통해 여러 검증 처리기를 너허줄 수 있다. 55 | 56 | 조건에 따라 다른 처리가 진행되어야 할 땐, `Classifier` 에 processor를 주입하고 `classify` 메소드를 57 | 오버라이딩하면 된다. 58 | 59 | 60 | ## ItemProcessor 직접 만들기 61 | 62 | 마찬가지로 ItemProcessor 인터페이스를 상속받은 커스텀 ItemProcessor 를 구현한다. 63 | 이 때 `process()` 메소드를 오버라이딩한다. 64 | 65 | 그 후 @Bean으로 커스텀 아이템 프로세서를 등록해주면 된다. 66 | null 처리로 리턴되는 아이템은 스키핑될 것이며 그렇지 않으면 카운트가 올라가 context에 저장될 것이다. 67 | 68 | -------------------------------------------------------------------------------- /09주차/요약본/[9주차]_8장_ItemProcessor_이호빈.md: -------------------------------------------------------------------------------- 1 | # 8장 ItemProcessor 2 | 3 | - ItemReader로 읽은 데이터를 이용해 어떤 작업을 수행하는 컴포넌트 4 | - 비즈니스 로직을 구현하는 곳 5 | - ItemWriter가 쓰기 처리를 하지 않도록 필터링할 수도 있다 6 | - 입력의 유효성 검증을 할 수 있다. 7 | - 옛날 버전에서는 ItemReader에서 했었다. ValidatingItemReader 클래스를 상속해서 썼어서 입력 방법에 영향을 줬다. 8 | - 지금은 ItemProcessor에서 유효성 검증을 한다. ItemProcessor에서 하면 입력 방법에 상관없이 처리 전에 객체의 유효성 검증을 해줄 수 있다. 9 | - 기존 서비스를 재사용하는 ItemProcessorAdapter를 제공한다. 10 | - 특정 스크립트를 사용할 수도 있다.(ScriptItemProcessor) 11 | - ItemProcessor 체인을 만들 수 있다. 단일 아이템으로 여러 작업을 순서대로 수행할 수 있다. 12 | - 입력 Item의 타입과 반환하는 Item의 타입이 같을 필요가 없다. ItemProcessor가 반환하는 타입은 ItemWriter가 입력으로 사용하는 타입이 된다. 13 | - ItemProcessor가 null을 반환하면 추가적인 여러 ItemProcessor나 ItemWriter는 호출되지 않는다. 14 | - ItemReader는 입력 데이터가 없을 때 null을 반환. ItemProcessor가 null을 반환하면 다른 아이템 처리가 계속 된다. 15 | 16 | ### ValidatingItemProcessor 17 | 18 | - 입력 아이템의 유효성 검증을 수행하는 Validator 인터페이스 구현체를 사용할 수 있다. 19 | - 유효성 검증에 실패하면 ValidationException이 발생한다. 20 | - NotNull, Alphabetic, Numeric, Size 등등... 21 | - 검증 애너테이션을 적용하려면 spring-boot-starter-validation이라는 새로운 스타터를 사용해야 한다. 22 | - 유효성 검증 도구의 하이버네이트 구현체를 가지고 온다. 23 | 24 | ### ItemProcessorAdapter 25 | 26 | - 기존 서비스를 ItemProcessor로 사용할 수 있다 27 | 28 | ### ScriptItemProcessor 29 | 30 | - ScriptItemProcessor를 통해 JS 같은 스크립트 파일을 주입해줄 수 있다. 31 | 32 | ### CompositeItemProcessor 33 | 34 | - 단일 ItemProcessor에 몰아두는 것이 아니라 ItemProcessor를 체인처럼 연결해서 책임을 분담시킬 수 있다. 35 | - ItemProcessor와 마찬가지로 null을 반환하면 해당 Item은 더이상 처리되지 않는다. 36 | - 유효성 검증을 통과하지 못한 아이템을 걸러내기만 하도록 ItemProcessor를 구성할 수 있다. 37 | - 일부 아이템은 ItemProcessorA에게 전달하고 일부 아이템은 ItemProcessorB에게 전달하고 싶다면? → ClassifierCompositeItemProcessor를 쓰면 된다. 38 | 39 | ### 커스텀 ItemProcessor 구현 40 | 41 | - 아이템을 필터링하는 커스텀 ItemProcessor 구현 42 | - 읽은 얘들만 JobRepository에 기록한다. 43 | 44 | ```jsx 45 | // 이렇게 구현 후 46 | public class EvenFilteringItemProcessor implements ItemProcessor { 47 | @Override 48 | public Customer process(Customer item) { 49 | return Integer.parseInt(item.getZip()) % 2 == 0 ? null : item; 50 | } 51 | } 52 | 53 | // Bean으로 등록 54 | public EvenFilteringProcessor itemProcessor() { 55 | return new EvenFilteringItemProcessor(); 56 | } 57 | ``` 58 | 59 | [실습 코드](https://github.com/aegis1920/my-lab/tree/master/def-guide-spring-batch) 60 | -------------------------------------------------------------------------------- /09주차/요약본/[9주차]_8장_ItemProcessor_황준호.md: -------------------------------------------------------------------------------- 1 | ## 8장. ItemProcessor 2 | 3 | ### ItemProcessor 소개 4 | 5 | ```java 6 | public interface ItemProcessor { 7 | O process(I item) throws Exception; 8 | } 9 | ``` 10 | 11 | - `ItemProcessor`가 반환하는 타입(`O`)는 `ItemWriter`의 입력 타입 12 | - `process()`가 `null`을 반환하면 해당 아이템의 이후 모든 처리가 중지된다 13 | - 스텝 내에서 해당 아이템의 이후 `ItemProcessor`들과 `ItemWriter`는 호출되지 않는다 14 | 15 | ### 스프링 배치가 제공하는 ItemProcessor 들 16 | 17 | - `ValidatingItemProcessor` : 읽어온 데이터의 유효성 검증에 사용한다 18 | 19 | - 애너테이션을 이용해서 검증할 수 있다 20 | - `@NotNull`, `@Pattern`, `@Size` 등 21 | - 유효성 검증 기능은 `Validator`의 구현체가 제공하며, 이 인터페이스는 `void validate(T value);` 를 제공한다 22 | - 검증에 실패하면 `ValidationException`을 발생시킨다 23 | 24 | ```java 25 | @Bean 26 | public Step step() { 27 | return this.stepBuilderFactory.get("step") 28 | .chunk(5) 29 | .reader(...) 30 | .processor(customerValidatingItemProcessor()) 31 | .writer(...) 32 | .build(); 33 | } 34 | 35 | @Bean 36 | public BeanValidatingItemProcessor customerValidatingItemProcessor() { 37 | return new BeanValidatingItemProcessor<>(); 38 | } 39 | ``` 40 | 41 | - 만약 검증 로직을 직접 작성하고 싶다면? (예 : lastName이 유일해야 할 때) 42 | 43 | ```java 44 | public class UniqueLastNameValidator extends ItemStreamSupport implements Validator { 45 | 46 | private Set lastNames = new HashSet<>(); 47 | 48 | @Override 49 | public void validate(Customer value) throws ValidationException { 50 | if (lastNames.contains(value.getLastName())) { 51 | throw new ValidationException("Duplicate last name was found: " + value.getLastName()); 52 | } 53 | this.lastNames.add(value.getLastName()); 54 | } 55 | 56 | // Execution 간에 상태를 유지하는 데 사용 57 | @Override 58 | public void update(ExecutionContext executionContext) { 59 | executionContext.put(getExecutionContextKey("lastNames"), this.lastNames); 60 | } 61 | 62 | // Execution 간에 상태를 유지하는 데 사용 63 | @Override 64 | public void open(ExecutionContext executionContext) { 65 | String lastNames = getExecutionContextKey("lastNames"); 66 | if (executionContext.containsKey(lastNames)) { 67 | this.lastNames = (Set) executionContext.get(lastNames); 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | ```java 74 | @Bean 75 | public Step step() { 76 | return this.stepBuilderFactory.get("step") 77 | .chunk(5) 78 | .reader(...) 79 | .processor(customerValidatingItemProcessor()) 80 | .writer(...) 81 | .stream(validator()) // <-- 82 | .build(); 83 | } 84 | 85 | // BeanValidatingItemProcessor -> ValidatingItemProcessor 86 | @Bean 87 | public ValidatingItemProcessor customerValidatingItemProcessor() { 88 | return new ValidatingItemProcessor<>(validator()); 89 | } 90 | 91 | @Bean 92 | public UniqueLastNameValidator validator() { 93 | UniqueLastNameValidator uniqueLastNameValidator = new UniqueLastNameValidator(); 94 | uniqueLastNameValidator.setName("validator"); 95 | return uniqueLastNameValidator; 96 | } 97 | ``` 98 | 99 | - `ItemProcessorAdapter` : 기존 서비스를 아이템 프로세서로 만들 수 있다 100 | 101 | - 예 : 이름을 대문자로 만드는 `UpperCaseNameService.upperCase(Customer customer)` 가 이미 있을때 102 | 103 | ```java 104 | @Bean 105 | public Step step() { 106 | return this.stepBuilderFactory.get("step") 107 | .chunk(5) 108 | .reader(...) 109 | .processor(itemProcessor(null)) 110 | .writer(...) 111 | .build(); 112 | } 113 | 114 | @Bean 115 | public ItemProcessorAdapter itemProcessor(UpperCaseNameService service) { 116 | ItemProcessorAdapter adapter = new ItemProcessorAdapter<>(); 117 | adapter.getTargetObject(service); 118 | adapter.getTargetMethod("upperCase"); 119 | return adapter; 120 | } 121 | ``` 122 | 123 | - `ScriptItemProcessor` : 스크립트로 프로세서를 작성할 수 있다 124 | 125 | - 예 : 이름을 대문자로 만들기 (자바스크립트) 126 | 127 | ```java 128 | @Bean 129 | @StepScope 130 | public ScriptItemProcessor itemProcessor( 131 | @Value("#{jobParameters['script']}") Resource script 132 | ) { 133 | ScriptItemProcessor itemProcessor = new ScriptItemProcessor<>(); 134 | itemProcessor.setScript(script); 135 | return itemProcessor; 136 | } 137 | ``` 138 | 139 | 이후, 잡을 실행할때 파라미터로 js파일의 위치를 입력하면 됨 140 | 141 | - `CompositeItemProcessor` : 여러 프로세서를 체인처럼 연결할 수 있다 142 | 143 | - 이전 프로세서가 반환한 타입과 다음 프로세서의 입력 타입이 같아야 한다 144 | - 한 프로세서가 null을 반환하면 해당 아이템은 더이상 처리되지 않는다 145 | 146 | ```java 147 | @Bean 148 | public CompositeItemProcessor itemProcessor() { 149 | CompositeItemProcessor itemProcessor = new CompositeItemProcessor(); 150 | itemProcessor.setDelegates(Arrays.asList( 151 | itemProcessor1(), itemProcessor2(), itemProcessor3() 152 | )); 153 | return itemProcessor; 154 | } 155 | ``` 156 | 157 | - `ClassifierCompositeItemProcessor` : 아이템마다 전달되는 itemProcessor를 다르게 하고 싶을때 158 | 159 | - 예 : 우편번호가 홀수인지, 짝수인지에 따라 전달되는 itemProcessor 다르게하기 160 | 161 | ```java 162 | @AllArgsConstructor 163 | public class ZipCodeClassifier implements Classifier> { 164 | 165 | private ItemProcessor oddItemProcessor; 166 | private ItemProcessor evenItemProcessor; 167 | 168 | @Override 169 | public ItemProcessor classify(Customer classifiable) { 170 | if (Integer.parseInt(classifiable.getZipCode()) % 2 == 0) { 171 | return evenItemProcessor; 172 | } else { 173 | return oddItemProcessor; 174 | } 175 | } 176 | } 177 | ``` 178 | 179 | ```java 180 | @Bean 181 | public ClassifierCompositeItemProcessor itemProcessor() { 182 | ClassifierCompositeItemProcessor itemProcessor 183 | = new ClassifierCompositeItemProcessor<>(); 184 | itemProcessor.setClassifier(classifier()); 185 | return itemProcessor; 186 | } 187 | 188 | @Bean 189 | public Classifier classifier() { 190 | return new ZipCodeClassifier(upperCaseItemProcessor(null), lowerCaseItemProcessor(null)); 191 | } 192 | ``` 193 | 194 | ### ItemProcessor 직접 만들기 195 | 196 | - `ItemProcessor`가 null을 반환하면 해당 아이템을 필터링한다 197 | 198 | - 스프링 배치는 필터링된 레코드수를 보관하고 `JobRepository`에 저장한다 199 | 200 | - 아래 예시는 우편번호가 짝수일때 건너뛰는 프로세서를 직접 만든 예시. 201 | 202 | ```java 203 | public class EvenFilteringItemProcessor implements ItemProcessor { 204 | @Override 205 | public Customer process(Customer item) throws Exception { 206 | return Integer.parseInt(item.getZipCode()) % 2 == 0 ? null : item; 207 | } 208 | } 209 | ``` 210 | 211 | ```java 212 | @Bean 213 | public EvenFilteringItemProcessor itemProcessor() { 214 | return new EvenFilteringItemProcessor(); 215 | } 216 | ``` 217 | 218 | - 다음 쿼리로 필터링된 아이템 수를 알 수 있다 219 | 220 | ```sql 221 | select STEP_EXECUTION_ID as ID, STEP_NAME, STATUS, COMMIT_COUNT, READ_COUNT, FILTER_COUNT, WRITE_COUNT 222 | from BATCH_STEP_EXECUTION; 223 | ``` 224 | -------------------------------------------------------------------------------- /09주차/요약본/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /10주차/발표자료/[10주차]_9장_ItemWriter_김성모.md: -------------------------------------------------------------------------------- 1 | # ItemWriter 2 | 3 | ItemReader에 의해 읽힌 데이터를 필터링하여 ItemWriter가 진행될 수 있도록 하기 위한 단계이다. 4 | 5 | ## ItemWriter 소개 6 | 7 | 청크 기반으로 묶음 단위로 아이템을 처리한다. ItemReader와 ItemProcessor가 아이템을 건건히 처리하며 청크 단위에 도달하면 ItemWriter가 처리한다. 8 | ![처리프로세스](https://user-images.githubusercontent.com/48056463/139166357-c7424f2d-02cd-4885-9afb-cc52860acd02.png) 9 | 10 | 청크 기반 처리 방식 시 파일 쓰기와 같은 트랜잭션이 적용되지 않는 리소스를 처리할 때 이미 쓰여진 내용을 롤백할 수 있는 방법이 없다. 하지만 스프링배치는 추가적인 보호조치에 대한 조치 취해진 ItemWriter를 11 | 제공해준다. 12 | 13 | ## 파일기반 ItemWriter 14 | 15 | ### FlatFileItemWriter 16 | 17 | 텍스트 파일로 출력을 만들 때 사용한다. 18 | 19 | FlatFileItemWriter 는 출력할 리소스(`Resource`) 와 `LineAggregator` 구현체로 구성된다. 20 | `LineAggregator`는 객체를 기반으로 출력 문자열을 생성하는 역할을 한다. 21 | 22 | 파일에 트랜잭션을 적용한 후 롤백 처리를 위하여 `FlatFileItemWriter` 는 롤백이 가능한 커밋 전 마지막 순간까지 출력 데이터의 저장을 지연시킨다. 23 | 24 | 이 기능은 `TransactionSynchronizationApter`의 `beforeCommit` 메서드로 구현되어 있다. 25 | 26 | 27 | code 링크 28 | 29 | ### StaxEventItemWriter 30 | 31 | XML 파일로 출력을 만들 때 사용한다. 32 | 33 | 의존성 추가 34 | ``` 35 | implementation 'com.thoughtworks.xstream:xstream:1.4.18' 36 | implementation 'org.springframework:spring-oxm:5.3.11' 37 | ``` 38 | 39 | code 링크 40 | 41 | ### 데이터베이스기반 ItemWriter 42 | 43 | 위에서 봤던 파일과는 다르게 트랜잭션이 적용되는 리소스다. 44 | 45 | #### JdbcBatchItemWriter 46 | 47 | > 하나의 청크에 대한 배치업데이트 기능을 사용한다. 48 | 49 | code 링크 50 | 51 | #### HibernateItemWriter 52 | 53 | 의존성 추가 54 | ``` 55 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 56 | ``` 57 | 58 | #### JpaItemWriter 59 | 60 | ### 스프링 데이터의 ItemWriter 61 | 62 | -------------------------------------------------------------------------------- /10주차/발표자료/[10주차]_9장_ItemWriter_황준호.md: -------------------------------------------------------------------------------- 1 | ## 9-1장. ItemWriter 2 | 3 | ### ItemWriter 소개 4 | 5 | - `ItemWriter`는 `ItemReader`와는 달리 아이템을 건건이 쓰지 않는다. 그래서 `ItemReader`와는 달리 리스트를 받는다 6 | 7 | ```java 8 | public interface ItemWriter { 9 | void write(List items) throws Exception; 10 | } 11 | ``` 12 | 13 | - 14 | 15 | ![chunk-oriented-processing-with-item-processor](https://docs.spring.io/spring-batch/docs/current/reference/html/images/chunk-oriented-processing-with-item-processor.png) 16 | 17 | ### 파일 기반 ItemWriter 18 | 19 | - `FlatFileItemWriter` 20 | 21 | - 구성 요소 : `Resource`와 `LineAggregator`구현체 22 | 23 | - `Resource` : 출력할 리소스 24 | - `LineAggregator`구현체 : Reader의 `LineMapper`에 해당함. (객체 -> 문자열) 25 | 26 | - 트랜잭션이 작동하는 방식 27 | 28 | - 트랜잭션 주기 내에서 실제 쓰기 작업을 가능한 한 늦게 수행하도록 설계됨 29 | - 쓰기 외 모든 작업을 완료한 뒤 디스크에 쓰기 직전에 커밋한다 (한번 쓰면 롤백할 수 없으니까) 30 | 31 | - 형식화된 텍스트 파일로 쓰기 (고객 정보 csv 파일을 읽어서 `"%s %s lives at %s %s in %s, %s."` 형식으로 쓰기) 32 | 33 | ```java 34 | // itemReader 부분은 생략 35 | 36 | @Bean 37 | public FlatFileItemWriter formattedItemWriter() { 38 | return new FlatFileItemWriterBuilder() 39 | .name("customerItemWriter") 40 | .resource(...) //출력될 리소스 41 | .formatted() // FormatterLineAggregator를 사용할 수 있도록 하는 별도의 빌더를 리턴해줌 42 | .format("%s %s lives at %s %s in %s, %s.") 43 | .names("firstName", "lastName", "address", "city", "state", "zip") 44 | .build(); 45 | } 46 | ``` 47 | 48 | - 구분자로 구분된 파일로 쓰기 (고객 정보 csv 파일을 읽어서 구분자를 `,`에서 `;`으로 바꾸기) 49 | 50 | ```java 51 | @Bean 52 | public FlatFileItemWriter delimitedItemWriter() { 53 | return new FlatFileItemWriterBuilder() 54 | .name("customerItemWriter") 55 | .resource(...) //출력될 리소스 56 | .delimited() // DelimitedLineAggregator를 사용할 수 있도록 하는 별도의 빌더를 리턴해줌 57 | .delimiter(";") 58 | .names("zip", "state", "city", "address", "lastName", "firstName") 59 | .build(); 60 | } 61 | ``` 62 | 63 | - `FlatFileItemWriter`의 여러가지 고급 옵션 64 | 65 | - `shouldDeleteIfEmpty` 66 | - false(기본값) : 스텝이 완료됐을때 아무 아이템도 안써졌으면 빈 파일로 남음 67 | - true : 스텝이 완료됐을때 아무 아이템도 안써졌으면 파일 삭제함 68 | - `shouldDeleteIfExist` 69 | - true(기본값) : 이름이 같은 파일이 있으면 삭제하고 새로 만듦 70 | - false : 이름이 같은 파일이 있으면 `ItemStreamException` 발생. 실행 결과를 매번 보호하고 싶을때 사용 71 | - `append` 72 | - false(기본값) 73 | - true : `shouldDeleteIfExist`을 자동으로 false로 설정함. 여러 스텝이 하나의 출력 파일에 쓸때 유용함 74 | - 결과 파일이 존재하지 않는 경우 : 새 파일 생성 75 | - 결과 파일이 존재하는 경우 : 기존 파일에 데이터 추가 76 | 77 | - `StaxEventItemWriter` 78 | 79 | - `FlatFileItemWriter`와 동일하게 한 번에 청크 단위호 XML을 생성하며 로컬 트랜잭션이 커밋되기 직전에 파일에 쓴다 80 | 81 | ``` 82 | // build.gradle 83 | implementation 'org.springframework:spring-oxm' 84 | implementation 'com.thoughtworks.xstream:xstream:1.4.10' 85 | ``` 86 | 87 | ```java 88 | @Bean 89 | public StaxEventItemWriter xmlCustomerWriter() { 90 | Map> aliases = new HashMap<>(); 91 | aliases.put("customer", Customer.class); 92 | 93 | XStreamMarshaller marshaller = new XStreamMarshaller(); 94 | marshaller.setAliases(aliases); 95 | 96 | return new StaxEventItemWriterBuilder() 97 | .name("xmlOutputWriter") 98 | .resource(outputResource) // 기록할 리소스 99 | .marshaller(marshaller) // 마샬러(객체 -> xml)의 구현체 100 | .rootTagName("customers") // 마샬러가 생성할 각 xml 프래그먼트의 루트 태그 이름 101 | .build(); 102 | } 103 | ``` 104 | 105 | ### 데이터베이스 기반 ItemWriter 106 | 107 | - `JdbcBatchItemWriter` 108 | 109 | - `JdbcTemplate`를 사용하며, `JdbcTemplate`의 배치 SQL 실행 기능을 사용해 한 번에 청크 하나에 대한 모든 SQL을 실행한다. 110 | 111 | ```java 112 | @Bean 113 | @StepScope 114 | public JdbcBatchItemWriter jdbcCustomerWriter(DataSource dataSource) throws Exception { 115 | return new JdbcBatchItemWriterBuilder() 116 | .dataSource(dataSource) 117 | .sql("INSERT INTO CUSTOMER (first_name, middle_name, last_name, address, city, state, zip_code) VALUES (?, ?, ?, ?, ?, ?, ?)") 118 | .itemPreparedStatementSetter(new CustomerItemPreparedStatementSetter()) 119 | .build(); 120 | } 121 | ``` 122 | 123 | ```java 124 | public class CustomerItemPreparedStatementSetter implements ItemPreparedStatementSetter { 125 | 126 | @Override 127 | public void setValues(Customer item, PreparedStatement ps) throws SQLException { 128 | ps.setString(1, item.getFirstName()); 129 | ps.setString(2, item.getMiddleInitial()); 130 | ps.setString(3, item.getLastName()); 131 | ps.setString(4, item.getAddress()); 132 | ps.setString(5, item.getCity()); 133 | ps.setString(6, item.getState()); 134 | ps.setString(7, item.getZipCode()); 135 | } 136 | } 137 | ``` 138 | 139 | - PreparedStatement말고 네임드 파라미터 접근법을 사용할수도 있다 140 | 141 | - `ItemPreparedStatementSetter` 의 구현체가 아닌 `ItemSqlParameterSourceProvider`의 구현체를 사용한다 142 | 143 | ```java 144 | @Bean 145 | public JdbcBatchItemWriter jdbcCustomerWriter(DataSource dataSource) { 146 | return new JdbcBatchItemWriterBuilder() 147 | .dataSource(dataSource) 148 | .sql("INSERT INTO CUSTOMER (first_name, middle_initial, last_name, address, city, state, zip) " 149 | + "VALUES (:firstName, :middleInitial, :lastName, :address, :city, :state, :zip)") 150 | .beanMapped() // 이걸 사용하면 아이템에서 값을 추출하는 작업을 하지 않아도 된다 151 | .build(); 152 | } 153 | ``` 154 | 155 | - `HibernateItemWriter` 156 | 157 | - 설정해야 하는 것들 158 | 159 | 1. 하이버네이트 의존성 포함 160 | 161 | ``` 162 | // build.gradle 163 | 164 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 165 | ``` 166 | 167 | 2. `CurrentSessionContext` 구성 168 | 169 | ```yaml 170 | # application.yml 171 | 172 | spring: 173 | jpa: 174 | properties: 175 | hibernate: 176 | current_session_context_class: org.springframework.orm.hibernate5.SpringSessionContext 177 | ``` 178 | 179 | 3. `Customer` 객체 매핑(`@Id`, `@Entity` 등 이용) 180 | 181 | 4. 하이버네이트를 지원하도록 `SessionFactory`와 새로운 `TransactionManager` 둘 다 구성 182 | 183 | - 일반적으로 제공되는 `DataSourceTransactionManager` 대신 `HibernateTransactionManager`를 제공하도록 구성 184 | 185 | 5. `HibernateItemWriter` 구성 186 | 187 | ```java 188 | @Bean 189 | public HibernateItemWriter hibernateItemWriter(EntityManagerFactory entityManager) { 190 | return new HibernateItemWriterBuilder() 191 | // SessionFactory에 대한 참조는 필수 의존성 192 | .sessionFactory(entityManager.unwrap(SessionFactory.class)) 193 | .build(); 194 | } 195 | ``` 196 | 197 | - `JpaItemWriter` 198 | 199 | - `JpaItemWriter`는 모든 아이템을 저장한 뒤 flush를 호출하기 전에 아이템 목록을 순회하면서 아이템마다 `EntityManager.merge()`를 호출한다 200 | 201 | - 설정해야 하는 것들 202 | 203 | 1. `JpaTransactionManager`를 생성하는 `BatchConfigurer` 구현체 작성 204 | 205 | - 이전 `HibernateBatchConfigurer`와의 차이점 206 | 1. 생성자에서 `SessionFactory`대신 `EntityManager`를 저장한다 207 | 2. initialize 메소드에서 `HibernateTransactionManager`대신 `JpaTransactionManager`를 생성한다 208 | 209 | 2. `JpaItemWriter` 구성 210 | 211 | ```java 212 | @Bean 213 | public JpaItemWriter jpaItemWriter(EntityManagerFactory entityManagerFactory) { 214 | JpaItemWriter jpaItemWriter = new JpaItemWriter<>(); 215 | jpaItemWriter.setEntityManagerFactory(entityManagerFactory); 216 | return jpaItemWriter; 217 | } 218 | ``` 219 | 220 | ### 스프링 데이터의 ItemWriter 221 | 222 | - 몽고DB 223 | 224 | - 설정해야 하는 것 225 | 226 | 1. Customer 매핑 227 | 228 | - id를 String으로 바꾸기(id에 long 사용불가) 229 | 230 | 2. 의존성 추가 `implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'` 231 | 232 | 3. `application.yml`에서 몽고DB 설정 233 | 234 | 4. `MongoItemWriter` 작성 235 | 236 | ```java 237 | @Bean 238 | public MongoItemWriter mongoItemWriter() 240 | .collection("customers") // 데이터베이스의 컬렉션 이름 241 | .template(mongoTemplate) // 242 | // delete 플래그도 있음 (true:매칭되는 아이템 삭제, false:매칭되는 아이템 저장(default)) 243 | .build(); 244 | } 245 | ``` 246 | 247 | - 몽고DB가 트랜잭션을 지원하지 않기 때문에 커밋 직전까지 쓰기를 버퍼링하고 마지막 순간에 쓰기 작업을 수행한다 248 | 249 | - 네오4j - 생략 250 | 251 | - 피보탈 젬파이어와 아파치 지오드 - 생략 252 | 253 | - 리포지터리 254 | 255 | - 슈퍼 인터페이스인 `CrudRepository`를 사용한다 256 | 257 | - 설정해야 하는 것들 258 | 259 | 1. 의존성 추가 260 | 261 | 2. 리포지터리 기능을 부트스트랩 하기 위해 스프링에게 `Repository`를 어디서 찾아야 하는지 알려줘야 한다 262 | 263 | ```java 264 | @Configuration 265 | @EnableJpaRepositories(basePackageClasses = Customer.class) // <-- 266 | public class RepositoryJobConfig { 267 | ... 268 | } 269 | ``` 270 | 271 | 3. `RepositoryItemWriter` 작성 272 | 273 | ```java 274 | @Bean 275 | public RepositoryItemWriter repositoryItemWriter(CustomerRepository repository) { 276 | return new RepositoryItemWriterBuilder() 277 | .repository(repository) 278 | .methodName("save") 279 | .build(); 280 | } 281 | ``` 282 | 283 | ### -------------------------------------------------------------------------------- /10주차/발표자료/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /10주차/요약본/[10주차]_9장_ItemWriter_김민석.md: -------------------------------------------------------------------------------- 1 | # 9장 ItemWriter 2 | 3 | ## 파일 기반 4 | 5 | ```java 6 | @Bean 7 | @StepScope 8 | public FlatFileItemWriter customerItemWriter( 9 | @Value("#{jobParameters['outputFile']}") Resource outputFile) { 10 | 11 | return new FlatFileItemWriterBuilder() 12 | .name("customerItemWriter") 13 | .resource(outputFile) 14 | .formatted() 15 | .format("%s %s lives at %s %s in %s, %s.") 16 | .names(new String[] {"firstName", "lastName", "address", "city", "state", "zip"}) 17 | .build(); 18 | } 19 | ``` 20 | 21 | ```java 22 | @Bean 23 | @StepScope 24 | public FlatFileItemReader customerFileReader( 25 | @Value("#{jobParameters['customerFile']}")Resource inputFile) { 26 | 27 | return new FlatFileItemReaderBuilder() 28 | .name("customerFileReader") 29 | .resource(inputFile) 30 | .delimited() 31 | .names(new String[] {"firstName", 32 | "middleInitial", 33 | "lastName", 34 | "address", 35 | "city", 36 | "state", 37 | "zip"}) 38 | .targetType(Customer.class) 39 | .build(); 40 | } 41 | ``` 42 | 43 | ## XML 44 | 45 | ```java 46 | Bean 47 | @StepScope 48 | public StaxEventItemWriter xmlCustomerWriter( 49 | @Value("#{jobParameters['outputFile']}") Resource outputFile) { 50 | 51 | Map aliases = new HashMap<>(); 52 | aliases.put("customer", Customer.class); 53 | 54 | XStreamMarshaller marshaller = new XStreamMarshaller(); 55 | 56 | marshaller.setAliases(aliases); 57 | 58 | marshaller.afterPropertiesSet(); 59 | 60 | return new StaxEventItemWriterBuilder() 61 | .name("customerItemWriter") 62 | .resource(outputFile) 63 | .marshaller(marshaller) 64 | .rootTagName("customers") 65 | .build(); 66 | } 67 | ``` 68 | 69 | ## 데이터베이스 기반 70 | 71 | ### JDBC 72 | 73 | ```java 74 | @Bean 75 | public JdbcBatchItemWriter jdbcCustomerWriter(DataSource dataSource) throws Exception { 76 | return new JdbcBatchItemWriterBuilder() 77 | .dataSource(dataSource) 78 | .sql("INSERT INTO CUSTOMER (first_name, " + 79 | "middle_initial, " + 80 | "last_name, " + 81 | "address, " + 82 | "city, " + 83 | "state, " + 84 | "zip) VALUES (:firstName, " + 85 | ":middleInitial, " + 86 | ":lastName, " + 87 | ":address, " + 88 | ":city, " + 89 | ":state, " + 90 | ":zip)") 91 | .beanMapped() 92 | .build(); 93 | } 94 | ``` 95 | 96 | ### JPA 97 | 98 | ```java 99 | @Bean 100 | public JpaItemWriter jpaItemWriter(EntityManagerFactory entityManagerFactory) { 101 | JpaItemWriter jpaItemWriter = new JpaItemWriter<>(); 102 | 103 | jpaItemWriter.setEntityManagerFactory(entityManagerFactory); 104 | 105 | return jpaItemWriter; 106 | } 107 | ``` 108 | 109 | ## 스프링데이터 110 | 111 | ### Repository 112 | 113 | ```java 114 | @Entity 115 | @Table(name = "customer") 116 | public class Customer implements Serializable { 117 | private static final long serialVersionUID = 1L; 118 | 119 | @Id 120 | @GeneratedValue(strategy = GenerationType.IDENTITY) 121 | private long id; 122 | private String firstName; 123 | private String middleInitial; 124 | private String lastName; 125 | private String address; 126 | private String city; 127 | private String state; 128 | private String zip; 129 | private String email; 130 | 131 | // Getter and Setter 132 | } 133 | ``` 134 | 135 | ```java 136 | @Bean 137 | public RepositoryItemWriter repositoryItemWriter(CustomerRepository repository) { 138 | return new RepositoryItemWriterBuilder() 139 | .repository(repository) 140 | .methodName("save") 141 | .build(); 142 | } 143 | ``` -------------------------------------------------------------------------------- /10주차/요약본/[10주차]_9장_ItemWriter_이호빈.md: -------------------------------------------------------------------------------- 1 | # 9-1장 ItemWriter 2 | 3 | - ItemWriter는 스프링 배치의 출력 매커니즘 4 | - 원래는 Item을 읽고 처리되는대로 바로 출력했다. 그러나 지금은 Chunk단위로 출력한다. 5 | - Chunk 단위를 매개변수로 받기 때문에 List로 받는다. 6 | 7 | > 트랜잭션이 적용되지 않은 리소스를 처리한다면 Write에 실패했을 때 롤백할 방법이 없다. 그래서 추가적인 보호 조치를 취해야 한다. 8 | > 9 | 10 | ### 파일 기반 ItemWriter 11 | 12 | - FlatFileItemWriter 13 | - 텍스트 파일 출력을 만들 때 사용하라고 스프링 배치가 만들어둔 ItemWriter의 구현체다 14 | - 트랜잭션 주기 내에서 실제 쓰기 작업을 가능한 늦게 수행하도록 설계해서 롤백이 그나마 수월하도록 구현했다. 15 | - 실제로 데이터를 기록할 때 TransactionSynchronizationAdapter의 beforeCommit 메서드를 사용해서 해당 매커니즘을 구현했다. 16 | - 즉, 쓰기 외 모든 작업을 완료한 뒤 Writer로 데이터를 실제로 디스크에 기록하기 직전에 PlatformTransactionManager가 트랜잭션을 커밋한다. 17 | - 디스크로 Flush되면 롤백할 수 없기때문에 딱 디스크에 기록 전에 이뤄지도록 한다. 18 | - 각종 파일 관리 옵션이 있다 19 | - 파일이 이미 만들어져 있을 때 삭제하고 다시 만든다든지 20 | - 파일이 이미 만들어져 있으면 여기에 추가한다든지 등등... 21 | - StaxEventItemWriter 22 | - XML 작성 구현체 23 | - FlatFileItemWriter와 같이 실제 쓰기 작업을 가능한 늦게 수행한다. 24 | 25 | ### 데이터베이스 기반 ItemWriter 26 | 27 | - JdbcBatchItemWriter 28 | - 하나의 Chunk에 대한 모든 SQL 문을 한 번에 실행하기 위해 PreparedStatement의 배치 업데이트 기능을 사용한다. 29 | - 이렇게 하면 실행 성능을 향상시키면서 실행 자체를 현재 트랜잭션에서 할 수 있다 30 | - `.beanMapped()` **를 사용해서 파라미터에 ?가 아니라 이름을 줄 수 있다 31 | - HibernateItemWriter 32 | - JpaItemWriter 33 | 34 | ### 스프링 데이터의 ItemWriter 35 | 36 | - Mongo DB 37 | - 테이블이 없기 떄문에 Entity에서 JPA 애너테이션을 제거해야한다. 38 | - yml도 spring.data.mongodb.database로 알려줘야 한다. 39 | - MongoDB는 트랜잭션을 지원하지 않아서 커밋이 발생하기 직전까지 쓰기를 지연한다. 40 | 41 | ```sql 42 | @Bean 43 | public MongoItemWriter mongoItemWriter(MongoOperations mongoTemplate) { 44 | return new MongoItemWriterBuilder() 45 | .collection("customers") 46 | .template(mongoTemplate) 47 | .build(); 48 | } 49 | ``` 50 | 51 | - Repository 52 | - 쓰기 작업을 할 때는 페이징이나 정렬에 관해 걱정할 필요가 없다. 그래서 그냥 CrudRepository를 사용하면 된다. 53 | 54 | [실습코드](https://github.com/aegis1920/my-lab/tree/master/def-guide-spring-batch) 55 | -------------------------------------------------------------------------------- /10주차/요약본/[10주차]_9장_스프링배치_함호식.md: -------------------------------------------------------------------------------- 1 | 9.ItemWriter 2 | -- 3 | 4 | 청크 기반으로 처리하는 ItemWriter는 아이템을 1건씩 쓰지 않는다. 5 | 대신 아이템을 묶음(청크)단위로 쓴다. 6 | 이런 이유로 ItemReader 인터페이스와는 약간 다르다. 7 | 8 | ```java 9 | public interface ItemWriter { 10 | void write(List items) throws Exception; 11 | } 12 | ``` 13 | 14 | --- 15 | 16 | #### 파일 기반 ItemWriter 17 | 18 | 파일 사용 이유 19 | * 간단하고 신뢰할 수 있다. 20 | * 백업이 쉽다.(재시작을 해야하는 경우 복구도 마찬가지) 21 | 22 | `FlatFileItemWriter`는 텍스트 파일 출력을 만들때 사용할 수 있는 ItemWriter 구현체다. 23 | -> FlatFileItemWriter는 쓰기 데이터의 노출을 제한해, 24 | 롤백이 가능한 커밋 전 마지막까지 출력 데이터의 저장을 지연한다. 25 | 26 | 27 | ##### delimiter를 이용한 outputFile 출력 28 | ```java 29 | @Bean 30 | @StepScope 31 | public FlatFileItemWriter flatFileItemWriter() { 32 | return new FlatFileItemWriterBuilder() 33 | .name("customerItemWriter") 34 | .resource(new ClassPathResource("ch9/outputFile.txt")) 35 | .delimited() 36 | .delimiter("|") 37 | .names(new String[] { 38 | "firstName" 39 | ,"lastName" 40 | ,"city" 41 | ,"zip"}) 42 | .append(true) 43 | .build(); 44 | } 45 | ``` 46 | 47 | outputFile.txt 48 | ```text 49 | hosik|kim|seoul|12311 50 | hosik|ham|seoul|22222 51 | hosik|lee|seoul|12312 52 | hosik|yoo|seoul|44444 53 | hosik|tee|seoul|21231 54 | hosik|sss|busan|22222 55 | ``` 56 | 57 | ##### format을 이용한 outpufFile 출력 58 | 59 | ```java 60 | @Bean 61 | @StepScope 62 | public FlatFileItemWriter flatFileItemWriterLab1() { 63 | return new FlatFileItemWriterBuilder() 64 | .name("customerItemWriter") 65 | .resource(new ClassPathResource("ch9/lab1/outputFile.txt")) 66 | .delimited() 67 | .delimiter("|") 68 | .names(new String[] { 69 | "firstName" 70 | ,"lastName" 71 | ,"city" 72 | ,"zip"}) 73 | .append(true) 74 | .build(); 75 | } 76 | ``` 77 | 78 | outpufFile.txt 79 | ```text 80 | hosikkim lives at seoul. zip is 12311. 81 | hosikham lives at seoul. zip is 22222. 82 | hosiklee lives at seoul. zip is 12312. 83 | hosikyoo lives at seoul. zip is 44444. 84 | hosiktee lives at seoul. zip is 21231. 85 | hosiksss lives at busan. zip is 22222. 86 | ``` 87 | 88 | ##### 파일 관리 옵션 89 | 읽기 처리의 경우 읽을 파일이 반드시 존재해야 하지만, 90 | 출력 파일은 처리 시에 존재할 수도 있고 없을 수도 있다. 91 | 92 | * shouldDeleteIfEmpty 93 | * 스텝이 완료될 때 사용된다. 94 | * true로 설정되어 있을때, 스텝이 실행됐음에도 레코드가 기록되지 않은 경우, 95 | * default 값은 false이다. 96 | 스텝이 완료되는 시점에 파일이 삭제된다. 97 | * shouldDeleteIfExists 98 | * 쓰기 작업ㄷ 대상 출력파일과 같은 파일이 존재하면 해당 파일을 삭제한다. 99 | * default 값은 true이다. 100 | 101 | --- 102 | 103 | #### 데이터베이스 기반 ItemWriter 104 | 105 | 데이터베이스는 파일과 달리 트랜잭션이 적용되는 리소스다. 106 | 물리적인 쓰기를 트랜잭션의 일부분으로 포함할 수 있다. 107 | 108 | ##### JdbcBatchItemWriter 109 | JdbcTemplate 배치 SQL 실행 기능을 사용해 한번에 청크 하나에 대한 모든 SQL을 실행한다. 110 | 111 | 유의할 점 112 | -> 데이터 한 건마다 SQL문을 호출하는 대신 스프링은 하나의 청크에 대한 모든 SQL문을 한 번에 실행하기 위해 113 | PreparedStatement의 배치 업데이트 기능을 사용한다. 114 | 115 | 116 | 117 |
118 | 119 | 120 | JdbcBatchItemWriter code 121 | 122 | 123 | ```java 124 | @Override 125 | public void write(final List items) throws Exception { 126 | 127 | if (!items.isEmpty()) { 128 | if (logger.isDebugEnabled()) { 129 | logger.debug("Executing batch with " + items.size() + " items."); 130 | } 131 | 132 | int[] updateCounts; 133 | 134 | if (usingNamedParameters) { 135 | if(items.get(0) instanceof Map && this.itemSqlParameterSourceProvider == null) { 136 | updateCounts = namedParameterJdbcTemplate.batchUpdate(sql, items.toArray(new Map[items.size()])); 137 | } else { 138 | SqlParameterSource[] batchArgs = new SqlParameterSource[items.size()]; 139 | int i = 0; 140 | for (T item : items) { 141 | batchArgs[i++] = itemSqlParameterSourceProvider.createSqlParameterSource(item); 142 | } 143 | updateCounts = namedParameterJdbcTemplate.batchUpdate(sql, batchArgs); 144 | } 145 | } 146 | else { 147 | updateCounts = namedParameterJdbcTemplate.getJdbcOperations().execute(sql, new PreparedStatementCallback() { 148 | @Override 149 | public int[] doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException { 150 | for (T item : items) { 151 | itemPreparedStatementSetter.setValues(item, ps); 152 | ps.addBatch(); 153 | } 154 | return ps.executeBatch(); 155 | } 156 | }); 157 | } 158 | 159 | if (assertUpdates) { 160 | for (int i = 0; i < updateCounts.length; i++) { 161 | int value = updateCounts[i]; 162 | if (value == 0) { 163 | throw new EmptyResultDataAccessException("Item " + i + " of " + updateCounts.length 164 | + " did not update any rows: [" + items.get(i) + "]", 1); 165 | } 166 | } 167 | } 168 | } 169 | } 170 | ``` 171 |
172 | 173 | ```java 174 | @Bean 175 | public JdbcBatchItemWriter jdbcCustomerWriter(DataSource dataSource) { 176 | return new JdbcBatchItemWriterBuilder() 177 | .dataSource(dataSource) 178 | .sql("INSERT INTO CUSTOMER (first_name, " + 179 | "last_name, " + 180 | "city, " + 181 | "zip) VALUES (?, ?, ?, ?)") 182 | .itemPreparedStatementSetter(new CustomerItemPreparedStatementSetter()) 183 | .build(); 184 | } 185 | ``` 186 | 187 | ```java 188 | public class CustomerItemPreparedStatementSetter implements 189 | ItemPreparedStatementSetter { 190 | 191 | public void setValues(Customer customer, PreparedStatement ps) 192 | throws SQLException { 193 | 194 | ps.setString(1, customer.getFirstName()); 195 | ps.setString(2, customer.getLastName()); 196 | ps.setString(3, customer.getCity()); 197 | ps.setString(4, customer.getZip()); 198 | } 199 | } 200 | ``` 201 | 202 | CUSTOMER TABLE 203 | 204 | | first\_name | last\_name | city | zip | 205 | | :--- | :--- | :--- | :--- | 206 | | hosik | kim | seoul | 12311 | 207 | | hosik | ham | seoul | 22222 | 208 | | hosik | lee | seoul | 12312 | 209 | | hosik | yoo | seoul | 44444 | 210 | | hosik | tee | seoul | 21231 | 211 | | hosik | sss | busan | 22222 | 212 | 213 | --- 214 | 215 | #### HibernateItemWriter 216 | JdbcBatchItemWriter와 마찬가지로 HibernateItemWriter는 하이버테이트 세션 API의 간단한 래퍼 클래스이다. 217 | 청크가 완료되면 아이템 목록이 HibernateItemWriter로 전달되며, 218 | HibernateItemWriter 내에서는 세션과 관련 없는 아이템에 대해 saveOrUpdate 메서드를 호출한다. 219 | 220 | Hibernate를 사용하기 위한 BatchConfigurer를 구현하여 기본 세팅을 해줘야한다. 221 | ```java 222 | public class HibernateBatchConfigurer implements BatchConfigurer { 223 | 224 | private DataSource dataSource; 225 | private SessionFactory sessionFactory; 226 | private JobRepository jobRepository; 227 | private PlatformTransactionManager transactionManager; 228 | private JobLauncher jobLauncher; 229 | private JobExplorer jobExplorer; 230 | 231 | public HibernateBatchConfigurer(DataSource dataSource, EntityManagerFactory entityManagerFactory) { 232 | this.dataSource = dataSource; 233 | this.sessionFactory = entityManagerFactory.unwrap(SessionFactory.class); 234 | } 235 | 236 | @Override 237 | public JobRepository getJobRepository() { 238 | return this.jobRepository; 239 | } 240 | 241 | @Override 242 | public PlatformTransactionManager getTransactionManager() { 243 | return this.transactionManager; 244 | } 245 | 246 | @Override 247 | public JobLauncher getJobLauncher() { 248 | return this.jobLauncher; 249 | } 250 | 251 | @Override 252 | public JobExplorer getJobExplorer() { 253 | return this.jobExplorer; 254 | } 255 | 256 | @PostConstruct 257 | public void initialize() { 258 | 259 | try { 260 | HibernateTransactionManager transactionManager = new HibernateTransactionManager(sessionFactory); 261 | transactionManager.afterPropertiesSet(); 262 | 263 | this.transactionManager = transactionManager; 264 | 265 | this.jobRepository = createJobRepository(); 266 | this.jobExplorer = createJobExplorer(); 267 | this.jobLauncher = createJobLauncher(); 268 | 269 | } 270 | catch (Exception e) { 271 | throw new BatchConfigurationException(e); 272 | } 273 | } 274 | 275 | private JobLauncher createJobLauncher() throws Exception { 276 | SimpleJobLauncher jobLauncher = new SimpleJobLauncher(); 277 | 278 | jobLauncher.setJobRepository(this.jobRepository); 279 | jobLauncher.afterPropertiesSet(); 280 | 281 | return jobLauncher; 282 | } 283 | 284 | private JobExplorer createJobExplorer() throws Exception { 285 | JobExplorerFactoryBean jobExplorerFactoryBean = new JobExplorerFactoryBean(); 286 | 287 | jobExplorerFactoryBean.setDataSource(this.dataSource); 288 | jobExplorerFactoryBean.afterPropertiesSet(); 289 | 290 | return jobExplorerFactoryBean.getObject(); 291 | } 292 | 293 | private JobRepository createJobRepository() throws Exception { 294 | JobRepositoryFactoryBean jobRepositoryFactoryBean = new JobRepositoryFactoryBean(); 295 | 296 | jobRepositoryFactoryBean.setDataSource(this.dataSource); 297 | jobRepositoryFactoryBean.setTransactionManager(this.transactionManager); 298 | jobRepositoryFactoryBean.afterPropertiesSet(); 299 | 300 | return jobRepositoryFactoryBean.getObject(); 301 | } 302 | } 303 | ``` 304 | 305 | ```java 306 | @Bean 307 | public HibernateItemWriter hibernateItemWriter() { 308 | return new HibernateItemWriterBuilder() 309 | .sessionFactory(hibernateBatchConfigurer.getSessionFactory()) 310 | .build(); 311 | } 312 | ``` 313 | 314 | ```java 315 | @Data 316 | @Entity 317 | @Table(name = "customer") 318 | public class Customer implements Serializable { 319 | @Id 320 | @GeneratedValue(strategy = GenerationType.IDENTITY) 321 | private Long id; 322 | 323 | private String firstName; 324 | 325 | private String lastName; 326 | 327 | private String city; 328 | 329 | private String zip; 330 | } 331 | ``` -------------------------------------------------------------------------------- /10주차/요약본/[9주차]_9장_ItemWriter-1_김광훈.md: -------------------------------------------------------------------------------- 1 | # ItemWriter - 1 2 | ## 1. ItemWriter ?? 3 | #### 소개 4 | ItemWriter 는 스프링 배치의 출력을 담당하는 기능을 한다. 5 | 6 | 스프링 배치 처리 결과를 포맷에 맞게 출력할 필요가 있을 때 스프링 배치가 제공하는 ItemWriter 를 사용한다. 7 | 8 | 과거에는 ItemReader 와 동일하게 각 아이템이 처리되는 대로 출력이 되었지만, 스프링 배치2 에서 청크 기반처리 방식이 도입되면서 ItemWriter 의 역할이 바뀌었다. 9 | 10 | 즉, 청크 기반이기 때문에 아이템을 묶음 단위로 Write 하게 된다. 11 | 12 | ```java 13 | public interface ItemWriter { 14 | void write(List items) throws Exception; 15 | } 16 | ``` 17 | 18 |
19 | 20 | 21 | ## 2. 데이터베이스 기반 ItemWriter 22 | #### 소개 23 | 데이터베이스는 파일과 달리 트랜잭션이 적용되는 리소스이다. 24 | 25 | 파일 처리 기반과 달리, 물리적인 쓰기를 트랜잭션의 일부분으로 포함할 수 있다. 26 | 27 |
28 | 29 | #### HibernateItemWriter 30 | HibernateItemWriter 는 하이버네이트 세션 API 의 간단한 Wrapper 에 지나지 않는다. 31 | 32 | 청크가 완료되면 아이템 목록이 HibernateItemWriter 로 전달되며, HibernateItemWriter 내에서는 세션과 아직 연관되지 않은 각 아이템에 대해 33 | 34 | 하이버네이트의 Session.saveOrUpdate 메서드를 호출한다. 모든 아이템이 저장되거나 수정되면 HibernateItemWriter 는 Session 의 flush 메서드를 호출하여 변경 사항을 한 번에 실행한다. 35 | 36 |
37 | 38 | #### JpaItemWriter 39 | JpaItemWriter 는 Jpa 의 javax.persistence.EntityManager 를 감싼 간단한 Wrapper 에 불과하다. 40 | 41 | 청크가 완료가 되면 청크 내의 아이템 목록이 JpaItemWriter 로 전달된다. 42 | 43 | JpaItemWriter 는 모든 아이템을 저장한 뒤 flush 를 호출하기 전에 아이템 목록 내 아이템을 순회하면서 아이템마다 EntityManager 의 merge 메서드를 호출한다. 44 | 45 | HibernateItemWriter 와의 차이점은 생성자에서 SessionFactory 대신 EntityManager 를 저장한다. 46 | 47 | 그리고 initialize 메서드에서 HibernateTransactionManager 를 생성하는 대신 JpaTransactionManager 를 생성한다. 48 | 49 | ```java 50 | @Bean 51 | public JpaItemWriter jpaItemWriter() { 52 | JpaItemWriter jpaItemWriter = new JpaItemWriter<>(); 53 | jpaItemWriter.setEntityManagerFactory(entityManagerFactory); 54 | return jpaItemWriter; 55 | } 56 | ``` -------------------------------------------------------------------------------- /10주차/요약본/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /11주차/발표자료/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /11주차/요약본/[11주차]_9장_ItemWriter_김광훈.md: -------------------------------------------------------------------------------- 1 | # ItemWriter - 2 2 | ## JmsItemWriter 3 | JMS 는 둘 이상의 엔드포인트 간에 통신하는 메시지 지향적인 방식이다. 4 | 5 | JMS 을 사용해 작업을 하려면 JMS 브로커를 사용해야 한다. 6 | 7 | JMS 를 단일 스텝으로 사용한다면 큐에 올바르게 도착했는지 알 수 없기 때문에 두 개의 스텝을 사용한다. 8 | 9 | 첫 번쨰 스텝은 파일에 읽고 큐에 쓴다. 두 번째 스텝은 큐에서 데이터를 읽고 파일을 쓴다. 10 | 11 | ## SimpleMailMessageItemWriter 12 | 고객에게 메일을 보내려고 한다면, SimpleMailMessageItemWriter 를 사용하면 좋다. 13 | 14 | JMS 와 비슷한 이유로 두 개의 스텝을 사용하는 것을 권장한다. 15 | 16 | ## CompositeItemWriter 17 | CompositeItemWriter 를 사용해서 스텝 내에서 여러 ItemWriter 가 동일한 아이템에 대해 쓰기 작업을 수행할 수 있다. 18 | 19 | ## ClassifierCompositeItemWriter 20 | ClassifierCompositeItemWriter 는 서로 다른 유형의 아이템을 확인하고 어떤 ItemWriter 를 사용해 쓰기 작업을 수행할지 판별한 후 적절한 라이터에 아이템을 전달할 수 있다. 21 | 22 | ## ItemStream 인터페이스 23 | ItemStream 인터페이스는 주기적으로 상태를 저장하고 복원하는 역할을 한다. 24 | 25 | 해당 인터페이스는 open, update, close 세 가지 인터페이스로 구성되며, 상태를 가진 ItemReader 나 ItemWriter 에 의하여 구현된다. 26 | 27 | 예를 들어, 입력이나 출력에 파일을 상요한다면 open 메서드는 필요한 파일을 열고 close 메서드는 필요한 파일을 닫는다. 28 | 29 | update 메서드는 각 청크가 완료가 될 때 현재 상황을 기록한다. 30 | 31 | ## ClassifierCompositeItemWriter vs CompositeItemWriter 32 | 둘의 차이점은 CompositeItemWriter 가 org.springframework.batch.item.ItemStream 인터페이스를 구현했다는 것이다. 33 | 34 | CompositeItemWriter 에서 open 메서드는 필요에 따라 위임 ItemWriter 의 open 메서드를 반복적으로 호출한다. 35 | 36 | close 와 update 메서드도 동일한 방식으로 동작한다. 반면 ClassifierCompositeItemWriter 는 ItemStream 의 메서드를 구현하지 않는다. 37 | 38 | 이 때문에 XML 파일이 열리지 않은 상태에서 XMLEventFactory 생성되거나 XML 쓰기가 시도되어 예외가 발생한다. -------------------------------------------------------------------------------- /11주차/요약본/[11주차]_9장_ItemWriter_김성모.md: -------------------------------------------------------------------------------- 1 | # ItemWriter-2 2 | 3 | ## 그 밖의 ItemWriter 4 | 5 | ### ItemWriterAdapter 6 | 7 | > 기존에 운영하던 서비스를 재사용하기 위하여 `ItemWriterAdapter` 를 사용하여 ItemWriter로 사용한다. 8 | 9 | ItemWriterAdapter를 구현하기 위해서는 다음의 두 가지 의존성이 필요하다. 10 | 1. targetObject : 호출할 메서드를 가진 스프링 빈 11 | 2. targetMethod : 각 아이템을 처리할 때 호출할 메서드 12 | 13 | 단 targetMethod는 단 하나의 아규먼트만을 받아야만 한다. 14 | 15 | ### PropertyExtractingDelegatingItemWriter 16 | 17 | `ItemWriterAdapter`가 단 하나의 아규먼트만을 넘기는 것에 반해 `PropertyExtractingDelegatingItemWriter` 을 18 | 사용하면 파라미터를 지정해서 넘길 수 있다. 19 | 20 | 21 | ### JmsItemWriter 22 | 23 | 둘 이상의 엔드포인트 간에 통신하는 메시지 지향적 방식이다. (JavaMessagingService) 24 | 25 | 26 | ### SimpleMailMessageItemWriter 27 | 28 | 이메일을 보낼 때 사용하는 ItemWriter이다. 29 | 30 | ## 여러 자원을 사용하는 ItemWriter 31 | 32 | ### MultiResourceItemWriter 33 | 34 | 동일한 포맷의 다중 파일을 단일 스탭네에서 읽는 기능과 유사하게 다중 리소스를 생성하는 방법이다. 35 | 36 | ### CompositeItemWriter 37 | 38 | 하나의 스텝이 하나의 출력물을 만드는 것이 아닌 여러 엔드포인트의 작업을 진행할 때 사용한다. 39 | 40 | ### ClassifierCompositeItemWriter 41 | 42 | 서로 다른 유형의 레코드를 서로 다른 파서와 매퍼가 처리할 수 있도록 만드는 작업을 할 때 사용한다. 43 | 44 | ### ItemStream 인터페이스 45 | 46 | 주기적으로 상태를 저장하고 복원하는 역할을 하며 open, update, close 세 가지 메소드로 구성된다. 47 | 48 | open 은 파일을 열고 close 는 파일을 닫는다. 49 | update는 각 청크가 완료될 때 현재 상태를 기록한다. 50 | -------------------------------------------------------------------------------- /11주차/요약본/[11주차]_9장_ItemWriter_이호빈.md: -------------------------------------------------------------------------------- 1 | # 9-2장 ItemWriter 2 | 3 | ### ItemWriterAdapter 4 | 5 | - 기존 스프링 서비스를 ItemWriter로 사용할 때 쓴다. 6 | - ItemWriterAdapter는 기존 서비스를 살짝 감싼 Wrapper 역할을 한다 7 | 8 | ```java 9 | @Bean 10 | public ItemWriterAdapter itemWriter(CustomerService customerService) { 11 | ItemWriterAdapter customerItemrWriterAdapter = new ItemWriterAdapter<>(); 12 | customerItemrWriterAdapter.setTargetObject(customerService); // 서비스 클래스 13 | customerItemrWriterAdapter.setTargetMethod("logCustomer"); // 메서드 명 14 | 15 | return customerItemrWriterAdapter; 16 | } 17 | 18 | // ... 19 | // .reader(customerFileReader(null)) 20 | // .writer(null) // 프록시로 인해 시작시에는 들어간다. 21 | // .build(); 22 | ``` 23 | 24 | ### PropertyExtractingDelegatingItemWriter 25 | 26 | - ItemWriterAdapter를 사용할 때 processor 혹은 reader에서 넘어온 item을 그대로 사용하게 되는데 item의 모든 필드를 넘겨받고 싶지 않을 수 있다. 27 | - PropertyExtractingDelegatingItemWriter는 원하는 필드만 파라미터로 받고 싶을 때 사용할 수 있다. 28 | 29 | ```java 30 | @Bean 31 | public PropertyExtractingDelegatingItemWriter itemWriter(CustomerService customerService) { 32 | PropertyExtractingDelegatingItemWriter itemWriter = new PropertyExtractingDelegatingItemWriter<>(); 33 | itemWriter.setTargetObject(customerService); // 서비스 클래스 34 | itemWriter.setTargetMethod("logCustomer"); // 메서드 명 35 | itemWriter.setFieldsUsedAsTargetMethodArguments(new String[] {"address", "city", "state", "zip"}); 36 | 37 | return itemWriter; 38 | } 39 | 40 | // ... 41 | // .reader(customerFileReader(null)) 42 | // .writer(null) // 프록시로 인해 시작시에는 들어간다. 43 | // .build(); 44 | ``` 45 | 46 | ### JmsItemWriter 47 | 48 | - Java Messaging Service는 엔드포인트 간에 통신하는 메시지 지향 방식이다. 49 | - pub-sub 모델을 사용해 다른 기술과 통신할 수 있다 50 | - Jms 브로커가 필요한데 예제에서는 아파치 액티브 MQ를 사용했다. 51 | 52 | ```java 53 | // 스프링이 자체적으로 기능을 제공하는 게 있지만 잘 안된다고 한다? 54 | 55 | @Bean 56 | public MessageConverter jacksonJmsMessageConverter() { 57 | MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); 58 | converter.setTargetType(MessageType.TEXT); 59 | converter.setTypeIdPropertyName("_type"); 60 | return converter; 61 | } 62 | 63 | @Bean 64 | public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) { 65 | CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory(connectionFactory); 66 | cachingConnectionFactory.afterPropertiesSet(); 67 | 68 | JmsTemplate jmsTemplate = new JmsTemplate(cachingConnectionFactory); 69 | jmsTemplate.setDefaultDestinationName("customers"); 70 | jmsTemplate.setReceiveTimeout(5000L); 71 | 72 | return jmsTemplate; 73 | } 74 | 75 | @Bean 76 | public JmsItemReader jmsItemReader(JmsTemplate jmsTemplate) { 77 | return new JmsItemReaderBuilder() 78 | .jmsTemplate(jmsTemplate) 79 | .itemType(Customer.class) 80 | .build(); 81 | } 82 | 83 | @Bean 84 | public JmsItemWriter jmsItemWriter(JmsTemplate jmsTemplate) { 85 | return new JmsItemWriterBuilder() 86 | .jmsTemplate(jmsTemplate) 87 | .build(); 88 | } 89 | ``` 90 | 91 | ### SimpleMailMessageItemWriter 92 | 93 | - 이메일을 보낼 수 있는 ItemWriter 94 | - spring boot starter mail을 사용하며 구글 SMTP 서버를 사용한다 95 | - 각종 설정을 해주고 processor 단계에서 보낼 메일을 작성한다. 96 | 97 | ```java 98 | @Bean 99 | public SimpleMailMessageItemWriter emailItemWriter(MailSender mailSender) { 100 | return new SimpleMailMessageItemWriterBuilder() 101 | .mailSender(mailSender) 102 | .build(); 103 | } 104 | ``` 105 | 106 | ### MultiResourceItemWriter 107 | 108 | - 아이템 개수마다 새 파일을 생성해야될 때 쓰인다. 109 | - 예를 들어, 총 아이템 개수가 100개라면 25개마다 새 파일을 만들도록 설정할 수 있다. 110 | - MultiResourceItemWriter의 write 메서드가 호출되면 해당 Writer는 현재 리소스가 이미 생성된 상태로 열려있는지 확인한다. (생성되지 않았다면 새 파일을 생성한다.) 111 | - 그리고 itemWriter에 위임(delegate)한다 112 | - 아이템의 쓰기 작업이 이뤄지면 파일에 기록된 아이템 개수가 새 리소스 생성을 위해 구성된 임계값에 도달했는지 확인한다. 113 | - 새 리소스를 생성하기 전에 청크가 끝날 때까지 기다린다. (청크가 끝나고 나서 새 리소스를 생성한다) 114 | - 아이템 개수가 15개 일때 새 파일을 생성하도록 해도, 청크 단위가 20개라면 20개까지 처리하고 생성한다. 115 | 116 | ```java 117 | @Bean 118 | public MultiResourceItemWriter multiCustomerFileWriter(CustomerOutputFileSuffixCreator suffixCreator) throws Exception { 119 | return new MultiResourceItemWriterBuilder() 120 | .name("multiCustomerFileWriter") 121 | .delegate(delegateItemWriter()) // 위임할 ItemWriter 122 | .itemCountLimitPerResource(25) // 리소스(파일) 당 아이템 개수 123 | .resource(new FileSystemResource("Chapter09/target/customer")) // 리소스 경로 124 | .resourceSuffixCreator(suffixCreator()) // 확장자 125 | .build(); 126 | } 127 | 128 | @Component 129 | public class CustomerOutputFileSuffixCreator implements ResourceSuffixCreator { 130 | @Override 131 | public String getSuffix(int arg0) { 132 | return arg0 + ".xml"; 133 | } 134 | } 135 | ``` 136 | 137 | ### FlatFile에 Header와 Footer 추가하기 138 | 139 | - FlatFileHeaderCallback과 FlatFileFooterCallback을 사용해야 한다. 140 | - Aspect를 사용해야 한다. (open 메서드 수행 전에 실행되어야 하므로) 141 | 142 | ### CompositeItemWriter 143 | 144 | - 여러 ItemWriter를 동일한 아이템에 쓰기 작업을 수행할 수 있다. 145 | 146 | ```java 147 | @Bean 148 | public CompositeItemWriter compositeItemWriter() throws Exception { 149 | return new CompositeItemWriterBuilder() 150 | .delegates(Arrays.asList(xmlDelegateItemWriter(null), jdbcDelegateItemWriter(null))) 151 | .build(); 152 | } 153 | ``` 154 | 155 | - 쓰기 실행은 순차적으로 일어나며 동일한 트랜잭션에서 발생한다. 중간에 쓰기 작업을 수행하지 못했다면 전체 청크가 롤백된다. 156 | 157 | ### ClassifierCompositeItemWriter 158 | 159 | - 아이템을 분류(Classfier)해서 어떤 ItemWriter를 쓸 지 판별해 전달할 수 있다 160 | 161 | ```java 162 | @Bean 163 | public class CustomerClassifier implements Classifier> { 164 | private ItemWriter fileItemWriter; 165 | private ItemWriter jdbcItemWriter; 166 | 167 | // 생성자 주입 168 | 169 | @Override 170 | public ItemWriter classify(Customer customer) { 171 | if (customer.getState().matches("^[A-M].*")) { 172 | return fileItemWriter; 173 | } else { 174 | return jdbcItemWriter; 175 | } 176 | } 177 | } 178 | 179 | @Bean 180 | public ClassifierCompositeItemWriter classifierCompositeItemWriter() throws Exception { 181 | Classifier> classifier = new CustomerClassifier(xmlDelegate(null), jdbcDelegate(null)); 182 | 183 | return ClassifierCompositeItemWriterBuilder() 184 | .classifier(classifier) 185 | .build(); 186 | } 187 | ``` 188 | 189 | - ClassifierCompositeItemWriter는 ItemStream의 메서드를 구현하지 않기 때문에 해당 ItemReader나 ItemWriter를 stream으로 등록해야 한다. 190 | 191 | ```java 192 | // ... 193 | this.stepBuilderFactory.. 194 | .reader() 195 | .writer() 196 | .stream(xmlDelegate(null)) // 이 부분이 추가됐다. 197 | .build(); 198 | ``` 199 | -------------------------------------------------------------------------------- /11주차/요약본/[11주차]_9장_ItemWriter_황준호.md: -------------------------------------------------------------------------------- 1 | ## 9장. ItemWriter 2 | 3 | ### 그밖의 출력 방식을 위한 ItemWriter 4 | 5 | - `ItemWriterAdapter` 6 | 7 | - 기존 스프링 서비스를 ItemWriter로 이용할 수 있다. 8 | - 다음 예는 기존에 존재하는 서비스인 `CustomerService::method`를 이용하는 코드 9 | 10 | ```java 11 | @Bean 12 | public ItemWriterAdapter itemWriter(CustomerService customerService) { 13 | ItemWriterAdapter customerItemWriterAdapter = new ItemWriterAdapter<>(); 14 | customerItemWriterAdapter.setTargetObject(customerService); 15 | customerItemWriterAdapter.setTargetMethod("method"); // 단, 이 메서드는 인자로 Customer 하나만 받아야 한다 16 | return customerItemWriterAdapter; 17 | } 18 | ``` 19 | 20 | - 하지만, 이미 존재하는 서비스가 `Customer`를 받아들이지 않는다면, 값을 추출해 전달해야 한다. 이때는 다음에서 소개하는 `PropertyExtractingDelegatingItemWriter`를 사용한다 21 | 22 | - `PropertyExtractingDelegatingItemWriter` 23 | 24 | - 아이템에서 값을 추출한 후 서비스에 파라미터로 전달한다. 25 | 26 | ```java 27 | // 기존에 존재하는 서비스 28 | @Service 29 | public CustomerService { 30 | public void method(String address, String city, String state, String zip) { 31 | ... 32 | } 33 | } 34 | ``` 35 | 36 | ```java 37 | @Bean 38 | public PropertyExtractingDelegatingItemWriter itemWriter(CustomerService customerService) { 39 | PropertyExtractingDelegatingItemWriter itemWriter 40 | = new PropertyExtractingDelegatingItemWriter<>(); 41 | itemWriter.setTargetObject(customerService); 42 | itemWriter.setTargetMethod("method"); 43 | itemWriter.setFieldsUsedAsTargetMethodArguments(new String[]{"address", "city", "state", "zip"}); 44 | return customerItemWriterAdapter; 45 | } 46 | ``` 47 | 48 | - `JmsItemWriter` - 생략 49 | 50 | - `SimpleMailMessageItemWriter` 51 | 52 | - 이메일을 보낼 수 있다. 아래 예는 모든 신규 고객 정보를 불러온 후 신규 고객 모두에게 환영 이메일을 보내는 코드 53 | 54 | - 설정해야 하는 것들 55 | 56 | 1. 자바 Mail 의존성 추가 57 | 58 | `implementation 'org.springframework.boot:spring-boot-starter-mail'` 59 | 60 | 2. `application.yml` 설정 61 | 62 | ```yaml 63 | spring: 64 | mail: 65 | host: smtp.gmail.com 66 | port: 587 67 | username: ... 68 | password: ... 69 | properties: 70 | mail: 71 | smtp: 72 | auth: true 73 | starttls: 74 | enable: true 75 | ``` 76 | 77 | 3. 잡 config 78 | 79 | ```java 80 | @Bean 81 | public Step emailStep() { 82 | return stepBuilderFactory.get("emailStep") 83 | .chunk(10) 84 | .reader(...) 85 | .processor(itemProcessor()) 86 | .writer(emailItemWriter(null)) 87 | .build(); 88 | } 89 | 90 | @Bean 91 | public ItemProcessor itemProcessor() { 92 | return customer -> { 93 | SimpleMailMessage mail = new SimpleMailMessage(); 94 | mail.setFrom("service@gmail.com"); 95 | mail.setTo(customer.getEmail()); 96 | mail.setSubject("환영합니다!"); 97 | mail.setText(String.format("%s %s님, 회원가입을 축하합니다!", 98 | customer.getFirstName(), customer.getLastName())); 99 | return mail; 100 | }; 101 | } 102 | 103 | @Bean 104 | public SimpleMailMessageItemWriter emailItemWriter(MailSender mailSender) { 105 | return new SimpleMailMessageItemWriterBuilder() 106 | .mailSender(mailSender) 107 | .build(); 108 | } 109 | ``` 110 | 111 | ### 여러 자원을 사용하는 ItemWriter 112 | 113 | - `MultiResourceItemWriter` 114 | 115 | - 일정 수 기준으로 나눠서 여러 파일로 저장하고 싶을때 사용한다 116 | 117 | ```java 118 | @Bean 119 | public MultiResourceItemWriter multiCustomerFileWriter() { 120 | return new MultiResourceItemWriterBuilder() 121 | .name("multiCustomerFileWriter") 122 | .delegate(delegateItemWriter()) //실제 쓰기를 수행할 ItemWriter 123 | .itemCountLimitPerResource(25) //각 리소스에 쓰기를 수행할 아이템 수 (25개가 넘게 저장될 수도 있다) 124 | .resource(new FileSystemResource("chapter9/output/customers")) // 저장될 위치(루트 기준) 125 | .resourceSuffixCreator(index -> index + ".csv") // customers1.csv, customers2.csv... 이렇게 저장됨 126 | .build(); 127 | } 128 | ``` 129 | 130 | - 총 회원수가 100명, 청크 크기가 10, itemCountLimitPerResource가 25인 경우? 131 | - 파일은 총 4개가 생긴다. (`customers1.csv`, `customers2.csv`, `customers3.csv`, `customers4.csv`) 132 | - 각 파일에는 25명씩 저장되는 게 아니라 30명, 30명, 30명, 10명 저장된다 133 | - 매 청크 기준으로 쓰기를 수행할 때마다 쓰기 후 청크 경계에 도달했는지 체크하기 때문에 청크 중간에 새 리소스를 생성하지 않기 때문 134 | - 헤더와 푸터를 추가하는 부분은 생략 135 | 136 | - `CompositeItemWriter` 137 | 138 | ```java 139 | @Bean 140 | public CompositeItemWriter compositeItemWriter() { 141 | return new CompositeItemWriterBuilder() 142 | .delegates(itemWriter1(), itemWriter2(), itemWriter3()) 143 | .build(); 144 | } 145 | ``` 146 | 147 | - 쓰기 실행이 순차적으로 한번에 하나의 라이터에서 일어난다 148 | - itemWriter1수행 -> itemWriter2수행 -> itemWriter3수행 149 | - 모든 쓰기작업이 하나의 트랜잭션에서 일어나기 때문에 itemWriter2에서 실패하면 itemWriter1도 롤백된다 150 | - 100개를 썼다면 `JobRepository`에 쓰기 수가 300이 아닌 100이 기록된다. (스프링 배치는 아이템수를 세기 때문) 151 | 152 | - `ClassifierCompositeItemWriter` 153 | 154 | - A~M에 해당하는 주에 사는 고객은 플랫 파일, N~Z에 해당하는 주에 사는 고객은 데이터베이스에 쓰고 싶을땐? 155 | 156 | ```java 157 | @AllArgsConstructor 158 | public class CustomerClassifier implements Classifier> { 159 | 160 | private ItemWriter fileItemWriter; 161 | private ItemWriter jdbcItemWriter; 162 | 163 | @Override 164 | public ItemWriter classify(Customer customer) { 165 | if (customer.getState().matches("^[A-M].*")) { 166 | return fileItemWriter; 167 | } else { 168 | return jdbcItemWriter; 169 | } 170 | } 171 | } 172 | ``` 173 | 174 | ```java 175 | @Bean 176 | public Step classifierCompositeWriterStep() { 177 | return this.stepBuilderFactory.get("classifierCompositeWriterStep") 178 | .chunk(10) 179 | .reader(...) 180 | .writer(classifierCompositeItemWriter()) 181 | // ClassifierCompositeItemWriter은 ItemStream을 구현하고 있지 않기 때문에 182 | // 아래 stream을 빼먹으면 xml쓰기 시도시 오류가 난다. 183 | // 상태를 유지하며 작업을 수행할 수 있도록 수동으로 등록해줘야 한다. 184 | .stream(xmlItemWriter()) 185 | .build(); 186 | } 187 | 188 | @Bean 189 | public ClassifierCompositeItemWriter classifierCompositeItemWriter() { 190 | Classifier> classifier 191 | = new CustomerClassifier(xmlItemWriter(), jdbcItemWriter()); 192 | 193 | return new ClassifierCompositeItemWriterBuilder() 194 | .classifier(classifier) 195 | .build(); 196 | } 197 | ``` 198 | 199 | -------------------------------------------------------------------------------- /11주차/요약본/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /12주차/발표자료/[12주차]_11장_확장과 튜닝_김성모.md: -------------------------------------------------------------------------------- 1 | # 11장. 확장과 튜닝 2 | 3 | 4 | 5 | ## 배치 잡 프로파일링 6 | 7 | 8 | 9 | > 프로파일링을 통해서 최적화와 관련된 의사결정을 내릴 수 있고 어플리케이션의 성능을 높일 수 있다. 10 | 11 | 12 | 13 | ### VisualVM 알아보기 14 | 15 | > JVM이 어떤 상태인지 파악할 수 있는 도구이다. 16 | > 17 | > CPU 및 메모리 사용량, 메서드 실행 시간, 스레드 관리, GC 관련 정보 제공한다. 18 | 19 | 20 | 21 | #### 설치 22 | 23 | VisualVM 을 알아보기 위해서는 다음의 순서로 사전 작업을 진행한다. 24 | 25 | - VisualVM 설치하기(https://visualvm.github.io/index.html) 26 | - IntelliJ와 연동하기 (플러그인 : visualvm launcher) 27 | - 실행해보기 28 | 29 | 30 | 31 | #### 화면설명 32 | 33 | - Overview : 실행 중인 자바 어플리케이션의 개요 제공. 34 | main 클래스, 어플리케이션 이름, 프로세스ID, Arguments 등 35 | - Monitor : CPU사용률, 메모리 사용률, 로딩된 클래스 개수, 데몬 스레드의 수 36 | GC를 실행시킬 수 있으며 Heap dump 도 생성할 수 있다. 37 | - Threads : 어플이 실행한 모든 스레드와 작업 정보 표시한다. 38 | - Sampler : CPU 사용률과 메모리 할당 상태를 스냅샷으로 만들어낸다. 39 | 40 | 41 | 42 | Monitor 탭에서 JVM의 관점으로 CPU 또는 메모리의 상태를 파악하며 이때 파악된 문제점의 원인을 찾을 때 다른 여러탭을 활용한다. 43 | 44 | 45 | 46 | #### 스프링 배치 어플리케이션 프로파일링하기 47 | 48 | > 어플리케이션 프로파일링 시 두 가지 중 하나를 살펴본다. 49 | > 50 | > 1. 어떤 부분에서 많은 CPU를 사용하는가? 51 | > 2. 무엇 때문에 많은 메모리가 사용되는가? 52 | 53 | 54 | 55 | ##### CPU 프로파일링 56 | 57 | CPU 사용률과 관련된 데이터를 얻는 방법을 알아보자. 58 | 59 | 60 | 61 | 62 | 63 | ## 배치 잡의 여러 확장성 64 | 65 | 66 | 67 | ### 다중 스레드 스텝 68 | 69 | - 청크단위를 쓰레드로 일을 나눠서 처리할 수 있다. 70 | 장점 : 청크를 병렬로 생성하므로 빠른 작업이 가능하다. 71 | 단점 : ItemReader의 상태를 유지하는 stateful 기능이 여러 스레드에서 덮어쓰기하여 리더 상태가 제대로 저장되지 않는다. 72 | 또한 이미 네트워크, 디스크버스등 자원 모두 소모하고 있을 땐 성능이 나아지지 않는다. 73 | 74 | ### 병렬 스텝 75 | 76 | 스텝을 병렬로 실행하여 영향을 주지 않는 스텝들을 분리하여 전반전익 잡의 처리량을 늘리는 방법에 대해 살펴보자. 77 | 78 | 79 | 80 | ### 파티셔닝 81 | 82 | > 마스터 스텝이 처리할 일을 여러 워커 스텝으로 넘기는 개념이다. 83 | > 84 | > 파티션 스텝에서 큰 데이터셋은 더 작은 파티션으로 나뉘어 각 파티션이 병렬로 처리한다. 85 | > 86 | > 각 워커는 읽기, 처리, 쓰기를 담당하는 온전한 스프링 배치 스텝이다. 87 | 88 | 89 | 90 | 크게 두가지의 추상화 를 이해해야한다. 91 | 92 | 1. `partitioner` 인터페이스 를 사용하여 여러 파티션으로 나누는 역할을 한다. 93 | 기본적으로 `MultiResourcePartitioner`를 제공하는데. 리소스당 파티션을 만든다. 94 | 2. `partitionHandler` 워커와 의사 소통을 하는 데 사용되는 인터페이스이다. 95 | 96 | 97 | 98 | ### 원격 청킹 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /12주차/발표자료/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /12주차/요약본/[12주차]_11장_확장과튜닝_김명수.md: -------------------------------------------------------------------------------- 1 | # 목차 2 | - [배치 처리 프로파일링하기](#배치-처리-프로파일링하기) 3 | - [VisualVM 알아보기](#VisualVM-알아보기) 4 | - [스프링 배치 애플리케이션 프로파일링하기](#스프링-배치-애플리케이션-프로파일링하기) 5 | - [잡 확장하기](#잡-확장하기) 6 | - [다중 스레드 스텝](#다중-스레드-스텝) 7 | - [병렬 스텝](#병렬-스텝) 8 | 9 | --- 10 | 11 | # 배치 처리 프로파일링하기 12 | 13 | ## VisualVM 알아보기 14 | 15 | **VisualVM 설치하기** 16 | 17 | - (1) https://visualvm.github.io/ 에서 다운로그 하기 18 | - (2) `$ brew install --cask visualvm` 19 | - (3) IntelliJ 20 | 21 | ![VisualVM Main](https://user-images.githubusercontent.com/25560203/140611476-8c77eb9d-372e-401c-8795-3db4fcc379a4.png) 22 | (VisualVM 메인화면) 23 | 24 | 현재 로컬에서 실행중인 IntelliJ를 클릭하면 아래와 같은 `Overview`, `Monitor`, `Threads`, `Sampler`, `Profiler`를 확인할 수 있다. 25 | 26 | - **Overview**: 27 | 28 | 실행 중인 자바 애플리케이션의 개요를 제공한다. main 클래스, 애플리케이션 이름, 프로세스 ID, JVM Arguments 등이 표시된다. 29 | 30 | ![VisualVM - Overview](https://user-images.githubusercontent.com/25560203/140611592-c506cc59-32dc-43ea-9d9b-cd526f587fc1.png) 31 | 32 | - **Monitor**: 33 | 34 | CPU 사용률, 메모리 사용률(Heap과 PermGen), 로딩된 클래스 수, 수행 중인 데몬 스레드 수를 보여주는 차트를 표시한다. GC를 수행할 수 있으며 힙 덤프도 가능한다. 35 | 36 | ![VisualVM - Monitor](https://user-images.githubusercontent.com/25560203/140611663-3efef801-14ba-4c2e-8eb1-6dc717670126.png) 37 | 38 | - **Threads** 39 | 40 | 애플리케이션이 실행한 모든 스레드와 해당 스레드가 어떤 작업을 하고 있는지(running, sleeping, waiting 또는 monitoring)와 관련된 정보를 표시한다. 41 | 해당 데이터는 타임라인 형식, 테이블 형식, 세부 정보 형식으로 표시된다. 42 | 43 | ![VisualVM - Threads](https://user-images.githubusercontent.com/25560203/140611750-595656d2-0c22-4312-b3f6-5e0e38af4638.png) 44 | 45 | - **Sampler** 46 | 47 | 애플리케이션의 CPU 사용률과 메모리 할당 상태를 순간적으로 잡아내 스냅샷을 만들 수 있다. CPU 사용률은 어떤 메소드가 얼마나 오래 실행되는지를 보여준다. 48 | 49 | 메모리 사용률은 어떤 클래스가 얼마나 많은 메모리를 사용하는지 보여준다. 50 | 51 | ![VisualVM - Sampler](https://user-images.githubusercontent.com/25560203/140611813-95f5ee5c-0d24-4bca-b3b3-83b458811355.png) 52 | 53 | --- 54 | 55 | ## 스프링 배치 애플리케이션 프로파일링하기 56 | 57 | 애플리케이션에서 프로파일링할 때는 일반적으로 두가지 중 하나를 살펴본다. 58 | 59 | - 어떤 부분에서 얼마나 많은 CPU를 사용하는가? 60 | - CPU가 어떤 작업을 하는가와 연관돼 있다 61 | - 잡이 어려운 계산을 수행하는가? 62 | - CPU가 비즈니스 로직이 아닌 다른 곳에 많은 노력을 들이고 있는가?(e.g: 실제 계산 수행보다 파일을 파싱하는데 많은 시간을 소비하는지?) 63 | - 무엇 때문에 얼마나 많은 메모리가 사용되는가? 64 | - 가용 메모리를 거의 다 소모했는가? 65 | - 무엇이 메모리를 가득 차지하고 있는가? 66 | - 컬렉션을 지연 로딩하지 않은 하이버네이트 객체로 인해 메모리가 가득차게 됐는가? 67 | 68 | 69 | ### CPU 프로파일링 70 | 71 | CPU 프로파일링을 위해 아래와 같이 `AccountItemProcessor`에 0~100만 사이의 모든 소수를 계산하자. 72 | 73 | ```java 74 | public class AccountItemProcessor implements ItemProcessor { 75 | 76 | @Override 77 | public Statement process(Statement item) throws Exception { 78 | // FOR PROFILE 79 | final int threadCount = 10; 80 | final CountDownLatch doneSignal = new CountDownLatch(threadCount); 81 | 82 | for (int i = 0; i < threadCount; i++) { 83 | final Thread thread = new Thread(() -> { 84 | try { 85 | for (int j = 0; j < 100000; j++) { 86 | new BigInteger(String.valueOf(j)).isProbablePrime(0); 87 | } 88 | } finally { 89 | doneSignal.countDown(); 90 | } 91 | }); 92 | thread.setDaemon(true); 93 | thread.start(); 94 | } 95 | doneSignal.await(); 96 | ... 97 | } 98 | } 99 | ``` 100 | 101 | ![VisualVM Monitor](https://user-images.githubusercontent.com/25560203/140643641-46d2f88b-69c6-4a01-a3c7-e1c683a6a98a.png) 102 | 103 | ![VisualVM CPU Profile](https://user-images.githubusercontent.com/25560203/140643798-4a1a1f94-f208-49ac-9326-3f76b14e1284.png) 104 | 105 | ### 메모리 프로파일링 106 | 107 | 아래와 같이 메모리를 증가하는 코드를 추가하자. 108 | 109 | ```java 110 | public class AccountItemProcessor implements ItemProcessor { 111 | 112 | @Override 113 | public Statement process(Statement item) throws Exception { 114 | String memoryBuster = "memoryBuster"; 115 | 116 | for (int i = 0; i < 200; i++) { 117 | memoryBuster += memoryBuster; 118 | } 119 | ... 120 | } 121 | } 122 | ``` 123 | 124 | ![Visual VM](https://user-images.githubusercontent.com/25560203/140644143-52288f54-a8c5-444d-a723-cbc3c4f0745c.png) 125 | 126 | Profiler 또는 Snapshot 이용하기 127 | 128 | --- 129 | 130 | # 잡 확장하기 131 | 132 | ## 다중 스레드 스텝 133 | 134 | ```java 135 | @Bean 136 | @StepScope 137 | public FlatFileItemReader fileTransactionReader( 138 | @Value("#{jobParameters['inputFlatFile']}") Resource inputFile) { 139 | 140 | return new FlatFileItemReaderBuilder() 141 | ... 142 | .saveState(false) // 상태를 저장하지 않는다 143 | ... 144 | .build(); 145 | } 146 | 147 | @Bean 148 | public Step step1() { 149 | return stepBuilderFactory.get("step1") 150 | ... 151 | .taskExecutor(new SimpleAsyncTaskExecutor()) // 매 청크마다 해당 Excutor에서 실행한다. 152 | ... 153 | .build(); 154 | } 155 | ``` 156 | 157 | 아래와 같이 별도의 Excutor에서 실행되는것을 확인할 수 있다. 158 | 159 | ```log 160 | 2021-11-07 21:33:03.313 INFO 8099 --- [cTaskExecutor-2] i.s.batch.multithread.MultiThreadMain : beforeChunk: execution#1 161 | 2021-11-07 21:33:03.314 INFO 8099 --- [cTaskExecutor-4] i.s.batch.multithread.MultiThreadMain : beforeChunk: execution#1 162 | 2021-11-07 21:33:03.313 INFO 8099 --- [cTaskExecutor-3] i.s.batch.multithread.MultiThreadMain : beforeChunk: execution#1 163 | 2021-11-07 21:33:03.314 INFO 8099 --- [cTaskExecutor-1] i.s.batch.multithread.MultiThreadMain : beforeChunk: execution#1 164 | 2021-11-07 21:33:03.502 INFO 8099 --- [cTaskExecutor-3] i.s.batch.multithread.MultiThreadMain : afterChunk: execution#1 165 | 2021-11-07 21:33:03.504 INFO 8099 --- [cTaskExecutor-5] i.s.batch.multithread.MultiThreadMain : beforeChunk: execution#1 166 | ``` 167 | 168 | ## 병렬 스텝 169 | 170 | ```java 171 | @Bean 172 | Job parallelStepsJob() { 173 | Flow secondFlow = new FlowBuilder("secondFlow") 174 | .start(step2()) 175 | .build(); 176 | 177 | Flow parallelFlow = new FlowBuilder("parallelFlow") 178 | .start(step1()) 179 | .split(new SimpleAsyncTaskExecutor()) 180 | .add(secondFlow) 181 | .build(); 182 | 183 | return jobBuilderFactory.get("parallelStepsJob") 184 | .incrementer(new RunIdIncrementer()) 185 | .start(parallelFlow) 186 | .end() 187 | .build(); 188 | } 189 | ``` 190 | 191 | ```shell 192 | 2021-11-09 19:43:38.429 INFO 24348 --- [cTaskExecutor-1] i.s.batch.parallel.ParallelStepMain : [Step2] beforeChunk: execution#12 193 | 2021-11-09 19:43:38.430 INFO 24348 --- [cTaskExecutor-2] i.s.batch.parallel.ParallelStepMain : [Step1] beforeChunk: execution#11 194 | 2021-11-09 19:43:38.703 INFO 24348 --- [cTaskExecutor-1] i.s.batch.parallel.ParallelStepMain : [Step2] beforeChunk: execution#12 195 | ``` -------------------------------------------------------------------------------- /12주차/요약본/[12주차]_11장_확장과튜닝_김민석.md: -------------------------------------------------------------------------------- 1 | # 11장 확장과 튜닝 2 | 3 | ## 배치 처리 프로파일링 하기 4 | 5 | 프로파일링 프로그램 : VisualVM 6 | 7 | CPU 프로파일링 : CPU 사용량을 모니터링하여 튜닝 8 | 9 | 메모리 프로파일링 : 메모리 사용량을 모니터링하여 튜닝 가능 10 | 11 | 12 | ## 잡 확장하기 13 | 14 | ### 다중 스레드 스텝 15 | 16 | 스텝은 기본적으로 단일 스레드. 17 | 18 | 다중 스레드 스텝은 잡의 실행을 병렬화하는 가장 쉬운 방법. 19 | 20 | ### 병렬 스텝 21 | 22 | 다중 스레드 스텝은 청크를 병렬로 처리하는 기능 제공. 23 | 24 | 병렬 스텝은 그텝 자체를 병렬로 처리 가능. 25 | 26 | ### 파티셔닝 27 | 28 | 파티셔닝은 마스터 스텝이 처리할 일을 여러 워커 스텝으로 넘기는 개념. 29 | 30 | 큰 데이터 셋을 파티션으로 나눠서 워커가 따로 처리. 31 | 32 | ### 원격 청킹 33 | 34 | 풀 모델 대신 푸시 모델로 동작. 35 | 36 | 메타데이터가 전송되는 원격 파티셔닝과 달리 원격 청크 방식에서는 처리할 실제 데이터가 네트워크를 통해 전송. 37 | 38 | -------------------------------------------------------------------------------- /12주차/요약본/[12주차]_11장_확장과튜닝_이호빈.md: -------------------------------------------------------------------------------- 1 | # 11장 확장과 튜닝 2 | 3 | ## VisualVM 4 | 5 | > JVM의 상태를 확인할 수 있는 도구 (CPU 프로파일링, 메모리 프로파일링) 6 | 7 | - 어디서 얼마나 많은 CPU를 사용하는지, 얼마나 많은 메모리를 사용하는지 알 수 있다 8 | - 예를 들어, CPU가 비즈니스 로직이 아닌 파싱하는데 시간을 더 들인다든지, 어떤 게 메모리를 가득 차게 만들었는지 등등을 알 수 있다 9 | 10 | ## 다중 스레드 스텝 11 | 12 | > 스텝의 Chunk 단위를 하나의 스레드로 처리하는 기능 13 | > 청크 하나마다 스레드 하나를 사용해 각 청크를 병렬로 실행하게 된다 14 | - 스텝은 기본적으로 단일 스레드로 처리된다. 이를 여러 스레드로 처리되도록 해보자 15 | - Job내의 모든 스텝은 각 청크를 독립적으로 처리하면서 하나의 스레드 풀 내에서 처리되도록 할 수 있다 16 | - 해당 스텝을 TaskExecutor를 참조하도록 구성하기만 하면 된다. 17 | - 각 청크용으로 새 스레드를 생성해 각 청크를 병렬로 실행한다. 18 | 19 | ```java 20 | // ItemReader쪽에 써준다 21 | .saveState(false) 22 | 23 | // stepBuilderFactory쪽에 써준다 24 | .taskExecutor(new SimpleAsyncTaskExecutor()) 25 | ``` 26 | 27 | ### 문제점 28 | 29 | - JobRepository의 상태가 공유되기 때문에 서로 다른 스레드가 다른 상태로 덮어쓰게 되는 문제가 발생할 수있다. 30 | - 입력 IO의 자원을 모두 소모하고 있다면 적용해도 성능이 나아지지 않을 수 있다 31 | 32 | ## 병렬 스텝 33 | 34 | > 스텝 자체 또는 스텝의 Flow를 하나의 스레드로 처리하는 기능 35 | 36 | - 각 플로우가 자체 스레드에서 실행되도록 할 수 있다. 37 | 38 | ```java 39 | // FlowBuilder 쪽에 써준다. 40 | .split(new SimpleAsyncTaskExecutor()) 41 | ``` 42 | 43 | ## AsyncItemProcessor, AsyncItemWriter 44 | 45 | > ItemProcessor에서만 새로운 스레드를 할당해줄 수 기법(ItemProcessor에서 복잡한 계산을 할 때 사용) 46 | 47 | - spring-batch-integration을 사용해야 한다 48 | - SimpleAsyncTaskExecutor가 아닌 ThreadPoolTaskExecutor같은 걸 사용하자 49 | 50 | ```java 51 | @Bean 52 | public AsyncItemProcessor asyncItemProcessor() { 53 | AsyncItemProcessor processor = new AysncItemProcessor<>(); 54 | 55 | processor.setDelegate(processor()); 56 | processor.setTaskExecutor(new SimpleAsyncTaskExecutor()); 57 | 58 | return processor; 59 | } 60 | 61 | @Bean 62 | public AsyncItemWriter asyncItemProcessor() { 63 | AsyncItemWriter processor = new AsyncItemWriter<>(); 64 | 65 | processor.setDelegate(writer(null)); 66 | 67 | return processor; 68 | } 69 | ``` 70 | 71 | ## 파티셔닝 72 | 73 | > 마스터 스텝이 처리할 일을 여러 워커 스텝으로 넘기는 기능 74 | 75 | 예를 들어, 100만개의 행이 존재한다면 100만개를 25만개씩 4개의 파티션으로 분리한다든가... 76 | 77 | - Partitioner 인터페이스와 MultiResourcePartitioner 구현체, PartitionHandler 인터페이스, PartitionHandler의 구현체를 사용한다 78 | - 파티션은 필수 데이터를 포함하는 ExecutionContext로 표현될 수 있다 79 | 80 | ### TaskExecutorPartitionHandler 81 | 82 | - 단일 JVM 내에서 여러 스레드를 사용해 워커를 실행할 수 있게 해주는 구현체 83 | - 여러 파일을 DB에 적재하거나 할 때 쓰일 수 있다 (입력 IO간 의존성이 없으니까) 84 | 85 | ### MessageChannelPartitionHandler 86 | 87 | - spring integration 의존성을 추가해야 한다. 88 | - rabbit mq와 같은 메시지 채널을 통해 각 워커에게 전달한다. 89 | 90 | ### DeployPartitionHandler 91 | 92 | - 일이 완료되는데 필요한 만큼 워커가 실행된다. 93 | 94 | ## 원격 청킹 95 | -------------------------------------------------------------------------------- /12주차/요약본/[12주차]_11장_확장과튜닝_함호식.md: -------------------------------------------------------------------------------- 1 | 11. 확장과 튜닝 2 | 3 | #### 배치 처리 프로파일링하기 4 | 5 | intelliJ plulgins 6 | ![VisualVm Plugins](./images/visualVM_plugin.png) 7 | 8 | run and debug 9 | ![VisualVm Plugins](./images/visualVM_run_debug.png) 10 | 11 | 12 | overview: 13 | -> 실행중인 자바 애플리케이션의 전체적인 내용을 제공하고, JVM에 전달된 arguments가 포함된다. 14 | ![VisualVm Plugins](./images/visualVM_overview.png) 15 | 16 | monitor: 17 | -> CPU, 메모리 사용률, 로딩된 클래스 수, 수행중인 데몬 스레드 수를 보여준다. 18 | -> 추후 분석을 위해 힙 덤프를 생성할 수도 있다. 19 | ![VisualVm Plugins](./images/visualVM_monitor.png) 20 | 21 | thread: 22 | -> 애플리케이션이 실행한 모든 스레드와 해당 스레드가 어떤 작업을 하는지 알려준다. 23 | ![VisualVm Plugins](./images/visualVM_thread.png) 24 | 25 | sampler: 26 | -> 애플리케이션의 CPU와 메모리 할당 상태를 순간적으로 잡아내, 스냅샷을 만들 수 있다. 27 | ![VisualVm Plugins](./images/visualVM_sampler.png) 28 | 29 | --- 30 | 31 | ```java 32 | // 메모리 사용률을 높이기 위해 test 문자열에 문자열 반복적 추가 33 | // thread 개수를 늘리기 위해 100개 생성 후 실 34 | @Bean 35 | public ItemWriter visualVMItemWriterLab1() { 36 | ExecutorService executorService = Executors.newFixedThreadPool(100); 37 | for (int i = 0; i < 100; i++) { 38 | executorService.submit(() -> { 39 | String test = "test"; 40 | for (int j = 0; j < 1000; j++) { 41 | test += "test"; 42 | System.out.println(test); 43 | } 44 | }); 45 | } 46 | 47 | executorService.shutdown(); 48 | try { 49 | executorService.awaitTermination(60, TimeUnit.SECONDS); 50 | } catch (InterruptedException e) { 51 | log.error("executorService shutdown error!"); 52 | } 53 | 54 | return items -> { 55 | log.info("ItemWriter items :: {}", items); 56 | }; 57 | } 58 | ``` 59 | 60 | 메모리 사용률이 늘어나고, 61 | live peak thread 수가 122까지 늘어남 62 | ![VisualVm Plugins](./images/visualVM_custome_test.png) 63 | 64 | --- 65 | 66 | #### 다중 스레드 스텝 67 | 68 | 해당 스텝이 TaskExecutor를 참조하도록 구성하면 된다. 69 | 70 | `taskExecutor(new SimpleAsyncTaskExecutor())` 71 | 스텝 내에서 실행되는 각 청크용으로 새 스레드를 생성해 각 청크를 병렬로 실행한다. 72 | +) 잡의 각 스텝 내에서 `아이템 청크`를 `병렬로 처리`하는 기능을 제공한다. 73 | 74 | ```java 75 | @Bean 76 | public Step multiThreadStepLab2() { 77 | return this.stepBuilderFactory.get("multiThreadStepLab2") 78 | .chunk(CHUNK_SIZE) 79 | .reader(multiThreadItemReaderLab2()) 80 | .writer(multiThreadItemWriterLab2()) 81 | .taskExecutor(new SimpleAsyncTaskExecutor()) 82 | .build(); 83 | } 84 | ``` 85 | 86 | 메인 스레드에서 실행 87 | ``` 88 | 2021-11-10 23:25:57.907 INFO 63081 --- [ main] h.c.l.j.MultiThreadStepConfigurationLab2 : ItemWriter items :: [test0, test1, test2, test3, test4] 89 | 2021-11-10 23:25:57.911 INFO 63081 --- [ main] h.c.l.j.MultiThreadStepConfigurationLab2 : ItemWriter items :: [test5, test6, test7, test8, test9] 90 | 2021-11-10 23:25:57.915 INFO 63081 --- [ main] h.c.l.j.MultiThreadStepConfigurationLab2 : ItemWriter items :: [test10, test11, test12, test13, test14] 91 | 2021-11-10 23:25:57.918 INFO 63081 --- [ main] h.c.l.j.MultiThreadStepConfigurationLab2 : ItemWriter items :: [test15, test16, test17, test18, test19] 92 | ``` 93 | 94 | 다중 스레드로 실행 (cTaskExecutor-1,2,3,4, ...) 95 | ``` 96 | 2021-11-10 23:30:37.210 INFO 63711 --- [cTaskExecutor-2] h.c.l.j.MultiThreadStepConfigurationLab2 : ItemWriter items :: [test0, test1, test5, test9, test13] 97 | 2021-11-10 23:30:37.210 INFO 63711 --- [cTaskExecutor-3] h.c.l.j.MultiThreadStepConfigurationLab2 : ItemWriter items :: [test0, test3, test7, test11, test15] 98 | 2021-11-10 23:30:37.210 INFO 63711 --- [cTaskExecutor-4] h.c.l.j.MultiThreadStepConfigurationLab2 : ItemWriter items :: [test0, test2, test4, test8, test12] 99 | 2021-11-10 23:30:37.210 INFO 63711 --- [cTaskExecutor-1] h.c.l.j.MultiThreadStepConfigurationLab2 : ItemWriter items :: [test0, test3, test6, test10, test14] 100 | ``` 101 | 102 | --- 103 | 104 | #### 병렬 스텝 105 | 106 | 서로 관련없는 스텝을 동시에 실행할 때 성능을 향상 시킬 수 있다. 107 | 108 | ```java 109 | @Bean 110 | public Job parallelJobLab3() { 111 | Flow secondFlow = new FlowBuilder("secondFlow") 112 | .start(parallelStepLab3_2()) 113 | .build(); 114 | 115 | Flow parallelFlow = new FlowBuilder("parallelFlow") 116 | .start(step1()) 117 | .split(new SimpleAsyncTaskExecutor()) 118 | .add(secondFlow) 119 | .build(); 120 | 121 | 122 | return this.jobBuilderFactory.get("parallelJobLab3") 123 | .start(parallelFlow) 124 | .end() 125 | .incrementer(new UniqueRunIdIncrementer()) 126 | .build(); 127 | } 128 | ``` 129 | 130 | 각 Step이 `cTaskExecutor-1`, `cTaskExecutor-2`에서 실행됨. 131 | ``` 132 | 2021-11-10 23:39:20.376 INFO 64702 --- [cTaskExecutor-2] o.s.batch.core.job.SimpleStepHandler : Executing step: [parallelStepLab3_1] 133 | 2021-11-10 23:39:20.378 INFO 64702 --- [cTaskExecutor-1] o.s.batch.core.job.SimpleStepHandler : Executing step: [parallelStepLab3_2] 134 | 2021-11-10 23:39:20.391 INFO 64702 --- [cTaskExecutor-1] m.h.c.l.j.ParallelStepConfigurationLab3 : ItemWriter items :: [test1] 135 | 2021-11-10 23:39:20.391 INFO 64702 --- [cTaskExecutor-2] m.h.c.l.j.ParallelStepConfigurationLab3 : ItemWriter items :: [test1] 136 | 2021-11-10 23:39:20.396 INFO 64702 --- [cTaskExecutor-1] o.s.batch.core.step.AbstractStep : Step: [parallelStepLab3_2] executed in 18ms 137 | 2021-11-10 23:39:20.396 INFO 64702 --- [cTaskExecutor-2] o.s.batch.core.step.AbstractStep : Step: [parallelStepLab3_1] executed in 19ms 138 | ``` -------------------------------------------------------------------------------- /12주차/요약본/images/visualVM_custome_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/book-spring-batch/294c684e3b1895e12b0b15700b9e819fb3693293/12주차/요약본/images/visualVM_custome_test.png -------------------------------------------------------------------------------- /12주차/요약본/images/visualVM_monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/book-spring-batch/294c684e3b1895e12b0b15700b9e819fb3693293/12주차/요약본/images/visualVM_monitor.png -------------------------------------------------------------------------------- /12주차/요약본/images/visualVM_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/book-spring-batch/294c684e3b1895e12b0b15700b9e819fb3693293/12주차/요약본/images/visualVM_overview.png -------------------------------------------------------------------------------- /12주차/요약본/images/visualVM_plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/book-spring-batch/294c684e3b1895e12b0b15700b9e819fb3693293/12주차/요약본/images/visualVM_plugin.png -------------------------------------------------------------------------------- /12주차/요약본/images/visualVM_run_debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/book-spring-batch/294c684e3b1895e12b0b15700b9e819fb3693293/12주차/요약본/images/visualVM_run_debug.png -------------------------------------------------------------------------------- /12주차/요약본/images/visualVM_sampler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/book-spring-batch/294c684e3b1895e12b0b15700b9e819fb3693293/12주차/요약본/images/visualVM_sampler.png -------------------------------------------------------------------------------- /12주차/요약본/images/visualVM_thread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/book-spring-batch/294c684e3b1895e12b0b15700b9e819fb3693293/12주차/요약본/images/visualVM_thread.png -------------------------------------------------------------------------------- /12주차/요약본/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /13주차/발표자료/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /13주차/요약본/[13주차]_13장_배치_처리_테스트하기_김광훈.md: -------------------------------------------------------------------------------- 1 | # 13장. 배치 처리 테스트하기 2 | ## Junit / Mockito 를 사용한 단위 테스트 3 | 단위 테스트는 격리된 단일 컴포넌트를 반복 가능한 방식으로 수행하는 테스트다. 4 | 5 | 배치 개발에는 아래와 같은 단위 테스트를 적용할 수 있다. 6 | 7 | #### - 단일 테스트 8 | 단 하나를 테스트한다. -> 최소 컴포넌트를 테스트 9 | 10 | #### - 격리 11 | 단위 테스트의 목표는 이러한 각 의존성의 통합을 테스트하는 것이 아니라, 개별 컴포넌트의 동작 방식을 테스트하는 것이다. 12 | 13 | #### - 반복 가능한 방식 14 | - 단위 테스트는 동일한 시나리오를 반복해서 수행할 수 있어야 한다. 그럼으로써 시스템 변경 시 회귀 테스트에 사용할 수 있다. 15 | 16 |
17 | 18 | ## Junit 생명주기 19 | Junit 테스트는 테스트 케이스라고 부른다. 20 | 21 | 목적 --> 클래스 레벨에서 특정한 기능성을 테스트하는 것이다. 22 | 23 | ## Mock 객체 24 | Stub 는 목 객체가 아니다. Stub 은 테스트에서 사용되는 하드코딩된 구현체로, 런타임시에 필요한 동작을 정의할 수 있는 목 객체가 재사용 가능한 구조인 경우에 사용되낟. 25 | 26 | 27 | ## 배치 테스트 28 | #### @SpringBatchTest 29 | 스프링 배치 잡을 테스트하는 데 사용할 수 있는 유틸리티를 제공한다. 30 | 31 | 책에서는 JobLauncherTestUtils 사용 -------------------------------------------------------------------------------- /13주차/요약본/[13주차]_13장_배치_처리_테스트하기_김민석.md: -------------------------------------------------------------------------------- 1 | # 13장 배치 처리 테스트하기 2 | 3 | ## 잡과 스코프 빈 테스트하기 4 | 5 | - 스프링 배치가 스텝 내에서 Execution을 에뮬레이터 하는 방법 6 | - TestExecutionListener 7 | - 일반적으로 StepScopeTestExecutionListener 사용 8 | - 테스트 케이스에서 팩토리 메서드 사용해 StepExecution 가져오고 반환된 컨텍스트를 현재 테스트 메서드의 컨텍스트로 사용(getStepExecution() 사용) 9 | - 각 테스트 메서드가 실행되는 동안 스텝 컨텍스트 제공 10 | 11 | 12 | - @SpringBatchTest 13 | - 자동 빈 추가 14 | - JobLauncherTestUtils 15 | - JobRepositoryTestUtils 16 | - StepScopeTestExecutionListener 17 | - JobScopeTestExecutionListener 18 | 19 | 20 | - 스텝 테스트하기 21 | 22 | ```java 23 | @ExtendWith(SpringExtension.class) 24 | @ContextConfiguration(classes = {ImportJobConfiguration.class, 25 | CustomerItemValidator.class, 26 | AccountItemProcessor.class}) 27 | @JdbcTest 28 | @EnableBatchProcessing 29 | @SpringBatchTest 30 | public class FlatFileItemReaderTests { 31 | 32 | @Autowired 33 | private FlatFileItemReader customerUpdateItemReader; 34 | 35 | public StepExecution getStepExecution() { 36 | JobParameters jobParameters = new JobParametersBuilder() 37 | .addString("customerUpdateFile", "classpath:customerUpdateFile.csv") 38 | .toJobParameters(); 39 | 40 | return MetaDataInstanceFactory.createStepExecution(jobParameters); 41 | } 42 | 43 | @Test 44 | public void testTypeConversion() throws Exception { 45 | this.customerUpdateItemReader.open(new ExecutionContext()); 46 | 47 | assertTrue(this.customerUpdateItemReader.read() instanceof CustomerAddressUpdate); 48 | assertTrue(this.customerUpdateItemReader.read() instanceof CustomerContactUpdate); 49 | assertTrue(this.customerUpdateItemReader.read() instanceof CustomerNameUpdate); 50 | } 51 | 52 | } 53 | ``` 54 | 55 | - 잡 테스트하기 56 | 57 | ```java 58 | @ExtendWith(SpringExtension.class) 59 | @ContextConfiguration(classes = {JobTests.BatchConfiguration.class, BatchAutoConfiguration.class}) 60 | @SpringBatchTest 61 | @Transactional(propagation = Propagation.NOT_SUPPORTED) 62 | public class JobTests { 63 | 64 | @Autowired 65 | private JobLauncherTestUtils jobLauncherTestUtils; 66 | 67 | @Test 68 | public void test() throws Exception { 69 | JobExecution jobExecution = 70 | this.jobLauncherTestUtils.launchJob(); 71 | 72 | assertEquals(BatchStatus.COMPLETED, 73 | jobExecution.getStatus()); 74 | 75 | StepExecution stepExecution = 76 | jobExecution.getStepExecutions().iterator().next(); 77 | 78 | assertEquals(BatchStatus.COMPLETED, stepExecution.getStatus()); 79 | assertEquals(3, stepExecution.getReadCount()); 80 | assertEquals(3, stepExecution.getWriteCount()); 81 | } 82 | 83 | @Configuration 84 | @EnableBatchProcessing 85 | public static class BatchConfiguration { 86 | 87 | @Autowired 88 | private JobBuilderFactory jobBuilderFactory; 89 | 90 | @Autowired 91 | private StepBuilderFactory stepBuilderFactory; 92 | 93 | @Bean 94 | public ListItemReader itemReader() { 95 | return new ListItemReader<>(Arrays.asList("foo", "bar", "baz")); 96 | } 97 | 98 | @Bean 99 | public ItemWriter itemWriter() { 100 | return (list -> { 101 | list.forEach(System.out::println); 102 | }); 103 | } 104 | 105 | @Bean 106 | public Step step1() { 107 | return this.stepBuilderFactory.get("step1") 108 | .chunk(10) 109 | .reader(itemReader()) 110 | .writer(itemWriter()) 111 | .build(); 112 | } 113 | 114 | @Bean 115 | public Job job() { 116 | return this.jobBuilderFactory.get("job") 117 | .start(step1()) 118 | .build(); 119 | } 120 | 121 | @Bean 122 | public DataSource dataSource() { 123 | return new EmbeddedDatabaseBuilder().build(); 124 | } 125 | 126 | } 127 | } 128 | ``` -------------------------------------------------------------------------------- /13주차/요약본/[13주차]_13장_배치_처리_테스트하기_함호식.md: -------------------------------------------------------------------------------- 1 | 13. 배치 처리 테스트하기 2 | 3 | #### 스프링 배치 테스트하기 4 | 5 | 1. junit을 사용하여 테스트 케이스를 작성한다. 6 | 2. JobLauncherTestUtils를 이용하여 Step이나 Job을 실행시켜, 기대한대로 동작하는지 확인한다. 7 | 3. SpringBatchTest 기능을 이용하여 StepExecutionContext에 접근하여 기대한 값을 갖고 있는지 확인한다. 8 | 9 | 테스트 진행시 클래스 및 역할 10 | * ForTestConfigurationJobTest : 테스트 클래스 11 | * ContextConfiguration 어노테이션 : 테스트할 클래스의 Bean 등록 역할 12 | * ForTestConfigurationLab1 : 테스트에서 기대한대로 작동할 지 확인할 Job 13 | * TestConfiguration : JobLauncherTestUtils의 Bean 등록을 위한 Configuration 클래스 14 | 15 | ```java 16 | @ContextConfiguration(classes = { 17 | ForTestConfigurationLab1.class, 18 | TestConfiguration.class 19 | }) 20 | @RunWith(SpringRunner.class) 21 | @SpringBatchTest 22 | @EnableBatchProcessing 23 | @EnableAutoConfiguration 24 | public class ForTestConfigurationJobTest { 25 | 26 | @Autowired 27 | private Job forTestJobLab1; 28 | 29 | @Autowired 30 | private JobLauncherTestUtils jobLauncherTestUtils; 31 | 32 | @Autowired 33 | private ListItemReader forTestItemReaderLab1; 34 | 35 | // Reader에서 읽어온 값이 기대한 값과 동일한지 확인한다. 36 | @Test 37 | public void itemReaderTest() { 38 | int index = 1; 39 | String expect = "test"; 40 | String actual = forTestItemReaderLab1.read(); 41 | 42 | while (actual != null) { 43 | assertEquals(expect + index, actual); 44 | actual = forTestItemReaderLab1.read(); 45 | index++; 46 | } 47 | } 48 | 49 | // Step이 정상적으로 종료되는지 확인한다. 50 | @Test 51 | public void stepTest() { 52 | JobParameters jobParameters = new JobParameters(); 53 | 54 | jobLauncherTestUtils.setJob(forTestJobLab1); 55 | JobExecution jobExecution = jobLauncherTestUtils.launchStep("forTestStepLab1", jobParameters); 56 | 57 | assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus()); 58 | } 59 | 60 | // Step > ItemReader의 readCount를 확인한다. 61 | // Job이 정상적으로 끝나는지 확인한다. 62 | @Test 63 | public void jobTest() { 64 | JobParameters jobParameters = new JobParameters(); 65 | 66 | jobLauncherTestUtils.setJob(forTestJobLab1); 67 | JobExecution jobExecution = jobLauncherTestUtils.launchStep("forTestStepLab1", jobParameters); 68 | 69 | StepExecution stepExecution = jobExecution.getStepExecutions().iterator().next(); 70 | 71 | assertEquals(2, stepExecution.getReadCount()); 72 | assertEquals(ExitStatus.COMPLETED,jobExecution.getExitStatus()); 73 | } 74 | } 75 | ``` 76 | 77 | ```java 78 | @Slf4j 79 | @Configuration 80 | @RequiredArgsConstructor 81 | public class ForTestConfigurationLab1 { 82 | private final JobBuilderFactory jobBuilderFactory; 83 | private final StepBuilderFactory stepBuilderFactory; 84 | 85 | private static final int CHUNK_SIZE = 5; 86 | 87 | @Bean 88 | public Job forTestJobLab1() { 89 | return this.jobBuilderFactory.get("forTestJobLab1") 90 | .start(forTestStepLab1()) 91 | .incrementer(new UniqueRunIdIncrementer()) 92 | .build(); 93 | } 94 | 95 | @Bean 96 | public Step forTestStepLab1() { 97 | return this.stepBuilderFactory.get("forTestStepLab1") 98 | .chunk(CHUNK_SIZE) 99 | .reader(forTestItemReaderLab1()) 100 | .writer(forTestItemWriterLab1()) 101 | .build(); 102 | } 103 | 104 | @Bean 105 | public ListItemReader forTestItemReaderLab1() { 106 | return new ListItemReader<>(Arrays.asList("test1", "test2")); 107 | } 108 | 109 | @Bean 110 | public ItemWriter forTestItemWriterLab1() { 111 | return items -> log.info("ItemWriter items :: {}", items); 112 | } 113 | } 114 | ``` 115 | 116 | ```java 117 | package me.ham.configuration; 118 | 119 | import lombok.RequiredArgsConstructor; 120 | import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; 121 | import org.springframework.batch.core.repository.JobRepository; 122 | import org.springframework.batch.test.JobLauncherTestUtils; 123 | import org.springframework.context.annotation.Bean; 124 | import org.springframework.context.annotation.Configuration; 125 | 126 | @Configuration 127 | @EnableBatchProcessing 128 | @RequiredArgsConstructor 129 | public class TestConfiguration { 130 | private final JobRepository jobRepository; 131 | 132 | @Bean 133 | JobLauncherTestUtils jobLauncherTestUtils() { 134 | JobLauncherTestUtils jobLauncherTestUtils = new JobLauncherTestUtils(); 135 | jobLauncherTestUtils.setJobRepository(jobRepository); 136 | return jobLauncherTestUtils; 137 | } 138 | } 139 | ``` 140 | 141 | -------------------------------------------------------------------------------- /13주차/요약본/[13주차]_13장_배치처리테스트하기_김성모.md: -------------------------------------------------------------------------------- 1 | # 13장. 배치 처리 테스트하기 2 | 3 | ## JUnit과 Mockito를 사용한 단위 테스트 4 | 5 | > 단위 테스트란? 격리된 단일 컴포넌트를 반복 가능한 방식으로 수행하는 테스트 6 | 7 | 단위테스트 시 다음의 정의를 구체화한다. 8 | 9 | - 단일 테스트 : 단 하나를 테스트한다. 최소 컴포넌트를 테스트하며 메소드 단위라고 보면 된다. 10 | - 격리 : 의존성은 시스템 테스트 시 혼란을 줄 수 있다. 따라서 각 의존성의 통합을 테스트하지 않고 개별 컴포넌트의 동작 방식을 테스트하는 것이다. 11 | - 반복 가능한 방식 : 브라우저를 매번 띄워 클릭하는 것이 아니라 동일한 시나리오를 반복해서 수행할 수 있어야 한다. 12 | 13 | ### JUnit 14 | > 표준화된 단위테스트를 할 수 있는 기능을 제공하는 간단한 프레임 워크이다. 15 | 16 | #### Junit 생명주기 17 | 18 | 생성자가 아규먼트를 갖지 않는 클래스를 만들고 @Test 어노테이션을 붙여서 테스트 메소드를 만들 수 있다. 19 | 20 | 각 테스트는 메서드 이전에 실행할 것과 이 후에 실행할 것으로 나눌 수 있고 21 | 이때는 각각 `@BeforeEach` 와 `@AfterEach` 를 사용할 수 있고 `public void` 상태여야 한다. 22 | 23 | #### Mock 객체 24 | 25 | 배치 잡이 수십 개 이상 클래스를 필요로 할 수 있고 외부 시스템에 의존할 수도 있다. 26 | 이런 모든 가변적인 영역을 관리하기가 어려우므로 목객체를 사용해 외부 의존성의 영향없이 비즈니스 로직을 실행한다. 27 | 28 | 목 객체에 접근 방식에는 2가지가 있다. 29 | 1. 프록시 기반 30 | 2. 클래스 재매핑 방식 31 | 32 | 33 | ## 스프링 배치의 유틸리티를 사용한 통합 테스트 34 | 35 | ### 스프링을 사용해 통합 테스트하기 36 | 37 | > 코어 스프링의 통합 테스트 기능을 사용해 통합 테스트를 수행하는 두 가지 주요 사용 사례는 다음과 같다. 38 | 39 | 1. 데이터베이스와 상호작용을 테스트하는 것 40 | 2. 스프링 빈과의 상호작용을 테스트하는 것 41 | 42 | ### 스프링 배치 테스트하기 43 | 44 | #### 잡과 스텝 스코프 빈 테스트하기 45 | - @ExtendWith(SpringExtenstion.class) : JUnit 5가 제공하는 스프링의 기능을 사용할 수 있게 한다. 46 | - @JdbcTest : 데이터 베이스 테스트를 할 때 테스트 관련 기능을 제공한다. 47 | - @ContextConfiguration : ApplicationContext를 빌드하는데 필요한 클래스를 제공한다. 48 | - @SpringBatchTest : 스프링 배치를 쉽게 테스트할 수 있는 유틸리티(JobLauncherTestUtils)를 제공한다. 49 | - @Transactional(propagation=Propagation.NOT_SUPPORTED) : 메서드 완료 시 롤백한다. @JdbcTest 애너테이션의 트래잭션 기능이 동작하지 않게 비활성화한다. -------------------------------------------------------------------------------- /13주차/요약본/tmp.md: -------------------------------------------------------------------------------- 1 | # test 2 | ## G 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Batch Book Study 2 | 3 | - [Github](https://github.com/Meet-Coder-Study) 4 | - [책 링크](http://www.yes24.com/Product/Goods/99422216) 5 | - [모집 링크](https://forms.gle/C1T3C28Cf9WQifbAA) 6 | 7 | ## 📆 기간 8 | 9 | - 모집 기간 : 2021. 08. 10(화) ~ 2021. 08. 13(금) 10 | - 일정 : 2021. 08. 19(목) ~ 2021. 11. 18(수) - 총 14주 11 | - 매주 목요일 21시에 Zoom으로 진행합니다. 12 | - 첫째주는 간단한 OT를 진행합니다. 13 | 14 | ## 🖐 모집 인원 및 장수 15 | 16 | - 총 7명 17 | - 총 13장으로 1주일에 1장씩 진행합니다. 18 | 19 | ## 📜 진행방식 20 | 21 | - 하나의 장 마다 하나의 Github Issue가 등록 됩니다. 22 | - 각 아이템들을 공부하다가 이해가 되지 않거나 토론할 내용이 있다면 Comment를 남겨주세요. 23 | - 해당 Comment에 대해서는 자유롭게 답변을 달아 주시면 됩니다. 24 | - 매주 화요일 00시 전에 발표자는 발표 자료를 PR로 올립니다. 25 | - 매주 수요일 00시 전에 그 외 인원은 예제 코드와 주석으로 간단한 정리 자료 그리고 질문 한 개씩 PR로 올립니다. 26 | - 발표에 대한 내용은 아래를 참고해주세요. 27 | - 발표가 모두 끝나면 20분 내외의 회고를 진행합니다. 28 | - 회고에 대한 내용은 아래를 참고해주세요. 29 | 30 | ## 🖥 발표 31 | 32 | - 4주마다 발표자를 미리 정해서 진행합니다. 33 | - 발표자는 발표자료를 만드는 작업을 진행합니다. 34 | - 그 외 인원은 예제 코드 작성 및 주석으로 간단한 정리를 해 PR을 올립니다. 35 | - 시작 시 발표자는 발표자료를 이용해 발표를 진행합니다. 36 | - 라이브 코딩도 좋습니다. 37 | 38 | ## 📚 Github 저장소 39 | 40 | - PR에 대한 머지는 직접 운영진이 할 예정입니다. 41 | - 발표자료의 파일명은 아래와 같이 해주세요. 42 | 43 | ```java 44 | [X주차]_X장_제목_이름 45 | ``` 46 | 47 | - PR을 올려주실때는 포크 저장소를 이용해주세요. 48 | - PR은 발표자료 파일명과 동일하게 올려주세요. 49 | ```java 50 | [X주차]_X장_제목_이름 51 | ``` 52 | 53 | ## 👨‍👩‍👧‍👦 회고 54 | 55 | - 1주일 동안 책을 읽으면서 느낀점, 발표를 보고 느낀점 들을 자유롭게 이야기 해주시면 됩니다. 56 | - 자유롭게 각자 하고 싶은 말을 하면 되니 부담 가지실 필요는 없습니다. 57 | 58 | ## 💰 보증금 및 벌금 59 | 60 | - 보증금 2만원을 첫 시작 때 운영진에게 입금합니다. 61 | - 발표자료를 기한 내에 PR하지 않은 경우 벌금으로 차감됩니다. 62 | - 벌금은 4000원입니다. 63 | - 마지막 모임에서 남은 금액에 대해 환급을 받는다. 64 | - 모든 벌금은 마지막 모임 시, 오프라인 모임 시 사용됩니다.(스터디룸 비 및 회식 비) 65 | - 중도 하차하신 분은 보증금을 돌려주지 않습니다. 66 | 67 | ## 기타 68 | 69 | - 커뮤니케이션을 위해 Slack을 사용합니다. 70 | 71 | ## Q&A 72 | --------------------------------------------------------------------------------