Spring

[Spring] @Configuration 안에서 @Bean을 사용해야 하는 이유

jeong_ii 2024. 12. 31. 01:19

Spring에서 수동으로 빈을 등록할 때에는 @Configuration 클래스 안에서 @Bean을 사용해야 한다.

🔹 @Configuration 안에서 @Bean을 사용해야 하는 이유

[ @Bean 어노테이션을 이용한 수동 빈 등록 ]

스프링에서는 일반적으로 컴포넌트 스캔을 사용해 자동으로 빈을 등록하는 방법을 이용한다.

하지만 @Bean 어노테이션을 사용해 수동으로 빈을 등록해야 하는 경우도 있다.

 

1. 개발자가 직접 제어가 불가능한 라이브러리를 활용할 때

2. 애플리케이션 전범위적으로 사용되는 클래스를 등록할 때

3. 다형성을 활용하여 여러 구현체를 등록해 주어야 할 때

 

@Bean을 이용한 수동 빈 메서드는 스프링 빈 안에만 구현되어 있다면 모두 동작된다.

하지만 스프링에서 @Bean은 반드시 @Configuration 어노테이션을 활용하도록 강조한다.

그 이유는 @Configuration에 특별한 부가 기능이 적용되기 때문이다.

 

[ @Configuration에 적용되는 프록시 패턴 ]

@Configuration 어노테이션 안에는 @Component 어노테이션이 붙어있어서 @Configuration 붙어있는 클래스 역시 스프링 빈으로 등록된다. 그럼에도 불구하고 스프링이 @Configuration을 따로 만든 이유는 CGLib으로 프록시 패턴을 적용해 수동으로 등록하는 스프링 빈이 반드시 싱글톤으로 생성됨을 보장하기 위해서이다.

 

예를 들어, 다음과 같이 스프링 빈으로 등록하고자 하는 클래스가 있다.

public class BeanResource {

}

위의 클래스를 @Component를 이용해 자동으로 빈 등록을 한다면

스프링이 해당 클래스의 객체 생성을 제어하게 되고(제어의 역전, IoC), 한 개의 객체만 생성되도록 컨트롤할 수 있다.

하지만 위의 클래스를 @Bean을 사용해 직접 빈으로 등록한다면

다음과 같이 해당 빈 등록 메서드를 여러 번 호출할 수 있게 된다.

@Configuration
public class BeanConfiguration {
    @Bean
    public BeanResource beanResource() {
        return new BeanResource();
    }

    @Bean
    public FirstBean firstBean() {
        return new FirstBean(beanResource);
    }

    @Bean
    public SecondBean secondBean() {
        return new SecondBean(beanResource);
    }
}

실수로 위와 같이 빈을 생성하는 메서드를 여러 번 호출했다면, 불필요하게 여러 개의 빈이 생성된다.

스프링은 이러한 문제를 방지하고자 @Configuration이 있는 클래스를 객체로 생성할 때 CGLib 라이브러리를 사용해 프록시 패턴을 적용한다. 그래서 @Bean이 있는 메서드를 여러 번 호출해도 항상 동일한 객체를 반환하여 싱글톤을 보장한다.

 

이를 이해하기 쉬운 코드로 나타내면 다음과 같다.

public class BeanConfigurationProxy extends BeanConfiguration {
    
    private Object source;

    @Override
    public BeanResource beanResource() {
        if (beanResource == null) {
            source = super.beanResource();
        }

        return source;
    }

    @Override
    public FirstBean firstBean() {
        return super.firstBean();
    }

    @Override
    public SecondBean secondBean() {
        return super.secondBean();
    }
}

CGLib는 상속을 사용해 프로시를 구현함으로 다음과 같이 프록시가 구현된다고 이해할 수 있다.

물론 실제로는 이렇게 생성되지 않고, 내부 클래스를 사용하는 등의 차이가 있으므로 이해를 돕기 위한 코드로만 생각하면 된다.

 

CGLib 프록시 동작 원리를 조금 더 자세히 살펴보자.

 

[ CGLib 프록시 동작 원리 ]

public class BeanConfigurationProxy extends BeanConfiguration {
    
    private BeanResource beanResourceInstance;

    @Override
    public BeanResource beanResource() {
        if (beanResourceInstance == null) {
            System.out.println("Creating BeanResource for the first time");
            beanResourceInstance = super.beanResource();
        } else {
            System.out.println("Returning cached BeanResource");
        }

        return beanResourceInstance;
    }
}

[ 사용 예시 ]

public class Main {
    public static void main(String[] args) {
        BeanConfigurationProxy proxy = new BeanConfigurationProxy();

        // 첫 번째 호출 : 새로운 객체 생성
        BeanResource resource1 = proxy.beanResource();

        // 두 번째 호출 : 캐싱된 객체 반환
        BeanResource resource2 = proxy.beanResource();
        
        // 세 번째 호출 : 여전히 동일한 객체 반환
        BeanResource resource3 = proxy.beanResource();
        
        // 동일한 객체인지 확인
        System.out.println(resource1 == resource2); // true;
        System.out.println(resource2 == resource3); // true;
    }
}

[ 실행 결과 ]

Creating BeanResource for the first time
Returning cached BeanResource
Returning cached BeanResource
true
true

 

[ 싱글톤 여부를 제어하기 위한 proxyBeanMethods ]

대부분의 경우 @Bean에 의한 수동 빈을 등록할 때, 싱글톤으로 생성되기를 원한다.

하지만 @Bean 메서드를 호출할 때 의도적으로 매번 다른 객체가 생성되기를 원할 수도 있다.

그럴 때는 @Configuration 어노테이션이 갖고 있는 proxyBeanMethods를 false로 설정해 주면 된다.

 

예를 들어, 위에서 작성했던 예제 코드에 아래와 같이 proxyBeanMethods를 false로 설정해 보자.

@Configuration(proxyBeanMethods = false)
public class BeanConfiguration {
    @Bean
    public BeanResource beanResource() {
        return new BeanResource();
    }

    @Bean
    public FirstBean firstBean() {
        return new FirstBean(beanResource);
    }

    @Bean
    public SecondBean secondBean() {
        return new SecondBean(beanResource);
    }
}

그러면 위의 설정 클래스에 대해서는 프록시가 적용되지 않으며 모든 @Bean 메서드 호출마다 새로운 객체를 생성한다. 하지만 매번 새로운 객체의 생성을 필요로 하는 경우는 거의 없다.

@Configuration이 아니라면 빈이 싱글톤임을 보장받을 수 없으므로 반드시 @Configuration 안에 @Bean을 사용하자.



 

 

 

📃 reference