📚FOS Study
홈카테고리
홈카테고리

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agents 페이지로 이동
    • custom-agents 페이지로 이동
    • Claude Code의 Skill 시스템 - 개발자를 위한 AI 자동화의 새로운 차원
    • 멀티모달 LLM (Multimodal Large Language Model)
  • architecture 페이지로 이동
    • 디자인 패턴
    • 분산 트랜잭션
    • 슬롯 게임 엔진 고도화 — 2025년 회고
  • css 페이지로 이동
    • FlexBox 페이지로 이동
  • database 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • 김영한의-실전-데이터베이스-설계 페이지로 이동
    • 커넥션 풀 크기는 얼마나 조정해야할까?
    • 인덱스 - DB 성능 최적화의 핵심
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • k8s-in-action 페이지로 이동
    • monitoring 페이지로 이동
  • go 페이지로 이동
    • Go 언어 기본 학습
  • http 페이지로 이동
    • HTTP Connection Pool
  • interview 페이지로 이동
    • 210812 페이지로 이동
    • 뱅크샐러드 AI Native Server Engineer
    • CJ 올리브영 지원 문항
    • CJ 올리브영 커머스플랫폼유닛 Back-End 개발 지원 자료
    • 마이리얼트립 - Platform Solutions실 회원주문개발 Product Engineer
    • NHN 서비스개발센터 AI서비스개발팀
    • nhn gameenvil console backend 직무 인터뷰 준비
    • 면접을 대비해봅시다
    • Tossplace Node.js Developer
    • 토스플레이스 Node.js 백엔드 컬처핏
  • java 페이지로 이동
    • jdbc 페이지로 이동
    • opentelemetry 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • Java의 로깅 환경
    • MDC (Mapped Diagnostic Context)
    • OpenTelemetry 란 무엇인가?
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • Data_Structures_and_Algorithms 페이지로 이동
    • Heap 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node.js
    • npm vs pnpm 선택기준은 무엇인가요?
    • `setImmediate()`
  • kafka 페이지로 이동
    • Kafka 기본
    • Kafka를 사용하여 **데이터 정합성**은 어떻게 유지해야 할까?
    • 메시지 전송 신뢰성
  • network 페이지로 이동
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • react 페이지로 이동
    • JSX 페이지로 이동
    • VirtualDOM 페이지로 이동
    • v16 페이지로 이동
  • redis 페이지로 이동
    • Redis
    • Redis Hash와 Lua 스크립트로 잭팟 누적 구현하기
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
📚FOS Study

개발 학습 기록을 정리하는 블로그입니다.

바로가기

  • 홈
  • 카테고리

소셜

  • GitHub
  • Source Repository

© 2025 FOS Study. Built with Next.js & Tailwind CSS

목록으로 돌아가기
☕java/ spring-batch

@StepScope — Step 실행마다 새로운 빈을 만드는 이유

약 4분
2026년 3월 22일
2026년 3월 22일 수정
GitHub에서 보기

@StepScope — Step 실행마다 새로운 빈을 만드는 이유

@StepScope가 뭔가

Spring의 빈 스코프는 기본이 singleton이다. 애플리케이션이 시작될 때 한 번 생성되고, 이후로는 같은 인스턴스를 계속 재사용한다.

Spring Batch에는 @StepScope라는 커스텀 스코프가 있다. 이 스코프가 붙은 빈은 Step이 실행될 때마다 새로운 인스턴스가 생성된다. Step이 끝나면 인스턴스도 함께 소멸한다.

@Bean
@StepScope
public ConfluencePageItemReader confluencePageItemReader(
    @Value("#{jobParameters['spaceKey']}") String spaceKey
) {
    return new ConfluencePageItemReader(spaceKey, ...);
}

왜 필요한가

1. Job Parameter를 빈 생성 시점에 주입받기 위해

@StepScope 없이 싱글톤 빈으로 만들면, 애플리케이션 컨텍스트가 로딩되는 시점에 빈이 생성된다. 이 시점에는 Job Parameter가 아직 없다. @Value("#{jobParameters['spaceKey']}")를 쓰면 null이 들어온다.

@StepScope를 붙이면 Step이 실제로 실행될 때 빈을 만들기 때문에, 그 시점에 이미 Job Parameter가 확정되어 있어서 값을 주입받을 수 있다.

애플리케이션 시작 → Job Parameter 없음 → 싱글톤 빈 생성 → null 주입 ❌

배치 실행 요청 (spaceKey=MY_SPACE) → Step 시작 → @StepScope 빈 생성 → "MY_SPACE" 주입 ✅

2. Step 실행마다 상태를 초기화하기 위해

Reader, Processor 같은 Step 컴포넌트는 내부 상태를 가지는 경우가 많다. 예를 들어 페이지네이션 커서, 읽은 데이터 버퍼 같은 것들이다.

싱글톤이면 두 번째 Job 실행 시 이전 실행의 상태가 남아있을 수 있다. @StepScope로 Step마다 새 인스턴스를 만들면 이런 상태 누수를 원천 차단한다.

3. 여러 Job이 동시에 실행될 때 격리하기 위해

Job A와 Job B가 같은 Reader 타입을 쓰는데 싱글톤이라면, 두 Job이 같은 Reader 인스턴스를 공유하게 된다. @StepScope를 쓰면 각 Step 실행마다 독립적인 인스턴스가 생기므로 Job 간 간섭이 없다.

프록시 방식으로 동작한다

@StepScope 빈은 애플리케이션 컨텍스트 로딩 시점에 프록시 객체가 먼저 등록된다. 실제 인스턴스는 Step이 시작될 때 프록시가 생성해서 반환한다.

이 때문에 한 가지 주의할 점이 있다. @StepScope 빈 클래스가 final이면 CGLIB 프록시를 만들 수 없어서 예외가 난다.

// ❌ CGLIB 프록시 생성 불가
@StepScope
public final class ConfluencePageItemReader { ... }

// ✅
@StepScope
public class ConfluencePageItemReader { ... }

추상 클래스의 메서드가 final인 경우도 마찬가지다. 상속이나 프록시로 오버라이드할 수 없기 때문이다.

@JobScope도 있다

@StepScope와 비슷한 개념으로 @JobScope도 있다. Job이 실행될 때마다 새로운 빈을 생성한다. Step 간에 공유해야 하는 상태가 있을 때 Tasklet에 주로 쓴다.

@Bean
@JobScope
public StartIndexingJobTasklet startIndexingJobTasklet(
    @Value("#{jobParameters['physicalIndexName']}") String physicalIndexName
) {
    return new StartIndexingJobTasklet(physicalIndexName, ...);
}

여러 Job이 같은 타입의 @StepScope 빈을 등록할 때

두 Job Config가 각각 같은 타입의 @StepScope 빈을 정의하면 문제가 생긴다.

expected single matching bean but found 2:
confluencePageItemEmbeddingProcessor,
planGymConfluencePageItemEmbeddingProcessor

Spring이 타입으로 의존성을 찾을 때 어느 것을 써야 할지 몰라서 터진다.

해결 방법은 두 가지다.

1. 공용 빈은 @Component @StepScope로 전역 등록하고 @Qualifier로 주입

// 공용으로 쓰는 빈은 @Component 붙여서 등록
@Component
@StepScope
public class SharedContextRefreshIndexTasklet implements Tasklet { ... }

// 주입할 때 이름으로 찾게 함
@Bean
public Step confluenceIndexRefreshStep(
    @Qualifier("sharedContextRefreshIndexTasklet") SharedContextRefreshIndexTasklet tasklet
) { ... }

2. 잡 전용 빈은 각자 Config에서만 정의하고 @Qualifier로 명시 주입

// ConfluenceIndexingJobConfig에서
@Bean
@StepScope
public ConfluencePageItemEmbeddingProcessor confluencePageItemEmbeddingProcessor(
    DefaultConfluenceDocumentMetadataProvider metadataProvider, ...
) { ... }

// PlanGymConfluenceSpaceIndexingJobConfig에서
@Bean
@StepScope
public ConfluencePageItemEmbeddingProcessor planGymConfluencePageItemEmbeddingProcessor(
    PlanGymConfluenceDocumentMetadataProvider metadataProvider, ...
) { ... }

// Step 정의 시 @Qualifier로 명시
@Bean
public Step confluencePageIndexingStep(
    @Qualifier("confluencePageItemEmbeddingProcessor") ConfluencePageItemEmbeddingProcessor processor, ...
) { ... }

테스트 코드에서도 맞춰줘야 한다

@BatchComponentTest처럼 전체 Spring Context를 띄우는 테스트에서 @Autowired로 빈을 주입받을 때도 @Qualifier를 붙여야 한다. 빠뜨리면 같은 에러가 난다.

@Autowired
@Qualifier("confluencePageItemEmbeddingProcessor")  // 필수
private ConfluencePageItemEmbeddingProcessor processor;

정리

스코프인스턴스 생성 시점소멸 시점주 용도
singleton (기본)애플리케이션 시작애플리케이션 종료상태 없는 공용 서비스
@JobScopeJob 시작Job 종료Job Parameter 주입, Job 수준 공유 상태
@StepScopeStep 시작Step 종료Job Parameter 주입, Step 컴포넌트 상태 격리

Reader, Processor, Writer처럼 Step 실행 중 상태를 가지는 컴포넌트, 또는 Job Parameter를 생성 시점에 받아야 하는 컴포넌트라면 @StepScope를 붙이는 게 원칙이다.

java 카테고리의 다른 글 보기수정 제안하기

댓글

댓글을 불러오는 중...
목차
  • @StepScope — Step 실행마다 새로운 빈을 만드는 이유
  • @StepScope가 뭔가
  • 왜 필요한가
  • 1. Job Parameter를 빈 생성 시점에 주입받기 위해
  • 2. Step 실행마다 상태를 초기화하기 위해
  • 3. 여러 Job이 동시에 실행될 때 격리하기 위해
  • 프록시 방식으로 동작한다
  • @JobScope도 있다
  • 여러 Job이 같은 타입의 @StepScope 빈을 등록할 때
  • 정리