Quartz.NET에서 Autofac 의존성 주입 문제 해결하기

최근 작업 중인 프로젝트에서 Job 실행 중 발생하는 예외를 캐치한 후, 이메일이나 메시지를 통해 담당자에게 알리는 기능을 구현하게 되었습니다. 이 과정에서 의존성 주입과 관련된 흥미로운 문제를 발견하여 기록으로 남깁니다.

더 좋은 해결 방법이 있거나 오류가 있다면 언제든지 알려주시기 바랍니다.

문제 상황

프로젝트에는 기능이 다른 여러 Job이 존재하며, 각 Job은 예외 발생 시 메시지를 전송해야 합니다. 기존 Job 구조는 다음과 같습니다.

이 요구사항을 구현하고 유지보수를 용이하게 하기 위해, 이벤트 델리게이트 패턴을 적용하기로 결정했습니다. 예외 메시지 전송 로직을 분리하여 단일 책임 원칙을 지키는 방식입니다. 설계 방향은 다음과 같습니다.

  1. Job과 IJob 사이에 IExceptionJobHandler 인터페이스를 추가하여 제약 조건으로 사용합니다.
  2. 인터페이스 내에 PushException 이벤트를 정의하여 예외 메시지 전송을 담당합니다.
  3. 각 Job은 예외 발생 시 이 이벤트를 트리거하고, 외부에서 단일 구독자로 이벤트를 처리합니다.

현재까지는 이 접근 방식이 유효합니다. (인터페이스를 사용하는 이유는 부모 클래스보다 느슨한 결합을 제공하기 때문이며, 다른 곳에서도 재사용하기 쉽습니다.) 하지만 새로운 문제가 발생했습니다. 기존에는 IJob을 직접 상속받았지만, 이제 중간 계층이 추가되었으므로 Autofac을 통해 Job을 어떻게 주입해야 할까요?

문제 분석

이 문제를 해결하는 데 약 4시간이 소요되었습니다. 의존성 주입 방식을 연구하고 인터넷에서 검색했지만 진전이 없었습니다. 그러다 프레임워크의 의존성 주입 코드를 다시 검토하면서 방향을 잘못 잡았음을 깨달았습니다. Job 주입에는 Autofac.Extras.Quartz를 사용하고 있었습니다.

WebAPI에서 Quartz를 사용하려면 AppStart에 다음 코드를 추가해야 합니다.

var builder = new ContainerBuilder();
builder.RegisterModule(new QuartzAutofacFactoryModule
{
    ConfigurationProvider = c => schedulerConfig
});

이 QuartzAutofacFactoryModule 내부에 필요한 내용이 있을 것이라 판단하여, Github에서 Autofac.Extras.Quartz 소스 코드를 다운로드하여 분석했습니다. 예상대로 Job 의존성 주입 관련 코드를 발견했습니다. 하지만 Job 등록 및 생성 코드는 보이지 않았습니다. 화살표로 표시된 부분에 AutofacJobFactory 클래스가 있었으며, 이 클래스는 ILifetimeScope를 받아 Job을 스코프 단위로 생성하는 역할을 했습니다.

해결 방안

  1. AutofacJobFactory가 Job 서비스 인스턴스를 생성하는 공장임을 확인했습니다. 따라서 이 공장을 수정하여 접근해야 합니다. ResolveJobInstance 메서드가 IJob을 반환하는데, 여기서 사용자 정의 인터페이스 IExceptionJobHandler를 주입하여 Job 인스턴스가 해당 인터페이스의 이벤트를 갖도록 해야 합니다.
  2. 설계에 따라 IExceptionJobHandler는 IJob을 상속받습니다. 따라서 IJob을 IExceptionJobHandler로 대체할 수 있습니다. 이를 구현하는 방법을 고민했습니다.
  3. AutofacJobFactory의 서비스 생성 메서드는 가상 메서드(virtual) 이므로, 상속을 통해 문제를 해결할 수 있습니다. ExtendAutofacJobFactory 클래스를 새로 만들어 IJob을 IExceptionJobHandler로 대체했습니다.
public class ExtendAutofacJobFactory : AutofacJobFactory
{
    protected override IJob ResolveJobInstance(ILifetimeScope nestedScope, IJobDetail jobDetail)
    {
        // Job이 IExceptionJobHandler에서 파생되었는지 확인
        if (typeof(IExceptionJobHandler).IsAssignableFrom(jobDetail.JobType))
        {
            IExceptionJobHandler instance = null;
            instance = (IExceptionJobHandler)CreateIJob(nestedScope, jobDetail);
            // 이벤트 처리기 등록
            instance.PushException += pushExceptionMessageManager.Value.ExceptionJobHandler;
            return instance;
        }
        else
        {
            return base.ResolveJobInstance(nestedScope, jobDetail);
        }
    }
}
  1. 이제 재정의한 팩토리를 QuartzAutofacFactoryModule에 적용해야 합니다. 하지만 QuartzAutofacFactoryModule은 사용자 정의 팩토리를 설정할 수 있는 진입점을 제공하지 않습니다. 따라서 다시 상속을 사용하여 이 모듈을 재정의했습니다.
  2. QuartzAutofacFactoryModule을 수정하여 서비스 생성 팩토리를 ExtendAutofacJobFactory로 교체했습니다.
  3. AppStart의 Autofac 등록 코드에서 QuartzAutofacFactoryModule을 사용자 정의 ExtendQuartzAutofacFactoryModule으로 변경하여 주입을 완료했습니다.
builder.RegisterModule(new ExtendQuartzAutofacFactoryModule
{
    ConfigurationProvider = c => schedulerConfig
});

결론

객체지향의 특징(캡슐화, 상속, 다형성)을 물어보면 대부분의 개발자가 쉽게 대답할 수 있습니다. 하지만 실제 개발 경험이 쌓이면서 가장 기본적이고 단순한 지식이 가장 중요하다는 것을 깨닫게 됩니다. 이번 사례는 이러한 기본 개념이 실제 문제 해결에 어떻게 적용될 수 있는지 보여주는 좋은 예입니다.

태그: Quartz.NET Autofac 의존성주입 C# Job스케줄링

5월 23일 15:20에 게시됨