파란하늘의 지식창고
반응형

Spring DispatcherServlet은 어떻게 LocaleContext에서 Locale을 지연 실행하여 획득할 수 있을까?

spring-webmvc의 DispatcherServlet에는 Locale을 획득하는 코드가 다음과 같은 형태로 구현되어 있다. (전체 코드 참고)

@Override
protected LocaleContext buildLocaleContext(final HttpServletRequest request) {
    LocaleResolver lr = this.localeResolver;
    if (lr instanceof LocaleContextResolver) {
        return ((LocaleContextResolver) lr).resolveLocaleContext(request);
    }
    else {
        return () -> (lr != null ? lr.resolveLocale(request) : request.getLocale());
    }
}

반환되는 LocaleContext는 interface인데 다음과 같다.

public interface LocaleContext {

    @Nullable
    Locale getLocale();

}

위 buildLocaleContext method 코드에서 주목해야 할 부분은 else 구문의 lambda expression으로 사용한 부분이다.

위 구문을 lambda expression을 사용하지 않고 풀어쓰면 다음과 같다.

@Override
protected LocaleContext buildLocaleContext(final HttpServletRequest request) {
    LocaleResolver lr = this.localeResolver;
    if (lr instanceof LocaleContextResolver) {
        return ((LocaleContextResolver) lr).resolveLocaleContext(request);
    }
    else {
        return new LocaleContext() {

            @Override
            public Locale getLocale() {
                return lr != null ? lr.resolveLocale(request) : request.getLocale();
            }
            
        };
    }
}

즉 LocaleContext interface에 대한 새로 생성된 객체를 반환하며 LocaleContext는 interface이기 때문에 getLocale method에 대한 정의만 하게 된다.

buildLocaleContext가 호출되는 시점엔 LocaleContext 객체가 반환되는 것이고 그 반환된 LocaleContext 객체의 getLocale method를 실행하는 시점에 'lr != null ? lr.resolveLocale(request) : request.getLocale()' 내용이 실행되게 되는 것이다.

이렇게 실행이 늦춰지는 것을 지연 연산(Lazy Evaluation)이라고 한다.

실행되는 시점이 늦춰지는 것은 다음과 같은 이점이 있다.

  • 호출이 되지 않으면 아예 해당 내용이 실행되지 않을 수 있다. (불필요한 연산이 없어짐)
  • 순차 처리를 하는 시점에서 처리하지 못하는 것들도 해결할 수 있다. (Filter 단계에서 처리하지 않고 선언만 하여 Servlet에서 처리, Servlet 영역에서의 경우를 가정하고 처리가 가능해짐)
  • 선언 시점에 존재하지 않는 객체에 대한 사용 처리를 미리 선언하여 사용할 수 있게 되어 순서에 얽메이지 않아도 된다.
  • 선언 시점에 해당 객체 외부 변수를 사용하여 기존 로직의 참조가 편리하다.

코드가 복잡해진다는 단점이 생기지만 DispatcherServlet의 locale 획득 처리의 경우 외에도 다양한 경우 lazy evaluation을 사용하면 위와 열거한 이득을 얻을 수 있다.

Filter에서 발생한 exception을 servlet 내에서 처리하기

Filter는 servlet을 벗어난 범위이기 때문에 여기에서 발생한 exception은 Spring의 BasicErrorController의 에러 처리 지원을 사용하지 못한다.

하지만 이렇게 lazy evaluation으로 filter에서 exception을 처리하면 기존의 exception 처리와 같이 공통으로 사용할 수 있게 된다.

예를 들어 별도의 context를 만들어 사용하고 해당 context를 filter에서 설정하는데 설정하지 못하는 경우 exception을 발생시켜 올바른 요청이 아님을 처리하는 경우 lazy evaluation 처리를 통해 exception을 발생시키면 servlet에서의 공통 에러 처리를 사용할 수 있게 된다.

또한 servlet에 설정된 intercepor나 aop등의 영역 내에 동작함으로 얻을 수 있는 기능을 사용하는 것이 가능해진다.

Spring Cloud는 어떻게 Spring Boot의 properties의 정보를 가져다 쓸수 있을까?

Spring Cloud의 Bootstrapper에서 properties 정보 처리

Spring Boot 2.5.4 기준으로 Spring Application을 실행하면 대략 다음과 같은 순서로 실행을 한다. (전체 코드 참고)

public ConfigurableApplicationContext run(String... args) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    DefaultBootstrapContext bootstrapContext = createBootstrapContext();
    ConfigurableApplicationContext context = null;
    configureHeadlessProperty();
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting(bootstrapContext, this.mainApplicationClass);
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
        configureIgnoreBeanInfo(environment);
        Banner printedBanner = printBanner(environment);
        context = createApplicationContext();
        context.setApplicationStartup(this.applicationStartup);
        prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
        refreshContext(context);
        afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
        }
        listeners.started(context);
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, listeners);
        throw new IllegalStateException(ex);
    }

    try {
        listeners.running(context);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, null);
        throw new IllegalStateException(ex);
    }
    return context;
}

위 내용을 보면 context를 만들고 실행하기 전에 bootstrapContext를 만들고(createBootStrapContext() 호출 부분) 그 다음에 property를 설정하는 단계가 있다.

Spring Cloud 쪽 library는 이러한 bootstrapContext를 사용하여 spring이 실행되기 전 처리를 사용하는데 예를 들어 spring-cloud-zookeeper-discovery의 경우 META-INF/spring.factories에 다음과 같이 bootstrapContext가 설정되어 있다.

org.springframework.boot.Bootstrapper=\
org.springframework.cloud.zookeeper.discovery.configclient.ZookeeperConfigServerBootstrapper

이 bootstrapper 정보는 SpringApplication이 실행될 때 가져오게 된다.

아래 SpringApplication 생성자 코드에서 getBootstrapRegistryInitializersFromSpringFactories()  부분이다.

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = deduceMainApplicationClass();
}

이를 통해 bootstrapRegistryInitializers 변수에 initializer 목록이 추가되고 spring-cloud-zookeeper-discovery를 사용하면 해당 라이브러리의 META-INF/spring.factories에 설정된 ZookeeperConfigServerBootstrapper가 해당 변수에 추가되게 된다.

이 ZookeeperConfigServerBootstrapper의 initialize method의 코드를 보면 대략 다음과 같은 형태의 코드들이 계속 사용된다. (전체 코드 참고)

// create discovery
registry.registerIfAbsent(ZookeeperDiscoveryProperties.class, context -> {
    Binder binder = context.get(Binder.class);
    if (!isEnabled(binder)) {
        return null;
    }
    return binder.bind(ZookeeperDiscoveryProperties.PREFIX, Bindable
                    .of(ZookeeperDiscoveryProperties.class), getBindHandler(context))
            .orElseGet(() -> new ZookeeperDiscoveryProperties(new InetUtils(new InetUtilsProperties())));
});
//... 중간 생략
registry.registerIfAbsent(ServiceDiscoveryCustomizer.class, context -> {
    if (!isEnabled(context.get(Binder.class))) {
        return null;
    }
    CuratorFramework curator = context.get(CuratorFramework.class);
    ZookeeperDiscoveryProperties properties = context.get(ZookeeperDiscoveryProperties.class);
    InstanceSerializer<ZookeeperInstance> serializer = context.get(InstanceSerializer.class);
    return new DefaultServiceDiscoveryCustomizer(curator, properties, serializer);
});
//... 중간 생략
registry.registerIfAbsent(ZookeeperDiscoveryClient.class, context -> {
    Binder binder = context.get(Binder.class);
    if (!isEnabled(binder)) {
        return null;
    }
    ServiceDiscovery<ZookeeperInstance> serviceDiscovery = context.get(ServiceDiscovery.class);
    ZookeeperDependencies dependencies = binder.bind(ZookeeperDependencies.PREFIX, Bindable
            .of(ZookeeperDependencies.class), getBindHandler(context))
            .orElseGet(ZookeeperDependencies::new);
    ZookeeperDiscoveryProperties discoveryProperties = context.get(ZookeeperDiscoveryProperties.class);

    return new ZookeeperDiscoveryClient(serviceDiscovery, dependencies, discoveryProperties);
});

이 코드의 내용을 보면 initialize method의 매개변수인 BootstrapRegistry registry에 registerIfAbsent method를 사용하여 등록을 하는데 등록하는 key, value의 value값 부분이 lambda expression으로 이루어져있다.

이 registerIfAbsent method는 다음과 같은 형태이다.

<T> void registerIfAbsent(Class<T> type, InstanceSupplier<T> instanceSupplier);

여기의 InstanceSupplier가 lambda expression으로 넘겨받는 부분인데 InstanceSupplier class는 다음과 같다.

@FunctionalInterface
interface InstanceSupplier<T> {

    T get(BootstrapContext context);

    //... default, static method 부분 생략

}

InstanceSupplier는 get method만 가지고 있는 functional interface이므로 위에서 lambda expression으로 넘긴 값은 lambda expression을 제거하고 보면 다음과 같다.

// create discovery
registry.registerIfAbsent(ZookeeperDiscoveryProperties.class, new InstanceSupplier<ZookeeperDiscoveryProperties() {

    @Override
    public ZookeeperDiscoveryProperties get(BootstrapContext context) {
        Binder binder = context.get(Binder.class);
        if (!isEnabled(binder)) {
            return null;
        }
        return binder.bind(ZookeeperDiscoveryProperties.PREFIX, Bindable
                        .of(ZookeeperDiscoveryProperties.class), getBindHandler(context))
                .orElseGet(() -> new ZookeeperDiscoveryProperties(new InetUtils(new InetUtilsProperties())));
    }
    
});

즉 등록할 시점엔 ZookeeperDiscoveryProperties를 가지고 있지 않더라도 이렇게 lazy evaluation 처리로 행위 자체를 등록하고 이후에 호출되는 시점에 BootstrapContext에 생성되어 있는 ZookeeperDiscoveryProperties를 꺼내 쓸 수 있게 되는 것이다.

이를 통해 Spring Boot의 Properties 호출을 실행되기 전 Spring Cloud의 Bootstrapper에서 관련 선언을 먼저 할 수 있게 된다.

반응형
profile

파란하늘의 지식창고

@Bluesky_

도움이 되었다면 광고를 클릭해주세요