Spring Batch 개념부터 Job/Step 구조, 메타테이블, Tasklet/Chunk 방식까지 완벽 정리. 대용량 데이터 처리를 위한 실무 예제와 성능 최적화 팁을 제공합니다.
Spring Batch란 무엇인가
Spring Batch는 Spring Framework 기반의 배치 처리 프레임워크입니다.
대용량 데이터를 효율적으로 처리하기 위해 설계되었으며, 엔터프라이즈 환경에서 안정적인 배치 작업을 수행할 수 있도록 다양한 기능을 제공합니다.
Spring Batch 개념의 핵심은 '일괄 처리'에 있습니다.
실시간으로 하나씩 처리하기에는 너무 많은 데이터나, 특정 시간에 몰아서 처리해야 하는 작업들을 효율적으로 관리합니다.
Spring Batch의 주요 특징
- 대용량 데이터 처리: 수백만 건의 레코드도 메모리 효율적으로 처리
- 견고성과 안정성: 실패 지점에서 재시작, 스킵, 재시도 등 다양한 복구 메커니즘
- 트랜잭션 관리: 청크 단위의 트랜잭션으로 데이터 일관성 보장
- 모니터링과 추적: 실행 상태, 진행률, 오류 정보 등 상세한 배치 로깅 추적
- 확장성: 멀티스레드, 병렬 처리, 파티셔닝 지원
배치와 스케줄러 차이점 이해하기
많은 개발자들이 배치와 스케줄러를 혼동하는 경우가 있습니다.
이 둘의 차이점을 명확히 구분해보겠습니다.
구분 | 배치(Batch) | 스케줄러(Scheduler) |
---|---|---|
역할 | 데이터 일괄 처리 방식 | 작업 실행 시점 관리 |
목적 | 대용량 데이터 효율적 처리 | 언제 실행할지 결정 |
예시 | Spring Batch, ETL 도구 | Quartz 스케줄러, Cron |
관심사 | How to Process | When to Execute |
배치 처리의 핵심
배치는 "어떻게 처리할 것인가"에 집중합니다.
- 대용량 데이터를 청크 단위로 나누어 처리
- 메모리 사용량 최적화
- 트랜잭션 경계 설정
- 오류 발생 시 복구 전략
스케줄러의 역할
스케줄러는 "언제 실행할 것인가"를 결정합니다.
// Quartz 스케줄러 예시
@Scheduled(cron = "0 0 2 * * ?") // 매일 새벽 2시
public void runBatchJob() {
// Spring Batch Job 실행
jobLauncher.run(job, jobParameters);
}
Spring Batch 스케줄러 차이를 이해한다면, Spring Batch는 배치 처리를 담당하고, 실제 실행 시점은 별도의 스케줄러가 관리한다는 점입니다.
Spring Batch 핵심 아키텍처
┌─────────────────┐ ┌─────────────────┐
│ JobLauncher │────│ JobRepository │
└─────────────────┘ └─────────────────┘
│ │
▼ │
┌─────────────────┐ │
│ Job │──────────────┘
│ ┌───────────┐ │
│ │ Step │ │
│ │ ┌───────┐ │ │
│ │ │Tasklet│ │ │ 또는
│ │ └───────┘ │ │
│ │ 또는 │ │
│ │ ┌───────┐ │ │
│ │ │ Chunk │ │ │
│ │ │R→P→W │ │ │
│ │ └───────┘ │ │
│ └───────────┘ │
└─────────────────┘
Job과 Step의 관계
Spring Batch Job Step 차이는 계층적 구조로 이해할 수 있습니다.
- Job: 배치 작업의 전체 단위 (예: 일매출 집계 배치)
- Step: Job을 구성하는 개별 처리 단계 (예: 데이터 읽기 → 가공 → 저장)
@Configuration
@EnableBatchProcessing
public class BatchConfiguration {
@Bean
public Job salesAggregationJob(JobBuilderFactory jobBuilderFactory,
Step extractStep, Step transformStep, Step loadStep) {
return jobBuilderFactory.get("salesAggregationJob")
.start(extractStep)
.next(transformStep)
.next(loadStep)
.build();
}
}
Spring Batch 메타 테이블 완벽 가이드
Spring Batch는 배치 작업의 상태와 진행 상황을 추적하기 위해 6개의 메타 테이블을 자동으로 생성합니다.
주요 메타 테이블 구조
BATCH_JOB_INSTANCE
├── JOB_INSTANCE_ID (PK)
├── JOB_NAME
└── JOB_KEY
BATCH_JOB_EXECUTION
├── JOB_EXECUTION_ID (PK)
├── JOB_INSTANCE_ID (FK)
├── CREATE_TIME
├── START_TIME
├── END_TIME
├── STATUS
├── EXIT_CODE
└── EXIT_MESSAGE
BATCH_JOB_EXECUTION_PARAMS
├── JOB_EXECUTION_ID (FK)
├── TYPE_CD
├── KEY_NAME
├── STRING_VAL
├── DATE_VAL
├── LONG_VAL
└── DOUBLE_VAL
BATCH_STEP_EXECUTION
├── STEP_EXECUTION_ID (PK)
├── STEP_NAME
├── JOB_EXECUTION_ID (FK)
├── START_TIME
├── END_TIME
├── STATUS
├── COMMIT_COUNT
├── READ_COUNT
├── FILTER_COUNT
└── WRITE_COUNT
BATCH_JOB_EXECUTION_CONTEXT
├── JOB_EXECUTION_ID (PK/FK)
└── SERIALIZED_CONTEXT
BATCH_STEP_EXECUTION_CONTEXT
├── STEP_EXECUTION_ID (PK/FK)
└── SERIALIZED_CONTEXT
Spring Batch 메타 테이블 활용 방법
이러한 메타 테이블을 통해 다음과 같은 정보를 확인할 수 있습니다.
-- 실패한 배치 작업 조회
SELECT je.JOB_EXECUTION_ID, ji.JOB_NAME, je.STATUS, je.EXIT_MESSAGE
FROM BATCH_JOB_EXECUTION je
JOIN BATCH_JOB_INSTANCE ji ON je.JOB_INSTANCE_ID = ji.JOB_INSTANCE_ID
WHERE je.STATUS = 'FAILED';
-- Step별 처리 건수 확인
SELECT STEP_NAME, READ_COUNT, WRITE_COUNT, COMMIT_COUNT
FROM BATCH_STEP_EXECUTION
WHERE JOB_EXECUTION_ID = ?;
Spring Batch 메타데이터 스키마 문서에서 더 자세한 정보를 확인할 수 있습니다.
Tasklet vs Chunk 처리 방식
Spring Batch에서 Step을 구현하는 두 가지 주요 방식이 있습니다.
Spring Batch Tasklet Chunk 방식의 차이점을 알아보겠습니다.
Tasklet 방식
단일 작업을 처리하는데 적합한 방식입니다.
@Bean
public Step backupStep(StepBuilderFactory stepBuilderFactory) {
return stepBuilderFactory.get("backupStep")
.tasklet(new Tasklet() {
@Override
public RepeatStatus execute(StepContribution contribution,
ChunkContext chunkContext) throws Exception {
// 데이터 백업 배치 작업 수행
performDatabaseBackup();
return RepeatStatus.FINISHED;
}
})
.build();
}
Chunk 방식
대량 데이터를 효율적으로 처리하는 방식입니다.
@Bean
public Step processingStep(StepBuilderFactory stepBuilderFactory,
ItemReader<Customer> reader,
ItemProcessor<Customer, CustomerDto> processor,
ItemWriter<CustomerDto> writer) {
return stepBuilderFactory.get("processingStep")
.<Customer, CustomerDto>chunk(100) // Chunk size
.reader(reader)
.processor(processor)
.writer(writer)
.build();
}
사용 시나리오 비교
구분 | Tasklet | Chunk |
---|---|---|
용도 | 단순 작업, 초기화, 정리 | 대량 데이터 처리 |
처리 방식 | 한 번에 모든 작업 | 청크 단위로 분할 처리 |
메모리 효율 | 전체 데이터 로드 | 청크 크기만큼만 로드 |
트랜잭션 | 전체가 하나의 트랜잭션 | 청크별 트랜잭션 |
예시 | 파일 압축, 디렉토리 정리 | 고객 데이터 이관, 정산 |
Spring Batch 핵심 컴포넌트
JobParameters와 ExecutionContext
Spring Batch JobParameters는 배치 작업에 전달되는 매개변수입니다.
@Bean
@StepScope
public FlatFileItemReader<Customer> customerReader(@Value("#{jobParameters[inputFile]}") String inputFile) {
return new FlatFileItemReaderBuilder<Customer>()
.name("customerReader")
.resource(new FileSystemResource(inputFile))
.delimited()
.names("id", "name", "email")
.targetType(Customer.class)
.build();
}
ExecutionContext는 Step 간 데이터 공유를 위한 저장소입니다.
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
ExecutionContext executionContext = chunkContext.getStepContext()
.getStepExecution()
.getJobExecution()
.getExecutionContext();
// 데이터 저장
executionContext.put("processedCount", 1000);
return RepeatStatus.FINISHED;
}
JobRepository와 관리 도구
Spring Batch JobRepository는 배치 메타데이터를 관리하는 핵심 컴포넌트입니다.
@Configuration
public class BatchConfig {
@Bean
public JobRepository jobRepository(DataSource dataSource,
PlatformTransactionManager transactionManager) throws Exception {
JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
factory.setDataSource(dataSource);
factory.setTransactionManager(transactionManager);
factory.setIsolationLevelForCreate("ISOLATION_READ_COMMITTED");
return factory.getObject();
}
}
JobLauncher, JobExplorer, JobOperator는 각각 다른 역할을 수행합니다
- JobLauncher: Job 실행
- JobExplorer: Job 실행 이력 조회
- JobOperator: Job 제어 (중지, 재시작 등)
실제 활용 사례별 구현 예제
일매출 집계 배치 구현
대표적인 일매출 집계 배치 예시를 살펴보겠습니다.
@Configuration
public class DailySalesJobConfig {
@Bean
public Job dailySalesJob(JobBuilderFactory jobBuilderFactory, Step salesAggregationStep) {
return jobBuilderFactory.get("dailySalesJob")
.start(salesAggregationStep)
.build();
}
@Bean
public Step salesAggregationStep(StepBuilderFactory stepBuilderFactory) {
return stepBuilderFactory.get("salesAggregationStep")
.<SalesRecord, DailySales>chunk(1000)
.reader(salesRecordReader())
.processor(salesAggregationProcessor())
.writer(dailySalesWriter())
.build();
}
@Bean
@StepScope
public JpaPagingItemReader<SalesRecord> salesRecordReader() {
return new JpaPagingItemReaderBuilder<SalesRecord>()
.name("salesRecordReader")
.entityManagerFactory(entityManagerFactory)
.queryString("SELECT s FROM SalesRecord s WHERE s.saleDate = :targetDate ORDER BY s.id")
.parameterValues(Collections.singletonMap("targetDate", LocalDate.now().minusDays(1)))
.pageSize(1000)
.build();
}
}
구독 메일 배치 시스템
구독 메일 배치 처리 예제입니다.
@Component
public class NewsletterItemProcessor implements ItemProcessor<Subscriber, EmailMessage> {
@Override
public EmailMessage process(Subscriber subscriber) throws Exception {
// 구독자별 맞춤 컨텐츠 생성
String personalizedContent = generatePersonalizedNewsletter(subscriber);
return EmailMessage.builder()
.to(subscriber.getEmail())
.subject("주간 뉴스레터")
.content(personalizedContent)
.build();
}
}
@Component
public class EmailItemWriter implements ItemWriter<EmailMessage> {
@Override
public void write(List<? extends EmailMessage> emails) throws Exception {
// 배치로 메일 발송
emailService.sendBulkEmails(emails);
// 발송 로그 기록
emails.forEach(email ->
log.info("Email sent to: {}", email.getTo())
);
}
}
ItemReader, ItemProcessor, ItemWriter 심화
Chunk 기반 처리의 3대 핵심 컴포넌트를 자세히 알아보겠습니다.
ItemReader 구현
JpaPagingItemReader를 활용한 대용량 데이터 읽기
@Bean
@StepScope
public JpaPagingItemReader<Customer> customerReader(@Value("#{jobParameters[status]}") String status) {
return new JpaPagingItemReaderBuilder<Customer>()
.name("customerReader")
.entityManagerFactory(entityManagerFactory)
.queryString("SELECT c FROM Customer c WHERE c.status = :status ORDER BY c.id")
.parameterValues(Collections.singletonMap("status", status))
.pageSize(100) // Paging size
.build();
}
ItemProcessor 패턴
데이터 변환과 비즈니스 로직 처리
@Component
public class CustomerValidationProcessor implements ItemProcessor<Customer, ValidatedCustomer> {
@Override
public ValidatedCustomer process(Customer customer) throws Exception {
// 유효성 검증
if (!isValidEmail(customer.getEmail())) {
return null; // null 반환 시 해당 아이템 필터링
}
// 데이터 변환
return ValidatedCustomer.builder()
.customerId(customer.getId())
.email(customer.getEmail().toLowerCase())
.status("VALIDATED")
.processedAt(LocalDateTime.now())
.build();
}
}
ItemWriter 최적화
배치 처리로 성능 향상
@Component
public class JpaItemWriter<T> implements ItemWriter<T> {
@PersistenceContext
private EntityManager entityManager;
@Override
public void write(List<? extends T> items) throws Exception {
for (T item : items) {
entityManager.merge(item);
}
// 청크 단위로 flush
entityManager.flush();
entityManager.clear();
}
}
성능 최적화 핵심 전략
Chunk size와 Paging size 설정
Chunk size Paging size 관계는 성능에 직접적인 영향을 미칩니다.
@Bean
public Step optimizedStep(StepBuilderFactory stepBuilderFactory) {
return stepBuilderFactory.get("optimizedStep")
.<Customer, CustomerDto>chunk(1000) // Chunk size
.reader(customerReader()) // Page size: 1000
.processor(customerProcessor())
.writer(customerWriter())
.taskExecutor(taskExecutor()) // 병렬 처리
.build();
}
권장사항
- Chunk size == Paging size: 메모리 사용량 최적화
- 정렬 보장: ORDER BY 절 필수 (데이터 일관성)
- 트랜잭션 크기: 너무 크면 롤백 비용 증가, 너무 작으면 오버헤드 증가
배치 로깅 추적 트랜잭션 관리
@Configuration
public class BatchConfig {
@Bean
public PlatformTransactionManager batchTransactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
transactionManager.setDefaultTimeout(300); // 5분 타임아웃
return transactionManager;
}
@Bean
public Step transactionalStep(StepBuilderFactory stepBuilderFactory) {
return stepBuilderFactory.get("transactionalStep")
.<Customer, Customer>chunk(100)
.reader(customerReader())
.writer(customerWriter())
.transactionManager(batchTransactionManager(null))
.build();
}
}
JobExecution과 StepExecution 모니터링
JobExecution StepExecution을 활용한 배치 작업 추적
@Component
public class BatchJobExecutionListener implements JobExecutionListener {
@Override
public void beforeJob(JobExecution jobExecution) {
log.info("Job {} started at {}",
jobExecution.getJobInstance().getJobName(),
jobExecution.getCreateTime());
}
@Override
public void afterJob(JobExecution jobExecution) {
if (jobExecution.getStatus() == BatchStatus.COMPLETED) {
log.info("Job {} completed successfully",
jobExecution.getJobInstance().getJobName());
} else {
log.error("Job {} failed with status {}",
jobExecution.getJobInstance().getJobName(),
jobExecution.getStatus());
}
// Step별 실행 결과 확인
jobExecution.getStepExecutions().forEach(stepExecution -> {
log.info("Step: {}, Read: {}, Write: {}, Skip: {}",
stepExecution.getStepName(),
stepExecution.getReadCount(),
stepExecution.getWriteCount(),
stepExecution.getSkipCount());
});
}
}
Spring Batch 테스트 코드 작성
배치 작업의 안정성을 보장하기 위한 Spring Batch 테스트 코드 예제입니다.
@SpringBatchTest
@SpringBootTest
@Transactional
class CustomerProcessingJobTest {
@Autowired
private TestJobLauncher testJobLauncher;
@Autowired
private Job customerProcessingJob;
@Autowired
private JobRepository jobRepository;
@Test
public void testCustomerProcessingJob() throws Exception {
// Given
JobParameters jobParameters = new JobParametersBuilder()
.addString("inputFile", "test-customers.csv")
.addDate("date", new Date())
.toJobParameters();
// When
JobExecution jobExecution = testJobLauncher.run(customerProcessingJob, jobParameters);
// Then
assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());
assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
// Step별 검증
StepExecution stepExecution = jobExecution.getStepExecutions().iterator().next();
assertEquals(100, stepExecution.getReadCount());
assertEquals(90, stepExecution.getWriteCount()); // 10개 필터링
assertEquals(0, stepExecution.getSkipCount());
}
@Test
public void testJobWithInvalidData() throws Exception {
// Given
JobParameters jobParameters = new JobParametersBuilder()
.addString("inputFile", "invalid-customers.csv")
.addDate("date", new Date())
.toJobParameters();
// When
JobExecution jobExecution = testJobLauncher.run(customerProcessingJob, jobParameters);
// Then - 오류 처리 검증
assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());
StepExecution stepExecution = jobExecution.getStepExecutions().iterator().next();
assertTrue(stepExecution.getSkipCount() > 0); // 스킵된 레코드 존재
}
}
테스트 유틸리티 활용
@TestConfiguration
public class BatchTestConfig {
@Bean
@Primary
public JobLauncherTestUtils jobLauncherTestUtils() {
return new JobLauncherTestUtils();
}
@Bean
@Primary
public JobRepositoryTestUtils jobRepositoryTestUtils() {
return new JobRepositoryTestUtils();
}
}
Spring Batch 테스트 가이드 문서를 참조하여 더 다양한 테스트 시나리오를 구현할 수 있습니다.
실무 적용 시 고려사항
재시작 및 장애 복구
@Bean
public Job restartableJob(JobBuilderFactory jobBuilderFactory, Step processingStep) {
return jobBuilderFactory.get("restartableJob")
.start(processingStep)
.incrementer(new RunIdIncrementer()) // 재실행 가능
.listener(jobExecutionListener())
.build();
}
@Bean
public Step faultTolerantStep(StepBuilderFactory stepBuilderFactory) {
return stepBuilderFactory.get("faultTolerantStep")
.<Customer, Customer>chunk(100)
.reader(customerReader())
.processor(customerProcessor())
.writer(customerWriter())
.faultTolerant()
.skip(ValidationException.class) // 특정 예외 스킵
.skipLimit(10) // 최대 스킵 횟수
.retry(TransientDataException.class) // 재시도 대상 예외
.retryLimit(3) // 재시도 횟수
.build();
}
배치 모니터링 구축
Spring Boot Actuator와 연동한 배치 상태 모니터링
@Component
public class BatchMetricsCollector {
private final MeterRegistry meterRegistry;
private final JobExplorer jobExplorer;
@EventListener
public void handleJobExecution(JobExecutionEvent event) {
JobExecution jobExecution = event.getJobExecution();
// 메트릭 수집
Timer.Sample sample = Timer.start(meterRegistry);
sample.stop(Timer.builder("batch.job.duration")
.tag("job.name", jobExecution.getJobInstance().getJobName())
.tag("status", jobExecution.getStatus().toString())
.register(meterRegistry));
// 처리량 메트릭
jobExecution.getStepExecutions().forEach(stepExecution -> {
Gauge.builder("batch.step.read.count")
.tag("job.name", jobExecution.getJobInstance().getJobName())
.tag("step.name", stepExecution.getStepName())
.register(meterRegistry, stepExecution, StepExecution::getReadCount);
});
}
}
분산 처리와 스케일링
@Bean
public Step partitionedStep(StepBuilderFactory stepBuilderFactory,
Partitioner partitioner,
Step workerStep) {
return stepBuilderFactory.get("partitionedStep")
.partitioner("workerStep", partitioner)
.step(workerStep)
.gridSize(4) // 파티션 수
.taskExecutor(taskExecutor())
.build();
}
@Component
public class DatePartitioner implements Partitioner {
@Override
public Map<String, ExecutionContext> partition(int gridSize) {
Map<String, ExecutionContext> partitions = new HashMap<>();
LocalDate startDate = LocalDate.now().minusDays(30);
LocalDate endDate = LocalDate.now();
long totalDays = ChronoUnit.DAYS.between(startDate, endDate);
long daysPerPartition = totalDays / gridSize;
for (int i = 0; i < gridSize; i++) {
ExecutionContext context = new ExecutionContext();
LocalDate partitionStart = startDate.plusDays(i * daysPerPartition);
LocalDate partitionEnd = (i == gridSize - 1) ? endDate :
startDate.plusDays((i + 1) * daysPerPartition - 1);
context.put("startDate", partitionStart);
context.put("endDate", partitionEnd);
partitions.put("partition" + i, context);
}
return partitions;
}
}
마무리
Spring Batch는 엔터프라이즈급 배치 처리를 위한 강력한 프레임워크입니다.
Job과 Step 기반의 체계적인 아키텍처, 메타 테이블을 통한 실행 추적, Tasklet과 Chunk 방식의 유연한 구현 옵션을 제공합니다.
성공적인 배치 시스템 구축을 위해서는 적절한 청크 크기 설정, 트랜잭션 관리, 장애 복구 전략, 그리고 충분한 테스트가 필요합니다.
Spring Batch 공식 레퍼런스 가이드를 통해 더 자세한 내용을 학습하고, 실제 프로젝트에 적용해보시기 바랍니다.
대용량 데이터 처리가 필요한 현대의 애플리케이션에서 Spring Batch는 더욱 중요한 역할을 할 것입니다.
체계적인 학습과 실습을 통해 안정적이고 효율적인 배치 시스템을 구축해보세요.
같이 보면 좋은 글
Spring Boot 테스트 컨테이너 실전 가이드 - Docker 없이 통합 테스트 자동화
현대적인 마이크로서비스 아키텍처에서 통합 테스트의 중요성이 날로 증가하고 있습니다.특히 테스트컨테이너 스프링부트 환경에서는 실제 데이터베이스, 메시지 큐, 외부 서비스와의 연동을
notavoid.tistory.com
Spring Boot 3.0 Native Image 완벽 가이드 - GraalVM으로 초고속 애플리케이션 만들기
"우리 서비스가 시작되는데 왜 이렇게 오래 걸리지?"많은 개발자들이 한 번쯤 겪어본 고민입니다.전통적인 Spring Boot 애플리케이션은 강력한 기능을 제공하지만,JVM 특성상 시작 시간이 길고 메모
notavoid.tistory.com
Spring Cloud Stream으로 이벤트 드리븐 마이크로서비스 구축: 실무 완벽 가이드
서론: 왜 이벤트 드리븐 아키텍처가 필요한가?현대 소프트웨어 개발에서 마이크로서비스 아키텍처는 선택이 아닌 필수가 되었습니다.하지만 서비스 간 통신과 데이터 일관성을 유지하는 것은
notavoid.tistory.com
[Spring Security] Spring Security의 FilterChain 구조 완벽 이해
안녕하세요! 오늘은 Spring Security의 핵심 엔진인 FilterChain에 대해 실무 관점에서 깊이 있게 알아보겠습니다.대규모 운영 환경에서 실제로 마주하는 성능 이슈와 해결 방법을 중심으로 설명드리겠
notavoid.tistory.com
MSA 완벽 가이드: 마이크로서비스 아키텍처 개념·장점·도입 전략 총정리
MSA(마이크로서비스 아키텍처)는 대규모 애플리케이션을 작은 독립적인 서비스들로 분해하여 개발 효율성과 확장성을 극대화하는 현대적인 소프트웨어 아키텍처 패턴입니다. 현대 소프트웨어
notavoid.tistory.com
'Spring & Spring Boot 실무 가이드' 카테고리의 다른 글
ddl-auto 옵션 종류 & 실무주의: 운영DB 테이블 날리는 사고 막기(JPA) (1) | 2025.09.19 |
---|---|
MSA 완벽 가이드: 마이크로서비스 아키텍처 개념·장점·도입 전략 총정리 (1) | 2025.09.08 |
Cloud Run + AI 에이전트 자동 배포 파이프라인 구축 가이드 (0) | 2025.06.26 |
Apache Camel로 엔터프라이즈 통합 패턴 구현하기: Spring Boot와 함께하는 실무 가이드 (0) | 2025.06.22 |
Spring Cloud Stream으로 이벤트 드리븐 마이크로서비스 구축: 실무 완벽 가이드 (0) | 2025.06.20 |