[Spring Batch] StepScope를 web api에서 사용할 순 없는걸까

구범모·2025년 12월 12일

들어가며

이번 글에서는 Spring Batch의 @StepScope로 관리되는 Bean을 Web MVC 환경에서도 함께 사용하면서 겪었던 기술적 고민과, 최종적으로 어떻게 문제를 해결했는지 공유하고자 합니다.

사전 개념

💡 Step Scope

Spring Batch에서 Batch Step이 실행될 때마다 Bean을 새로 초기화하는 스코프.
즉, 배치 작업 중 각 Step 실행 단위마다 독립적인 Bean 인스턴스를 생성해 사용된다. (참고 : StepScope란?)

위와 같이 StepScope로 선언된 객체는, Spring IoC container의 도움을 받아 Batch Job이 실행될 때 위의 객체를 사용하는 곳에 적절히 DI시켜준다. (예시로 아래 ExecuteService에 DI가 된다고 할 수 있겠다.)

💡 Request Scope

@RequestScope는 Spring MVC에서 HTTP 요청 하나당 Bean 하나를 생성하는 범위.
요청이 끝나면 해당 Bean의 생명주기도 종료된다. (참고 : RequestScope란?)

문제상황

현재 회사에서 맡은 프로젝트는 Spring Batch를 중심으로 돌아가고, 내가 맡은 부분은 판정 로직이다.
업무적으로 간단히 말하면 여러 개의 로직을 각각 클래스 단위로 쪼갠 뒤, Batch Job이 이들을 순차적으로 호출해 딜러에게 얼마를 보상해야 할지 판정하는 역할이다.

판정에 필요한 변수를 하나의 Batch Job 안에서 공유해야 해서, ValidLogicGlobalObject라는 클래스를 @StepScope Bean으로 선언해 전역변수처럼 사용한다.

요구사항

외관상 문제는 없지만 다음과 같은 요구사항이 존재한다.

  1. 서비스는 기본적으로 Batch Job을 통해 호출된다.
  2. 하지만 HTTP Request를 통해서도 호출할 수 있어야 한다.
  3. Batch Job과 HTTP Request 각각의 요청마다 전역 변수가 독립적으로 관리돼야 하고, 로직 실행 후에는 상태가 유지될 필요 없다(한 건의 요청 안에서만 상태가 유지된다).

이 요구사항을 테스트하기 위해 무심코 Web Endpoint를 열어 HTTP 요청을 해보니, 아래 오류가 발생했다.

org.springframework.beans.factory.support.ScopeNotActiveException: Error creating bean with name 'scopedTarget.ValidLogicGlobalObject': Scope 'step' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:384)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:35)

오류 원인

Spring Bean은 기본적으로 프록시 객체로 감싸진다.
Bean의 필드에 접근하거나 메서드를 호출할 때, 프록시가 내부적으로 실제 객체를 얻기 위해 Spring IoC 컨테이너의 doGetBean()을 호출한다.

이때 @StepScope로 선언된 Bean(ValidLogicGlobalObject)을 꺼내려 하면 StepContext에 접근한다.
하지만 Batch Job을 통해 실행한 게 아닌, HTTP 요청이라 StepContext가 활성화되어 있지 않아 오류가 발생한다. (아래 사진 참고)

doGetBean()을 통해, StepScope로 선언된 Bean을 get


내부적으로 bean을 가져오는 과정에서, StepContext에서 꺼내오려 시도했으나, step context holder가 존재하지 않음.

내가 생각한 제약조건

코드를 수정할 때 다음 조건을 지키려고 했다.
1. Production 코드를 가능한 한 변경하지 않는다.
2. Spring의 기능을 최대한 활용한다.

해결방법 나열

위와 같은 흐름으로, 오류 원인까지는 파악했다. 그렇다면 어떤 식으로 해결할 수 있을까?
1. Batch용 서비스와 HTTP용 서비스를 따로 만들어서, Batch 쪽에선 @StepScope Bean, HTTP 쪽에선 Singleton Bean을 쓴다.
- 단점 : 똑같은 비즈니스로직을 갖는 클래스를 두개 만들어야 한다. 이는 곧 유지보수 할 포인트가 늘어남을 뜻한다.
2. HTTP 요청 시에는 ValidLogicGlobalObject를 Bean으로 쓰지 않고 일반 POJO로 직접 생성해 사용한다.
(POJO기 때문에, 오류의 원인인 doGetBean()을 호출하지 않아 오류가 발생하지 않는다.)
- 단점 : Spring DI라는 강력한 기능을 이용하지 못한다. 따라서 개발자가 ValidLogicGlobalObject을 생성자 / setter 등을 통해 외부에서 service에 직접 주입해주어야 한다.
3. ValidLogicGlobalObject에서 @StepScope를 제거한다.
- 단점 : Spring Batch에서 @StepScope Bean에 제공하는 강력한 기능 중 하나인, 각 step마다 bean을 초기화 하는 장점을 누릴 수 없다.
4. ⭐ Batch에서 호출될 때는 ValidLogicGlobalObject을 @StepScope bean을 반환하고,
Http Request를 통해 호출될 때는 ValidLogicGlobalObject을 @RequestScope bean을 반환하는 custom Scope를 구현한다.

해결과정 나열

나는 처음에 2번 방법을 생각했고, 다음과 같은 코드를 작성했다.

2번 방법의 실제 구현

1. 웹 요청을 통해 서비스가 실행되기 이전, UI flag변수(boolean type)를 true로 바꾼다.
2. AOP를 이용하여, 어노테이션이 적용된 클래스(이하 targetClass)에 대하여 Reflection api를 이용해 필드들을 가져온다.
3. targetClass에서 ValidLogicGlobalObject를 사용하지 않는다면, 아무 작업도 하지 않고 실제 로직을 호출한다(joinPoint.proceed())
4. targetClass에서 ValidLogicGlobalObject를 사용한다면, Factory Pattern을 통해 POJO를 만들어 targetClass의 필드에 set해준다.
5. 실제 로직을 호출한다.
6. 다음 요청에 영향이 가지 않게 하기 위해, UI flag변수(boolean type)를 false로 바꾼다.
7. 다음 요청에 재사용되지 않기 위해, 팩토리에서 관리되는 POJO 객체에 할당된 메모리를 해제한다.(null로 바꿔준다.)

2번 방법의 문제점

  1. AOP에서 api 호출시에만 reflection을 사용하기 위해,UI flag를 별도로 관리하고 있다.
    따라서 api 호출 이전/이후에 UI flag를 on/off하는 코드가 반드시 수반되어야 한다.
  2. api 호출 이후에도 POJO를 null로 바꾸는 코드를 작성해야 한다.
    위 두가지 문제점 때문에, 코드 작성을 누락하는 휴먼에러가 발생할 수 있다고 판단하여
    최종적으로 4번 방법을 채택하였다.

4번 방법의 실제 구현

StepScope와 RequestScope를 동시에 사용할 수 있는 Custom Scope를, 아래와 같은 과정을 통해 구현한다.
1. Scope 인터페이스를 구현하는 클래스를 생성한다.

2. Scope가 적용된 Bean이 web요청인지, batch요청인지를 판단할 수 있는 boolean 메서드를 작성한다.
(이때 작성된 코드들은, 실제 Spring의 RequestScope와 StepScope에서 사용되는 코드이다.)
3. Scope 인터페이스의 메서드(위의 5개)를 overriding한다.
4. overriding한 메서드 내부에서 각 요청을 분기하여
web 요청이면 RequestScope의 로직을,
batch 요청이면 StepScope의 로직을 재사용한다.
5. 1~4번 과정을 통해 생성한 Custom Scope를, "stepAndWeb"이라는 이름으로 새로운 scope를 등록한다.
6. custom scope를 적용하고 싶은 곳에서, 5번 과정에 등록한 이름을 사용한다.

실제 사용

Web 요청으로 Bean 접근 시 isWebRequest() 분기를 타서 RequestScope Bean을 반환한다.

Batch Step 실행 시 isStepRequest() 분기를 타서 StepScope Bean을 반환한다.

정리

이 Custom Scope를 통해

  • Batch Job과 HTTP Request 양쪽에서 동일한 타입의 전역 변수를 사용.
  • Production 코드를 거의 변경하지 않고 Spring 스코프 기능만으로 문제를 해결.

위의 결과를 달성할 수 있었다.

profile
우상향 하는 개발자

0개의 댓글