본문 바로가기
backend/Spring

[spring] @Configuration의 프록시 객체

by seongju.lee 2023. 3. 10.

수동 빈 등록이 싱글톤 보장될 수 있는 이유

사실, 스프링에서 @Bean 애노테이션을 사용하여 수동 등록을 해도 싱글톤으로 등록된다.

하지만, 문제는 내부의 의존관계를 메서드 호출을 통해서 주입해주는 경우 발생하게 된다.

이러한 경우에는 객체가 새로 생성되기 때문에 DI컨테이너 내에 싱글톤이 깨지게 된다.

이 문제를 해결해주기 위한 애노테이션이 바로 @Configuration인 것이다.

@Bean을 사용하여 수동으로 빈을 등록할 때, @Configuration을 적어줌으로써 싱글톤이 유지되게 된다.

 

 

@Configuration의 유무에 따른 싱글톤 보장 여부

예제코드와 함께 직관적으로 살펴보면 아래와 같다.

 

- ItemService와 ItemRepository 클래스.

public class ItemRepository { }

@Getter
@RequiredArgsConstructor
public class ItemService{
	private final ItemRepository itemRepository;
}

 

- 빈 등록 & 의존성 주입을 위한 설정파일 (AppConfig 파일)

public class AppConfig{

    @Bean
    public ItemRepository itemRepository() {
        return new ItemRepository();
    }

    @Bean
    public ItemService itemService1() {
        return new ItemService(itemRepository());
    }

    @Bean
    public ItemService itemService2() {
        return new ItemService(itemRepository());
    }
    
}

 

 

- 위와 같은 두 개의 파일이 있고, 아래와 같이 테스트코드를 작성한다면 어떻게 될까?

@Test
void SingletonTest() {

    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    ItemRepository itemRepository = ac.getBean(ItemRepository.class);
    ItemService itemService1 = ac.getBean( "itemService1", ItemService.class);
    ItemService itemService2 = ac.getBean( "itemService2", ItemService.class);

    assertThat(itemService1.getItemRepository()).isSameAs(itemRepository);
    assertThat(itemService2.getItemRepository()).isSameAs(itemRepository);
}

- 서로 다른 빈인 itemService1과 itemService2를 불러온다.

- ItemService클래스 내에서 ItemRepository 객체를 생성하고 반환해 주는데,
만약 싱글톤이 보장된다면 "itemService1 빈과 itemService2 빈의 getItemRepository()의 결과" "itemRepository"와
같은 빈이여야 할 것이다.

- 위 테스트의 실행 결과는 Fail이다.

- 당연하게도 그 이유는 itemService1이나 itemService2 빈 생성 시에, ItemRepository 객체를 새로 생성해서 의존관계를 주입 하기 때문이다.

 

이 문제를 해결하기 위한 것이 바로 @Configuration 애노테이션이다.

위 AppConfig 파일에 아래와 같이 @Configuration애노테이션을 붙여보자.

@Configuration
public class AppConfig{

    @Bean
    public ItemRepository itemRepository() {
        return new ItemRepository();
    }

    @Bean
    public ItemService itemService1() {
        return new ItemService(itemRepository());
    }

    @Bean
    public ItemService itemService2() {
        return new ItemService(itemRepository());
    }
    
}

- 그러고 나서, 동일한 테스트 코드를 실행하면 Pass가 뜰 것이다.

- 즉 itemService1과 itemService2 모두 동일한 ItemRepository를 참조하는 것이라고 볼 수 있다.

 

 

@Configuration의 CGLIB 프록싱

애노테이션 하나 붙였다고 싱글톤이 바로 적용되는 이유는 바로 AppConfig파일에 @Configuration을 적용시킴으로써
AppConfig@CGLIB이라는 가짜 프록시 객체를 빈으로 등록하기 때문이다.

 

 

실제로 @Configuration의 유무에 따라서 itemRepository라는 스프링 빈을 조회해 보면 아래와 같다.

 

- @Configuration 미적용 상태.

AppConfg 객체가 Bean으로 등록되어 있다.

 

- @Configuration 적용 상태

AppConfg의 가짜 프록시 객체가 Bean으로 등록되어 있다.

 

위와 같이 @Configuration 유무에 따라 등록되는 스프링 빈이 달라지게 된다.

@Configuration를 적용하면 스프링이 CGLIB이라는 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 클래스를 만들고, 등록하는 것이다.

 

그래서, 이후에 AppConfig를 상속받아 만들어진 AppConfig@CGLIB에서 @Bean이 붙은 메서드가 컨테이너에 존재하는지 확인하고, 등록 혹은 생성 후 등록을 통해서 싱글톤이 보장되게끔 하는 것이다.

 

 

CGLIB 프록싱의 규칙

사실 이 포스팅을 하게 된 이유이기도 하다.

크게 두 가지 규칙을 기억하면 된다.

 

1. CGLIB 프록싱은 결국 상속을 통해서 이루어진다. 즉, 상속 규칙을 따라야한다.

  • @Configuration이 적용되는 클래스는 final이면 안된다.
  • @Bean이 적용되는 메소드는 final이나 private이면 안된다.

너무나도 당연하다. 자바는 private이나 final은 상속을 지원하지 않기 때문이다.

 

2. static @Bean 메소드는 당연히 오버라이딩이 불가하기 때문에, CGLIB 프록싱이 적용되지 않는다.