본문 바로가기

강의/토비의 스프링부트

@Configuration과 proxyBeanMethods

학습 테스트

남이 만든 코드를 동작 방식을 정확하게 이해할 수 있도록

테스트 코드로 샘플을 만드는 것이다.

 

어떤 기술을 정확하게 이해하고 싶을때

테스트 코드를 학습 목적으로 만들 수 있다.

 

테스트 코드로 사용법을 이해하고

이걸 사용하는 코드가 어떤 식으로 작성이 되는가 연습할 수 있는 기회가 될 수 있다.

 

configuration의 가장 default 구성 그 특징을 살펴보는 코드를 만들어 볼 것이다.

configuration의 특징은 이 안에 bean이라는 어노테이션이 붙은 메서드를 많이 가지고 있다.

각각의 메서드들이 자바 코드에 의해서 빈 오브젝트를 생성하고

다른 오브젝트의 관계를 설정하는 부분을 담당하게 된다.

 

이자체로는 평범하게 팩토리 메서드로 동작하면 된다고 하지만

생각보다 단순하지는 않다.

 

Bean1, Bean2가 있다고 가정

두가지가 모두 Common이라는 bean에 의존한다고 가정을 해보자.

 

spring에 등록되는 빈 오브젝트는 특별한 경우가 아니면 singleton으로 등록이 된다.

bean1이 의존하고 있는 Common 오브젝트와 bean2가 의존하고 있는 common 오브젝트가 정확히 동일해야 한다.

 

그런데 자바 코드로 팩토리 메서드를 만들어서 각 빈을 생성하는 메서드를 상호 호출하는 방식으로 의존관계를 주입하면 이 룰을 지킬 수가 없다.

 

빈 클래스를 만들어보자.

 

실패한 테스트이다.

package com.example.tobyboot.study;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class ConfigurationTest {
    @Test
    void configuration() {
        // 주소값까지 같은가 same
        MyConfig myConfig = new MyConfig();
        Bean1 bean1 = myConfig.bean1();
        Bean1 bean2 = myConfig.bean2();
        Assertions.assertThat(bean1.common).isSameAs(bean2.common);
    }

    @Configuration
    static class MyConfig {
        @Bean
        Common common() {
            return new Common();
        }

        @Bean
        Bean1 bean1() {
            return new Bean1(common());
        }

        @Bean
        Bean1 bean2() {
            return new Bean1(common());
        }
    }

    static class Bean1 {
        private final Common common;

        Bean1(Common common) {
            this.common = common;
        }
    }

    static class Bean2 {
        private final Common common;

        Bean2(Common common) {
            this.common = common;
        }
    }

    static class Common {

    }
}

 

그런데 재미잇는 것은

MyConfig라는 클래스를 스프링 컨테이너의 구성정보로 사용하게 되면 동작하는 방식이 달라진다.

 

성공한 테스트가 되어버렸다.

 

package com.example.tobyboot.study;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class ConfigurationTest {
    @Test
    void configuration() {
        // 주소값까지 같은가 same
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
        ac.register(MyConfig.class);
        ac.refresh();

        Bean1 bean1 = ac.getBean(Bean1.class);
        Bean2 bean2 = ac.getBean(Bean2.class);


//        MyConfig myConfig = new MyConfig();
//        Bean1 bean1 = myConfig.bean1();
//        Bean1 bean2 = myConfig.bean2();
        Assertions.assertThat(bean1.common).isSameAs(bean2.common);
    }

    @Configuration
    static class MyConfig {
        @Bean
        Common common() {
            return new Common();
        }

        @Bean
        Bean1 bean1() {
            return new Bean1(common());
        }

        @Bean
        Bean1 bean2() {
            return new Bean1(common());
        }
    }

    static class Bean1 {
        private final Common common;

        Bean1(Common common) {
            this.common = common;
        }
    }

    static class Bean2 {
        private final Common common;

        Bean2(Common common) {
            this.common = common;
        }
    }

    static class Common {

    }
}

 

Configuration annotation이 붙은 클래스가 스프링 컨테이너에서 사용되어질 때 발생되는 마법과 같은 것이다.

사실 스프링의 여러가지 설계 결정중에 이것이 가장 불만이었다.

어떤 자바 코드가 스프링 밖에 환경에서 사용되어 질 때 이게 어떻게 동작될지 기대되는 것과

스프링 빈에 등록되어서 동작하는 것의 동작 방식이 달라진다.

 

기본적으로 Configuration proxyBeanMethods가 default가 true로 설정되어있는 경우로

MyConfig가 Configuration으로 등록될때 proxy 오브젝트를 앞에 두고 등록되어진다.

 

어떤식으로 proxy가 만들어지는지 간단히 보자

 

package com.example.tobyboot.study;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class ConfigurationTest {
    @Test
    void configuration() {
        // 주소값까지 같은가 same
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
        ac.register(MyConfig.class);
        ac.refresh();

        Bean1 bean1 = ac.getBean(Bean1.class);
        Bean2 bean2 = ac.getBean(Bean2.class);


//        MyConfig myConfig = new MyConfig();
//        Bean1 bean1 = myConfig.bean1();
//        Bean1 bean2 = myConfig.bean2();
        Assertions.assertThat(bean1.common).isSameAs(bean2.common);
    }

    static class MyConfigProxy extends MyConfig {
        private Common common;
        @Override
        Common common() {
            if (this.common == null) this.common = super.common();
            return this.common;
        }
    }

    @Configuration
    static class MyConfig {
        @Bean
        Common common() {
            return new Common();
        }

        @Bean
        Bean1 bean1() {
            return new Bean1(common());
        }

        @Bean
        Bean1 bean2() {
            return new Bean1(common());
        }
    }

    static class Bean1 {
        private final Common common;

        Bean1(Common common) {
            this.common = common;
        }
    }

    static class Bean2 {
        private final Common common;

        Bean2(Common common) {
            this.common = common;
        }
    }

    static class Common {

    }
}

 

프록시가 되면 어떤 재미있는 것을 할 수 있냐면

위에서 configuration annotation이 붙은 클래스가 스프링 빈에 등록되어 사용되어질때

독특한 동작 방식을 흉내낼 수 있다.

 

프록시는 데코레이터처럼 타겟 오브젝트를 따로 두고 자기가 동적으로 끼어들어서 중계하는 역할이 아니라

애초에 확장해서 대치하는 방식으로 동작을 한다.

 

package com.example.tobyboot.study;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class ConfigurationTest {
    @Test
    void configuration() {
        // 주소값까지 같은가 same
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
        ac.register(MyConfig.class);
        ac.refresh();

        Bean1 bean1 = ac.getBean(Bean1.class);
        Bean2 bean2 = ac.getBean(Bean2.class);


//        MyConfig myConfig = new MyConfig();
//        Bean1 bean1 = myConfig.bean1();
//        Bean1 bean2 = myConfig.bean2();
        Assertions.assertThat(bean1.common).isSameAs(bean2.common);
    }

    @Test
    void proxycommonMethod() {
        MyConfigProxy myConfigProxy = new MyConfigProxy();
        Bean1 bean1 = myConfigProxy.bean1();
        Bean2 bean2 = myConfigProxy.bean2();

        Assertions.assertThat(bean1.common).isSameAs(bean2.common);
    }

    static class MyConfigProxy extends MyConfig {
        private Common common;
        @Override
        Common common() {
            if (this.common == null) this.common = super.common();
            return this.common;
        }
    }

    @Configuration
    static class MyConfig {
        @Bean
        Common common() {
            return new Common();
        }

        @Bean
        Bean1 bean1() {
            return new Bean1(common());
        }

        @Bean
        Bean2 bean2() {
            return new Bean2(common());
        }
    }

    static class Bean1 {
        private final Common common;

        Bean1(Common common) {
            this.common = common;
        }
    }

    static class Bean2 {
        private final Common common;

        Bean2(Common common) {
            this.common = common;
        }
    }

    static class Common {

    }
}

 

스프링 5.2에서 configuration element로 proxyBeanMethods를 엘리먼트로 추가하고

proxy를 끌 수 있도록 했다.

Configuration element로 등록하더라도 proxy로 동작하지 않고 자바 코드로 동작하게 할 수 있다는 것이다.

 

내가 만약에 bean 메서드를 통해서 bean 오브젝트를 만들때

또다른 bean 메서드를 호출해서 의존 오브젝트를 가져오는 코드를 만들지 않았다면

매번 비용이 드는 프록시를 사용할 필요가 없다.

 

예전에는 proxyBeanMethods를 기본적으로 true로 사용할 것을 권장했지만

최근에는 bean annotation이 붙은 메서드가 또다른 bean 메서드를 호출하지 않고

이자체로 충분한 메서드로 사용된다면 false로 둬도 상관없다고 한다.

 

EnableScheduling을 살펴보면 @import를 해서 다른 클래스를 로딩하도록 되어있고

SchedulingConfiguration은 다른 object를 의존하고 있지 않기 때문에 @Configuration의 proxyBeanMethods를 false로 두고있다.