@Configuration과 Singleton

namkun·2022년 3월 10일
0

Spring

목록 보기
6/18

해당 내용은 '스프링 입문을 위한 자바 객체 지향의 원리와 이해'와 인프런 김영한님의 '스프링 핵심 원리 - 기본편' 강의를 참고하였습니다.


이전에 우리의 Config 파일을 보자.

ApplicationConfig.java

@Configuration
public class ApplicationConfig {
    @Bean
    public Car carFuelTank() {
        return new CarImpl(getFuelTank());
    }

    @Bean
    public FuelTank getFuelTank() {
        return new GasolineOil();
//        return new ElectricBattery();
    }
}

위의 파일에서 보면 Bean에 등록될 때 다음과 같은 방식으로 진행될 것이다.

@Bean carFuelTank() -> new Gasolineoil();

만약에 여기에서 저 getFuelTank() 메서드를 사용하는 추가적인 Bean이 생긴다면 어떻게 될까?

ApplicationConfig.java

@Configuration
public class ApplicationConfig {
    @Bean
    public Car carFuelTank() {
        return new CarImpl(getFuelTank());
    }

    @Bean
    public FuelTank getFuelTank() {
        return new GasolineOil();
//        return new ElectricBattery();
    }
    
    @Bean
    public FixService fixService(){
        return new FixServiceImpl(getFuelTank(), getTransmissionInfo());
    }
    
    @Bean
    public Transmission getTransmissionInfo(){
        return new AutoTransmission();
    }
}

위와 같이 변한다면, fixService() 에서도 getFuelTank() 를 호출하고 그렇다면 Bean에 등록될 때 아래와 같은 방식도 같이 진행될 것이다.

@Bean fixService() -> new Gasolinoil(), new AutoTransmission();

뭔가 이상하다.

우리가 앞에서 배웠던대로라면, new Gasolinoil() 이 각각 두 번 호출 된 순간 싱글톤 패턴은 깨졌어야 한다.

근데 왜 아무런 문제 없이 실행이 될까?

혹시 new Gasoline()이 같은 연료탱크(FuelTank)를 가리키는 것일까?

테스트 코드를 짜서 확인해보자.

우선 갖다가 사용하는 곳(CarImpl, FixServiceImpl)에 아래와 같은 메서드를 만든다.

    // Test
    public FuelTank getFuelTank() {
        return fuelTank;
    }

그리고 호출해보자.

Test.java

@Test
void test(){
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(ApplicationConfig.class);

        CarImpl carFuelTank = annotationConfigApplicationContext.getBean("carFuelTank", CarImpl.class);
        FixServiceImpl fixService = annotationConfigApplicationContext.getBean("fixService", FixServiceImpl.class);

        System.out.println("carFuelTank.getFuelTank() = " + carFuelTank.getFuelTank());
        System.out.println("fixService.getFuelTank() = " + fixService.getFuelTank());
    }

결과는?

carFuelTank.getFuelTank() = car.tank.GasolineOil@d889905
fixService.getFuelTank() = car.tank.GasolineOil@d889905

같다!

분명 자바 코드에서는 두번 new를 하는데 왜 한번만 만들어서 공유하고 있을까?

다시 한번 확인해보자.

다음과 같이 ApplicationConfig.java를 수정해보자.

ApplicationConfig.java

@Configuration
public class ApplicationConfig {
    @Bean
    public Car carFuelTank() {
        System.out.println("ApplicationConfig.carFuelTank");
        return new CarImpl(getFuelTank());
    }

    @Bean
    public FuelTank getFuelTank() {
        System.out.println("ApplicationConfig.getFuelTank");
        return new GasolineOil();
//        return new ElectricBattery();
    }

    @Bean
    public FixService fixService(){
        System.out.println("ApplicationConfig.fixService");
        return new FixServiceImpl(getFuelTank(), getTransmissionInfo());
    }

    @Bean
    public Transmission getTransmissionInfo(){
        System.out.println("ApplicationConfig.getTransmissionInfo");
        return new AutoTransmission();
    }
}

이렇게 해두고, 대략 몇 번 호출할지 예상해보자.

@Bean carFuelTank() -> new Gasolineoil();
@Bean fixService() -> new Gasolinoil(), new AutoTransmission();

getFuelTank는 3번(carFuelTank, fixService, 자기 자신 빈 등록) 호출되어야 하고,

getTransmissionInfo는 2번 (fixService, 자기 자신 빈 등록) 호출되어야 하고,

나머지는 전부 한번 호출되어야할 것이다.

결과는

ApplicationConfig.carFuelTank
ApplicationConfig.getFuelTank
ApplicationConfig.fixService
ApplicationConfig.getTransmissionInfo

각자 딱 1번씩만 호출되었다. 도대체 왜일까?

@Configuration 과 CGLIB를 이용한 싱글톤 보장

ApplicationConfig이 어떤식으로 스프링 컨테이너에 등록되어있는지 확인해보자.

    @Test
    void configurationDeep(){
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(ApplicationConfig.class);
        ApplicationConfig bean = annotationConfigApplicationContext.getBean(ApplicationConfig.class);
        System.out.println("bean = " + bean.getClass());
    }

결과

bean = class car.ApplicationConfig$$EnhancerBySpringCGLIB$$8108128f

만약 순수한 클래스였다면 class car.ApplicationConfig 라고 나왔어야 한다.

그러나 뒤에 $$EnhancerBySpringCGLIB$$8108128f 다음과 같은 게 붙었다.

이는 스프링에서 CGLIB라는 Byte code 조작 라이브러리를 사용해서 ApplicationConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈에 등록한 것을 의미한다.

우리가 ApplicationConfig.class를 통해서 조회를 했음에도 CGLIB class가 조회된 것은, CGLIB class가 ApplicationConfig 클래스를 상속받았기 때문이다.

임의의 다른 클래스가 싱글톤이 보장되도록 해준다.

CGLIB 라이브러리에서 이미 Spring Container에 존재하는 Bean이라면 생성하지 않고 꺼내오고, 아니면 생성하고 이를 컨테이너에 등록하는 방식으로 작동한다고 한다. (전체 내부를 알기엔 너무 복잡하다. 그러려니 하자.)

@Bean이 붙은 메서드마다 위의 로직과 같은 Byte code 조작을 하는 코드가 애플리케이션을 실행함과 동시에 내부에서 동적으로 생성된다는 것이다.

이러한 CGLIB 라이브러리는 @Configuration 어노테이션을 통해 제공되며,

우리는 이 어노테이션을 통해 싱글톤의 유지를 보장받는다.

만약 해당 어노테이션이 없다면 어떻게 될까?

물론 Bean은 등록된다. 그러나 싱글톤은 깨진다.

CGLIB 라이브러리를 사용하지 않게되기에 호출하면 호출하는 대로 전부 Bean이 생성된다.(모두 다른 인스턴스가 된다. 직접 호출해보면 알 수 있다.)

그러니 우리는 꼭 @Configuration 어노테이션을 함께 사용해주자 ㅎㅎ

profile
개발하는 중국학과 사람

0개의 댓글