스케줄러가 클라이언트 역할을 하기 때문에, 스케줄러가 접근하는 서비스는 컨트롤러가 접근하는 서비스와 별개로 구성하는 것이 바람직함
기존 코드
- 스케줄러가 컨트롤러를 통해 접근하는 서비스에 접근하고 있음
- 배송 담당자 할당 기능은 컨트롤러와 직접 연관이 없음에도 DeliveryService에 포함되어 있음
- 스케줄러가 여러 서비스에 의존하여 의존성이 복잡함
public interface DeliveryService {
...
void assignPendingDeliveries();
...
}
public interface DeliveryRouteService {
...
void assignPendingDeliveries();
...
}
@Component
public class DeliveryAssignmentScheduler {
private final DeliveryService deliveryService;
private final DeliveryRouteService deliveryRouteService;
...
@Scheduled(fixedRate = 60000)
public void runAssignmentScheduler() {
...
deliveryService.assignPendingDeliveries();
deliveryRouteService.assignPendingDeliveries();
}
}
개선
: 스케줄러 전용 서비스 분리
- 스케줄러 관련된 코드를 모아둔 별도의 schedule 패키지 생성
- 배송 담당자 할당만 전담하는 DeliveryAssigmentService 인터페이스/구현체 생성
- 기존 배송 담당자 할당 로직을 새 서비스로 이동
- 스케줄러의 의존성을 새로운 서비스로 단일화
// schedule 패키지 내의 스케줄러
@Component
public class DeliveryAssignmentScheduler {
private final DeliveryAssignmentService deliveryAssignmentService;
...
@Scheduled(fixedRate = 60000)
public void runAssignmentScheduler() {
...
deliveryAssignmentService.assignCompanyDeliveryManager();
deliveryAssignmentService.assignHubDeliveryManager();
}
}
// schedule 패키지 내의 할당 서비스
public class DeliveryAssignmentService {
void assignCompanyDeliveryManager();
void assignHubDeliveryManager();
}
이상적으로는 스케줄러(클라이언트) - 워커(컨트롤러) - 서비스 구조가 좋지만, 이번 리팩토링에서는 워커 없이 진행
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'deliveryAssignmentScheduler' defined in file [/Users/t2024-m0206/Documents/git/chill/mono-repo/com.sparta.logistics.delivery-service/build/classes/java/main/com/sparta/logistics/delivery_service/application/service/DeliveryAssignmentScheduler.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'deliveryService' defined in file [/Users/t2024-m0206/Documents/git/chill/mono-repo/com.sparta.logistics.delivery-service/build/classes/java/main/com/sparta/logistics/delivery_service/application/service/DeliveryService.class]: Unsatisfied dependency expressed through constructor parameter 1: Error creating bean with name 'deliveryRouteService' defined in file [/Users/t2024-m0206/Documents/git/chill/mono-repo/com.sparta.logistics.delivery-service/build/classes/java/main/com/sparta/logistics/delivery_service/application/service/DeliveryRouteService.class]: Unsatisfied dependency expressed through constructor parameter 1: Error creating bean with name 'com.sparta.logistics.delivery_service.infrastructure.client.HubRouteClient': FactoryBean threw exception on object creation
DeliveryRouteService가 DeliveryService를 의존하고, DeliveryService가 또 다른 클라이언트들을 의존하는 방식에서 순환 의존성이 있을 수 있다고 생각함
→ @Lazy 어노테이션을 사용해 의존성을 지연로딩
→ 빈의 초기화를 애플리케이션 시작 시점이 아닌 실제 사용되는 시점까지 지연 가능
@Service
@RequiredArgsConstructor
public class DeliveryRouteService {
private final DeliveryRouteRepository deliveryRouteRepository;
@Lazy // 지연 로딩 적용
private final HubRouteClient hubRouteClient;
@Lazy // 지연 로딩 적용
private final DeliveryManagerClient deliveryManagerClient;
...
}
✅ 문제 원인
Caused by: java.lang.IllegalStateException: No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalancer?
✅ 해결 방법
로드밸런서 의존성 명시적으로 추가
🤔 왜 로드밸런싱을 추가했어야 했는가?
배운점
중요한 기능은 항상 명시적으로 의존성 선언하기
전이적 의존성에 의존하지 않기
@Lazy는 순환 참조 문제에는 효과적이나, 의존성 누락 문제는 해결할 수 없음
스케줄러 사용 시 주의점:
스케줄러는 애플리케이션 초기화 단계에서 의존성 문제를 빠르게 드러냄
외부 서비스 호출을 포함하는 스케줄러 사용 시 관련 의존성 신중히 검토
@Lazy로 초기화를 지연시켜도 스케줄러 작동 시점에는 모든 의존성이 필요함
### 중요한 발견: 순환 참조가 아닌 의존성 누락
처음에는 순환 참조 문제로 의심하여 @Lazy 어노테이션을 적용했으나, 로드밸런서 의존성을 추가한 후에는 @Lazy 어노테이션 없이도 정상적으로 작동했습니다. 이는 실제 문제가 순환 참조가 아니라 순수하게 의존성 누락이었음을 의미합니다.
이는 오류 메시지를 정확히 해석하는 것의 중요성을 보여줍니다. "Unsatisfied dependency" 오류는 순환 참조 문제일 수도 있지만, 이 경우에는 의존성 누락을 나타내는 것이었습니다. 결국 명확한 오류 메시지 해석과 근본 원인 분석이 문제 해결의 핵심이었습니다.
Spring Cloud 2020.0.0 이후 버전부터는 spring-cloud-starter-netflix-eureka-client가 자동으로 spring-cloud-starter-loadbalancer를 포함
근데? 나는?
Caused by: java.lang.IllegalStateException: No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalancer?
Spring Cloud Feign 클라이언트를 사용할 때 로드밸런싱 기능이 필요한데 관련 의존성이 누락됐다고한다..
Feign Client는 @FeignClient(name="service-name") 어노테이션을 통해 서비스 이름으로 요청을 보냄
로드밸런서는 이 서비스의 이름을 실제 서비스 인스턴스 URL로 변환하고 여러 인스턴스 간의 요청을 분산시킴.
Spring Cloud Feign → LoadBalancer → Service Discovery
@Scheduled 어노테이션은 Spring의 TaskScheduler 호출
Spring Cloud 환경에서 Feign 클라이언트를 주기적으로 호출
스케줄러가 다른 서비스를 호출하는 Feign Client를 자동으로 실행함
스케줄러: 자동으로 주기적인 서비스 호출 → 애플리케이션 시작 시점부터 로드밸런서가 필요
시스템 초기화 순서:
애플리케이션 시작 → 스케줄러 초기화 → TaskScheduler 빈 생성 → Feign Client 초기화 → 로드밸런서 필요
이 흐름에서 로드밸런서가 없으면 Feign Client 초기화가 실패하고, 결국 애플리케이션 시작이 실패합니다.
일반 API 컨트롤러에서는 사용자가 요청할 때만 이런 호출이 발생하므로, 애플리케이션 시작 시점에 로드밸런서 의존성이 확인되지 않을 수 있습니다. 하지만 스케줄러는 애플리케이션 시작 시점에 초기화되므로, 로드밸런서 의존성이 즉시 확인됩니다.
결론적으로, 스케줄러가 직접 로드밸런서를 필요로 하는 것이 아니라, 스케줄러가 호출하는 코드(Feign Client)가 로드밸런서를 필요로 합니다. 그리고 스케줄러는 애플리케이션 시작 시점에 이 의존성을 즉시 드러나게 만드는 역할을 한 것입니다.