1. Spring Batch 개념
1.1. Spring Batch란?
•
대량의 데이터를 일괄 처리(Batch Processing)하기 위한 경량 프레임워크
•
특정 시간에 많은 데이터를 효율적으로 처리하는 것이 목적
•
실시간 서비스 대비 개발 부담이 적다는 인식이 있으나, 대량 데이터 처리 시 성능 최적화 필수
1.2. 주요 구성 요소
•
Job (잡)
◦
전체 배치 작업을 의미하는 최상위 단위
◦
여러 개의 Step으로 구성 가능
◦
Job Repository에 메타 정보 저장
•
Step (스텝)
◦
Job을 구성하는 단위 작업
◦
Reader, Processor, Writer로 구성
◦
Processor는 선택사항, Reader와 Writer는 필수
•
Reader (리더)
◦
데이터를 읽어오는 역할
◦
DB, 파일, API 등 다양한 소스에서 데이터 조회
◦
한 건씩 데이터를 반환
•
Processor (프로세서)
◦
Reader에서 받은 데이터를 가공/변환하는 역할
◦
필수 구성 요소는 아님
◦
단순 변환 로직만 있다면 사용 가능, IO 작업 포함 시 성능 저하 원인
•
Writer (라이터)
◦
처리된 데이터를 저장하는 역할
◦
DB, 파일, API 등으로 데이터 저장
◦
Chunk 단위로 일괄 처리
1.3. Chunk Processing
1.3.1. 개념
•
대량 데이터를 Chunk Size 기준으로 분할하여 처리
•
메모리 부하 방지 및 커넥션 타임아웃 해결
•
100만 건을 한 번에 처리하지 않고, 1000건씩 1000번 나누어 처리
1.3.2. 동작 방식
1.
Reader가 데이터를 한 건씩 읽음
2.
Processor가 한 건씩 가공
3.
Chunk Size만큼 반복 (예: 1000건)
4.
Writer가 Chunk Size만큼 모인 데이터를 일괄 저장
5.
전체 데이터가 처리될 때까지 1~4 반복
•
코드 레벨 동작
외부 루프 (전체 데이터 크기만큼):
내부 루프 (Chunk Size만큼):
- Reader에서 아이템 읽기
- Processor에서 가공
- 리스트에 추가
Writer에서 리스트 일괄 저장
Plain Text
복사
1.4. 일반적인 배치 사용 사례
•
일괄 생성
◦
Read → Aggregation → Write
◦
기존 데이터를 조합하여 새로운 데이터 생성
◦
예: 주문 정보 + 사용자 정보 → 주문자 정보 생성
•
일괄 수정
◦
Read → Update → Write
◦
저장된 데이터를 일괄 수정
◦
예: 배송 정보 기반 주문 정보 업데이트
•
통계 데이터 생성
◦
Read → Aggregation → Write
◦
데이터를 집계하여 통계 생성
◦
예: 상품별 주문 금액 합산
2. Spring Batch 성능 저하 원인
2.1. 반복적인 IO 작업
•
배치 성능 저하의 주요 원인
•
Chunk Size만큼 IO 작업이 반복되면 성능 저하 발생
•
네트워크 IO와 데이터베이스 IO가 주요 병목 지점
2.2. 네트워크 IO 대기
•
문제점
◦
Processor에서 API 호출 시 응답 대기 상태 발생
◦
Chunk Size만큼 순차적으로 API 호출 반복
◦
예: 150ms API × 1000건 = 150초 (2.5분) 대기
•
구조적 문제
◦
Processor는 단건 처리 구조
◦
API 응답 완료까지 대기 상태로 남음
◦
병렬 처리 불가
2.3. 데이터베이스 IO 반복
•
문제점
◦
Chunk Size만큼 개별 UPDATE/INSERT 쿼리 실행
◦
IO 작업 빈도수가 높을수록 성능 저하
◦
JPA Dirty Checking 사용 시 성능 손실 증가
•
예시
◦
Chunk Size 1000일 때 최대 1000번의 DB IO 발생
◦
데이터 증가에 비례하여 처리 시간 비선형적 증가
2.4. JPA Paging ItemReader의 LIMIT/OFFSET 문제
•
성능 저하
◦
뒷 페이지로 갈수록 조회 속도 급격히 감소
◦
MySQL이 5천만 번째 데이터를 찾는 데 큰 부담
◦
데이터 증가에 비례하여 처리 시간 비선형적 증가
•
성능 측정 예시
◦
10만 건: 18초
◦
50만 건: 235초 (5배 증가 시 13배 시간 소요)
◦
300만 건: 112분
2.5. 쿼리 의존적 Aggregation의 문제
•
GROUP BY SUM 쿼리의 한계
◦
여러 테이블 조인 시 실행 계획 복잡도 증가
◦
Temporary Table 생성 및 File Sort 발생
◦
튜닝 난이도 높고, 카디널리티 변화 시 실행 계획 변경
•
인덱스 추가의 부작용
◦
튜닝을 위한 인덱스 추가 시 INSERT/UPDATE 성능 저하
◦
저장 용량 증가
◦
Reader 개선 효과 무용지물
2.6. 배치 퍼포먼스에 대한 무관심
•
일반적인 문제
◦
배치 개발을 쉽게 생각하는 경향
◦
배포 후 모니터링 부족
◦
배치 전용 APM 툴 부재로 문제 인지 어려움
•
데이터 증가의 영향
◦
초기에는 문제 없던 배치가 데이터 증가 시 급격한 성능 저하
◦
예: 25만 건 → 1억 건 증가 시, 개선 없이는 1시간 → 400시간 소요
3. 성능 최적화 전략: Read 개선
3.1. Zero Offset ItemReader
•
개념
◦
OFFSET을 항상 0으로 유지하여 LIMIT/OFFSET 성능 저하 해결
◦
PK 기준 오름차순 정렬 후, PK 조건을 자동 추가
•
동작 방식
1.
PK 기준 오름차순 정렬
2.
첫 페이지: WHERE PK > 0 LIMIT 1000
3.
두 번째 페이지: WHERE PK > 1000 LIMIT 1000
4.
세 번째 페이지: WHERE PK > 2000 LIMIT 1000
•
장점
◦
뒷 페이지로 가도 OFFSET이 항상 0
◦
조회 속도 일정 유지
◦
데이터 증가에 비례한 선형적 성능 증가
•
구현
◦
QueryDSL과 결합하여 쿼리 구현
◦
Zero Offset 동작 방식으로 자동 변환
◦
편리하고 안전한 쿼리 구현 가능
•
예시 코드 (QueryDSL Zero Offset)
fun zeroOffsetReader(lastId: Long?, chunkSize: Int): List<UserDto> =
queryFactory
.select(
QUserDto(
user.id,
user.name,
user.grade
)
)
.from(user)
.where(lastId?.let { user.id.gt(it) } ?: user.id.gt(0))
.orderBy(user.id.asc())
.limit(chunkSize.toLong())
.fetch()
Kotlin
복사
3.2. Cursor ItemReader
•
개념
◦
LIMIT/OFFSET 대신 Cursor 사용
◦
데이터가 없을 때까지 일정 개수씩 반복 제공
◦
Chunk Processing에 적합
•
주의사항: JPA Cursor ItemReader 사용 금지
◦
데이터를 모두 메모리에 로드 후 서버에서 Iterator로 커서 대량 데이터 처리 시 OOM 유발
◦
절대 사용 금지
•
추천: JDBC/Hibernate Cursor ItemReader
◦
MySQL 서버 커서 방식 동작
◦
필요할 때마다 일정 개수만큼 데이터 가져옴
◦
메모리 안정성 확보
•
단점
◦
쿼리를 HQL 또는 Native Query 문자열로 구현 필요
◦
가독성 및 유지보수성 저하
•
예시 코드 (Hibernate Cursor)
@Bean
fun cursorItemReader(entityManagerFactory: EntityManagerFactory): HibernateCursorItemReader<UserEntity> =
HibernateCursorItemReaderBuilder<UserEntity>()
.name("userCursorReader")
.sessionFactory(entityManagerFactory.unwrap(SessionFactory::class.java))
.queryString("select u from UserEntity u where u.status = 'ACTIVE'")
.fetchSize(1000)
.build()
Kotlin
복사
3.3. Exposed Cursor ItemReader
•
Exposed 소개
◦
JetBrains 기반 ORM 프레임워크
◦
DSL 방식과 DAO 방식 지원
◦
코틀린 호환성 우수 (자바 사용 불가)
◦
웬만한 RDBMS 지원
•
Exposed DSL 장점
◦
쿼리를 코드로 구현 (문자열 아님)
◦
코틀린 특성 활용한 세련된 쿼리 구현
◦
배치 인서트 지원
•
구현
◦
Exposed DSL로 쿼리 구현
◦
JDBC Cursor ItemReader로 동작
◦
성능과 개발 생산성 모두 확보
•
예시 코드 (Exposed Cursor)
object Users : Table("users") {
val id = long("id")
val name = varchar("name", 100)
val grade = varchar("grade", 20)
}
fun exposedCursor(chunkSize: Int, lastId: Long?): List<ResultRow> =
transaction {
Users
.select { Users.id greater (lastId ?: 0) }
.orderBy(Users.id to SortOrder.ASC)
.limit(chunkSize)
.toList()
}
Kotlin
복사
3.4. Reader 성능 비교
•
JPA Paging ItemReader는 데이터 증가 시 비선형적 성능 저하
•
QueryDSL Zero Offset과 Exposed Cursor는 선형 증가로 예측 가능한 성능
•
대량 처리 시 안정적인 메모리 사용 및 GC 발생 확인
4. 성능 최적화 전략: Write 개선
4.1. Batch Insert 사용
•
개념
◦
여러 건의 INSERT를 묶어서 한 번에 전송
◦
네트워크 레이턴시 최소화
◦
Chunk Size 단위로 DB IO를 1회로 감소
•
성능 측정 결과
◦
500만 건 저장:
▪
JPA 단독 저장: 90분
▪
JDBC Batch Insert: 2분 내외
◦
45배 성능 향상
•
JDBC Batch Insert 구현
fun batchInsert(users: List<UserDto>, chunkSize: Int, dataSource: DataSource) {
dataSource.connection.use { conn ->
conn.autoCommit = false
conn.prepareStatement(
"INSERT INTO users(id, name, grade) VALUES (?, ?, ?)"
).use { ps ->
users.chunked(chunkSize).forEach { chunk ->
chunk.forEach { user ->
ps.setLong(1, user.id)
ps.setString(2, user.name)
ps.setString(3, user.grade)
ps.addBatch()
}
ps.executeBatch()
}
}
conn.commit()
}
}
Kotlin
복사
•
유지보수성 문제
◦
SQL 문자열 관리 부담
◦
가독성 저하
4.2. Exposed Batch Insert
•
장점
◦
쿼리를 코드로 구현 (DSL 방식)
◦
가시적이고 깔끔한 구현
◦
JDBC Batch Insert와 동일한 성능
•
추천
◦
JDBC Batch Insert가 부담스럽다면 Exposed 사용 권장
◦
코틀린 프로젝트에서 특히 유용
•
예시 코드 (Exposed Batch Insert)
fun exposedBatchInsert(users: List<UserDto>) {
transaction {
Users.batchInsert(users) { user ->
this[Users.id] = user.id
this[Users.name] = user.name
this[Users.grade] = user.grade
}
}
}
Kotlin
복사
4.3. 배치 환경에서 JPA가 부적합한 이유
•
Dirty Checking 및 영속성 관리 불필요
◦
배치는 Read와 Write 구간이 명확히 분리
◦
JPA의 복잡한 영속성 관리 및 Dirty Checking 불필요
◦
스냅샷 비교 후 UPDATE SQL 생성하는 복잡한 체크 로직으로 성능 손실
→ 개선 방안
◦
QueryDSL Projections로 DTO 조회
◦
영속성 관리 및 Dirty Check 회피
◦
성능 개선
•
불필요한 컬럼 업데이트
◦
JPA는 특정 컬럼만 업데이트하는 것이 아니라 모든 컬럼 업데이트
◦
배치 특성상 원하는 쿼리가 명확한데 다른 쿼리 실행은 리스크
◦
Dynamic Update 기능도 쿼리 동적 생성으로 성능 저하 유발
→ 개선 방안
◦
필요한 컬럼만 명시적으로 업데이트하는 쿼리 작성
◦
JPA 대신 JDBC 또는 Exposed 사용
•
Batch Insert 지원 제한
◦
ID 생성 전략이 IDENTITY일 경우 Batch Insert 미지원
◦
실무에서 IDENTITY 사용 빈도가 높아 사실상 Batch Insert 불가
◦
Batch Insert는 대량 처리 필수 기능
•
결론
◦
대량 데이터 Write 시 JPA 포기 권장
◦
JDBC Batch Insert 또는 Exposed Batch Insert 사용
5. 성능 최적화 전략: Network I/O 개선
5.1. Processor 제거 및 Parallel 처리
•
문제점
◦
Processor는 단건 처리 구조
◦
API 호출 시 응답 대기로 인한 시간 낭비
◦
Chunk Size만큼 순차적 API 호출 반복
•
해결 방안
◦
Processor 제거
◦
Writer에서 Processor 역할 수행
◦
API 호출을 병렬 처리로 전환
•
병렬 처리 구현
◦
RxKotlin, Reactor, Coroutine 등 활용
◦
아이템 리스트를 여러 레일(Rail)로 분할
◦
각 레일을 병렬 처리 후 Sequencer로 병합
suspend fun parallelWrite(
users: List<UserDto>,
httpClient: HttpClient,
degree: Int = Dispatchers.IO.limitedParallelism(8).parallelism
) = coroutineScope {
users.chunked(degree).map { chunk ->
async(Dispatchers.IO) {
chunk.map { user ->
httpClient.get("<https://api.example.com/orders/${user.id}>")
}
}
}.awaitAll().flatten()
}
Kotlin
복사
•
레일 개수 결정
◦
◦
DB 커넥션 개수, CPU 리소스 고려
◦
무작정 늘린다고 빨라지지 않음 (레일 분할/병합 리소스 소모)
•
성능 결과
◦
10개 레일 사용 시 약 10배 성능 개선
•
JPA 사용 시 권장사항
◦
영속성 컨텍스트의 성능 단점이 큼
◦
Projection 사용하여 Dirty Checking 회피 권장
5.2. Processor 사용 가이드
•
단순 데이터 변환 또는 애플리케이션 로직만 있다면 사용 가능
•
IO 작업 포함 시 성능 저하 발생하므로 제거 권장
6. 성능 최적화 전략: DB I/O 최소화
6.1. WHERE IN 기반 업데이트
•
기존 문제
◦
Chunk Size만큼 개별 UPDATE 쿼리 실행
◦
1000건 처리 시 최대 1000번 DB IO 발생
•
해결 방안
◦
등급별로 그룹화
◦
UPDATE table SET grade = 'VIP' WHERE id IN (1, 2, 3, ...)
◦
Chunk Size와 무관하게 최대 등급 개수만큼만 DB IO 발생
•
효과
◦
1000건 기준: 최대 1000번 → 3번 (등급이 3개일 경우)
◦
DB IO 압도적 감소
•
적용 가능 조건
◦
업데이트 대상을 그룹화할 수 있을 때
◦
모든 레코드에 동일한 값을 설정하는 경우
•
예시 코드 (등급별 IN 업데이트)
fun updateGradeByGroup(
grouped: Map<String, List<Long>>,
dataSource: DataSource
) {
dataSource.connection.use { conn ->
conn.autoCommit = false
grouped.forEach { (grade, ids) ->
conn.prepareStatement(
"UPDATE users SET grade = ? WHERE id IN (${ids.joinToString(",")})"
).use { ps ->
ps.setString(1, grade)
ps.executeUpdate()
}
}
conn.commit()
}
}
Kotlin
복사
6.2. JDBC Batch를 통한 DB I/O 최소화
•
한계 상황
◦
유저마다 다른 값 업데이트 시 WHERE IN 불가
◦
예: 유저별 등급 포인트 점수가 모두 다른 경우
•
JDBC Batch 동작
◦
여러 건의 UPDATE/INSERT를 묶어서 한 번에 전송
◦
Chunk Size 단위로 DB IO를 단 1회로 감소
◦
JDBC 2.0 Batch 기능 활용
•
코드 구현
fun batchUpdatePoints(
users: List<UserPointDto>,
chunkSize: Int,
dataSource: DataSource
) {
dataSource.connection.use { conn ->
conn.autoCommit = false
conn.prepareStatement(
"UPDATE users SET point = ? WHERE id = ?"
).use { ps ->
users.chunked(chunkSize).forEach { chunk ->
chunk.forEach { user ->
ps.setLong(1, user.point)
ps.setLong(2, user.id)
ps.addBatch()
}
ps.executeBatch()
}
}
conn.commit()
}
}
Kotlin
복사
•
성능 측정 결과 (순수 UPDATE만)
◦
1만 건: 12분 → 2분
◦
5만 건: 65분 → 10분
◦
10만 건: 120분 → 21분
•
Exposed를 통한 Batch Update
◦
SQL 문자열 관리 부담 해소
◦
유지보수성 향상
◦
코틀린 프로젝트에서 권장
7. 성능 최적화 전략: Aggregation 개선
7.1. 쿼리 의존적 Aggregation 문제
•
GROUP BY SUM의 한계
◦
여러 테이블 조인 시 실행 계획 복잡
◦
Temporary Table 생성 및 File Sort 발생
◦
튜닝 난이도 높음
◦
카디널리티 변화 시 실행 계획 변경 위험
•
인덱스 추가의 부작용
◦
INSERT/UPDATE 성능 저하
◦
저장 용량 증가
◦
Reader 개선 효과 무용지물
7.2. Redis를 활용한 직접 Aggregation
•
개념
◦
GROUP BY 포기, 직접 Aggregation
◦
1000만 건을 Chunk 단위로 읽어 Redis에 합산
◦
최종 결과(예: 50만 건)를 DB에 저장
•
Redis 도입 이유
1.
연산 API 지원
•
HINCRBY, HINCRBYFLOAT 제공
•
메모리 수준에서 매우 빠른 정수/실수 합산
2.
넉넉한 메모리
•
50만 건 데이터는 수십~수백 GB Redis 메모리에 충분히 저장 가능
3.
빠른 연산
•
중간 저장 결과가 디스크 아닌 메모리에 저장
•
연산 속도 매우 빠름
•
영구 저장 불필요
•
아키텍처
1.
1000만 건 데이터를 1000개씩 Chunk 분할 (총 1만 개 Chunk)
2.
각 Chunk마다 Redis SUM 연산 요청
3.
반복 합산 결과 (50만 개)를 최종 저장소에 저장
•
예시 코드 (Redis 누적)
suspend fun accumulateToRedis(
key: String,
entries: List<Pair<String, Long>>,
redis: StatefulRedisConnection<String, String>
) {
val commands = redis.reactive()
entries.forEach { (field, delta) ->
commands.hincrby(key, field, delta).subscribe()
}
commands.close()
}
Kotlin
복사
7.3. Redis Pipeline을 통한 레이턴시 해결
•
문제점
◦
1000만 건 합산 시 최소 1000만 번 Redis 연산 요청
◦
최소 1ms 레이턴시 가정 시 1000만 ms = 약 3시간 소요
◦
오히려 성능 저하
•
Redis Pipeline 해결책
◦
여러 개의 Redis 커맨드를 묶어서 처리
◦
Chunk당 1번씩만 연산 요청
◦
1000만 번 네트워킹 → 1만 번으로 감소
◦
Aggregation 시간 대폭 단축
•
구현
◦
Spring Data Redis로는 처리 불가
◦
배치 전용 대량 처리 라이브러리 자체 개발 필요
•
예시 코드 (Lettuce 파이프라인)
suspend fun pipelineIncr(
key: String,
entries: List<Pair<String, Long>>,
redis: StatefulRedisConnection<String, String>
) {
redis.async().let { async ->
async.setAutoFlushCommands(false)
entries.forEach { (field, delta) ->
async.hincrby(key, field, delta)
}
async.flushCommands()
}
}
Kotlin
복사
8. 성능 최적화 전략: 구동 환경 개선
8.1. 기존 스케줄 툴의 한계
•
일반적인 스케줄 툴
◦
Cron, Jenkins, Airflow, Luigi 등
◦
실행 요청, 스케줄 관리, 워크플로우 관리, 모니터링 제공
•
놓치고 있는 부분
1.
자원 관리의 어려움
•
배치는 특정 시간에만 동작 후 자원 반납
•
자원 사용 시점 판단 어려움
•
한 번에 많은 배치 실행 시 자원 관리 어려움
•
물리 서버에서 OOM 발생 위험
2.
배치 상태 파악의 어려움
•
배치 과정이 길지만 로그 정보 빈약
•
서비스 상태를 로그로 판단하는 것은 시각적이지 않음
8.2. Spring Cloud Data Flow (SCDF) 도입
•
SCDF
◦
데이터 파이프라인 생성 및 오케스트레이션 툴
◦
Task 기능으로 배치 처리 지원
◦
K8s 연동
▪
K8s와 완벽 연동
▪
컨테이너 오케스트레이션처럼 배치 오케스트레이션
▪
리소스 사용 및 반납 조율
▪
다수 배치 동시 실행 시에도 안정적 리소스 컨트롤
◦
Spring Batch 호환성
▪
스프링 배치와 완벽 호환
▪
유용한 정보를 시각적으로 모니터링
▪
자체 대시보드 제공
▪
Grafana 연동 가능
•
SCDF Task의 구체적 기능
◦
K8s 활용
▪
K8s CronJob으로 스케줄 관리
▪
K8s 요청으로 애플리케이션 실행 및 배포
▪
Pod 상태 모니터링
◦
워크플로우 및 리소스 설정
▪
배치 실행 순서를 그래프 형태로 설정
▪
CPU 개수, 메모리 할당량 등 자원 설정 가능
▪
데이터 많거나 멀티스레드 작업 시 리소스 추가 할당
◦
그래프 기반 워크플로우
▪
여러 배치를 그래프로 묶음
▪
상태에 따라 실행할 배치 결정
▪
예: 실패 시 Task A 실행, 완료 시 Task B 실행
◦
스케줄 등록
▪
Cron 표현식으로 스케줄 설정
▪
Arguments와 Properties로 K8s 설정 및 배치 파라미터 조정
◦
모니터링 대시보드
▪
Step 정보
•
배치의 모든 Step 표시
•
전체 수행 시간 확인
▪
Step 상세
•
읽은 개수, 쓴 개수
•
커밋 개수, 롤백 개수
•
수행 시간, 최종 상태
▪
Step 누적 히스토리
•
전체 소요 시간
•
읽기/쓰기 횟수
•
최소, 최대, 평균, 표준편차 제공
▪
Grafana 연동
•
모니터링 내용 및 리소스 상태를 Grafana에 연동
9. 성능 최적화 단계적 접근 방법론
9.1. 1단계: 직관적이고 예측 가능한 최적화
•
특징
◦
유지보수 하기 좋은 코드
◦
직관적이고 예측 가능
◦
기존 구조를 크게 변경하지 않음
•
적용 기법
◦
WHERE IN 기반 업데이트
◦
JDBC Batch Insert/Update
◦
Zero Offset ItemReader
◦
Cursor ItemReader
•
우선순위
◦
먼저 1단계를 적용하여 성능 개선 시도
◦
대부분의 경우 1단계만으로도 충분한 성능 확보
9.2. 2단계: 고급 최적화 기법
•
적용 시점
◦
1단계로도 이슈 해결 안 될 경우
◦
더 빠른 성능이 필요한 경우
•
특징
◦
직관적이지 않음
◦
예상하기 어려운 방식
◦
코드 복잡도 증가
•
적용 기법
◦
멀티스레드/병렬 처리
◦
Redis를 활용한 직접 Aggregation
◦
복잡한 아키텍처 변경
•
주의사항
◦
처음부터 고급 기법 사용 시 나중에 더 큰 최적화 필요할 때 어려움
◦
현재 구조를 최대한 유지하면서 최적화할 방법 먼저 검토
9.3. 최적화 핵심 원칙
•
반복적인 IO 작업 그룹화
◦
성능 저하의 근본 원인은 반복적인 IO 작업
◦
IO 작업을 모아서 처리하는 구조로 변경
◦
병렬 처리 및 벌크 IO 처리 활용
•
책임과 역할 분리
◦
Reader, Processor, Writer 역할 명확히 분담
◦
개선 지점 파악 용이
◦
단계별 최적화 가능
•
선형적 성능 증가 확보
◦
데이터 증가에 비례하여 선형적으로 처리 시간 증가
◦
처리 시간 예측 가능
◦
안정적인 운영 가능
10. 결론
10.1. 핵심 요약
1.
Reader 개선: Zero Offset 또는 Cursor 방식으로 선형적 성능 확보
2.
Writer 개선: JPA 포기, Batch Insert/Update로 DB IO 최소화
3.
Network IO 개선: Processor 제거, 병렬 처리 적용
4.
DB IO 개선: WHERE IN 또는 JDBC Batch로 벌크 처리
5.
Aggregation 개선: Redis + Pipeline으로 직접 Aggregation
6.
구동 환경 개선: SCDF + K8s로 리소스 관리 및 모니터링 강화
10.2. 최적화 접근 전략
1.
반복적인 IO 작업을 그룹화하여 최소화
2.
단계적 접근: 직관적 방법 우선, 고급 기법은 필요 시에만
3.
현재 구조를 크게 변경하지 않는 선에서 최적화
4.
데이터 증가에 비례한 선형적 성능 증가 확보
10.3. 범용성
•
스프링 배치뿐만 아니라 모든 애플리케이션의 대량 처리에 적용 가능
•
IO 작업 최소화는 성능 최적화의 핵심 원칙
10.4. 지속적 개선
•
배치 퍼포먼스 모니터링 지속
•
데이터 증가 추이 파악
•
병목 지점 발견 시 즉시 개선
•
APM 툴 또는 SCDF 활용한 지속적 모니터링 체계 구축