Backend/Spring

스프링 컨테이너와 스프링 빈

olsohee 2023. 12. 28. 18:37

스프링 컨테이너

스프링은 스프링 컨테이너를 제공함으로써 이전에 직접 만들었던 AppConfig처럼 각 객체들의 생성과 의존관계를 관리해준다. 스프링 컨테이너가 객체 간의 의존관계를 관리해줌으로써 각 구현체 클래스들은 다른 구현체 클래스에 의존하지 않을 수 있다. 

 

이전에 정의했던 AppConfig에 스프링을 적용하면 다음과 같다.

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
}

 

스프링 컨테이너는 @Configuration이 붙은 클래스를 설정 정보로 사용한다. 그리고 @Bean이 붙은 메소드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 빈으로 등록한다

public class MemberServiceTest {

    MemberService memberService;

    @BeforeEach
    void init() {
//        AppConfig appConfig = new AppConfig();
//        memberService = appConfig.memberService();
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        memberService = applicationContext.getBean("memberService", MemberService.class); // 빈 이름, 반환 타입
    }
}

 

ApplicationContext를 스프링 컨테이너라고 한다. 기존에는 개발자가 AppConfig 객체를 생성하고 AppConfig에서 메소드를 호출하여 필요한 객체를 조회했다. 그러나 스프링을 적용함으로써 이제는 스프링 컨테이너에서 필요한 객체(빈)를 조회한다. 즉, 기존에는 개발자가 직접 자바 코드로 모든 것을 했다면, 이제는 스프링 컨테이너가 객체를 스프링 빈으로 등록하면, 개발자는 스프링 컨테이너에서 필요한 객체를 찾아서 사용한다. 

스프링 컨테이너의 생성 과정

1. 스프링 컨테이너 생성

new AnnotationConfigApplicationContext(AppConfig.class)를 통해 스프링 컨테이너를 생성한다. 스프링 컨테이너를 생성할 때는 생성자 인자로 설정 정보 클래스를 지정해주어야 한다. 여기서는 AppConfig.class를 설정 정보 클래스로 지정했다.

2. 스프링 빈 등록

스프링 컨테이너는 파라미터로 넘어온 설정 정보 클래스를 사용해서 스프링 빈을 등록한다. 이때 빈 이름은 메소드 이름을 사용하는데, @Bean(name = "memberService2")와 같이 name 속성을 통해 빈 이름을 지정해줄 수도 있다. 주의할 점은 빈 이름은 중복되면 안되므로 항상 다른 이름을 부여해야 한다.

3. 의존관계 설정 준비

스프링 컨테이너 안에서는 빈들 간에 의존관계를 설정할 준비를 한다.

4. 의존관계 주입

스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입한다(DI). 

싱글톤 컨테이너

싱글톤 패턴은 이미 만들어진 딱 1개의 객체를 공유해서 사용하므로 효율적이다. 그러나 다음과 같은 문제점을 갖는다.

  • 싱글톤 패턴을 구현하는 코드가 많이 들어간다.
  • 의존성이 높아진다: 싱글톤 패턴을 사용하는 경우 객체를 미리 생성한 뒤 필요할 때 정적 메소드를 이용한다. 따라서 클래스 간 의존성이 높아진다. 싱글톤 인스턴스가 변경되면 해당 인스턴스를 참조하는 모든 클래스를 수정해야 한다. 
  • private 생성자 때문에 상속이 불가하다: 싱글톤 패턴은 기본 생성자를 private으로 설정한다. 따라서 상속을 통한 자식 클래스를 만들 수 없다는 문제점이 있다. 
  • 테스트하기 힘들다: 싱글톤 패턴의 객체는 자원을 공유하고 있다. 이는 서로 독립적이어야 하는 단위 테스트를 하는데 문제가 된다. 따라서 독립적인 테스트를 위해서는 전역에서 공유되고 있는 객체의 상태를 매번 초기화해주어야 한다.

스프링이 제공하는 스프링 컨테이너는 위와 같은 싱글톤의 문제점은 해결하면서 객체를 싱글톤으로 관리한다. 

  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도 객체를 싱글톤으로 관리해준다. 즉 스프링 컨테이너는 싱글톤 컨테이너의 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다.
  • 따라서 스프링 컨테이너를 통해 싱글톤 패턴을 위한 코드가 들어가지 않아도 된다.
  • 그리고 DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.

그런데 싱글톤 방식에서 주의할 점이 있다. 따라서 스프링 컨테이너에 등록되는 객체를 정의할 때는 다음을 주의하자.

  • 싱글톤 패턴이든 스프링 컨테이너 같은 싱글톤 컨테이너이든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 객체를 공유하기 때문에 싱글톤 객체의 상태를 유지(stateful)하게 설계하면 안되며 무상태(stateless)로 설계해야 한다.
  • 따라서 가급적 읽기만 가능해야 하고, 외부에서 그 내부 필드 값을 변경하면 안된다.
  • 웬만해서는 필드 대신에 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.

@Configuration과 싱글톤

앞서 설정 정보 클래스에 @Configuration을 붙이면 스프링이 해당 클래스를 설정 정보로 참고하여 스프링 컨테이너를 생성한다고 했다. 그런데 @Configuration에는 숨겨진 비밀이 있는데, 바로 @Configuration을 통해 각 객체들이 싱글톤임이 보장된다는 것이다.

 

다음 AppConfig 코드를 보자. 

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}
  • memberService()를 호출하면서 memberRepository()가 호출되어, MemoryMemberRepository 객체가 생성된다.
  • orderService()를 호출하면서 memberRepository()가 호출되어, MemoryMemberRepository 객체가 생성된다.
  • memberRepository()를 호출하면서, MemoryMemberRepository 객체가 생성된다.

즉 총 3번 MemoryMemberRepository 객체가 생성된다. 그러면 싱글톤이 깨지는 것이 아닐까?

 

그러나 스프링 컨테이너가 생성되고 빈이 생성될 때 우리의 예상과 달리 MemoryMemberRepository 객체는 딱 한 번만 생성되어 스프링 컨테이너에 보관된다. 즉 MemberServiceImpl 객체에서 사용하는 MemoryMemberRepository, OrderServiceImpl에서 사용하는 MemoryMemberRepository 객체, MemberRepository의 구현체인 MemoryMemberRepository 객체는 모두 같은 객체이다. 

 

그렇다면 3번 생성될 거 같았던 객체가 어떻게 한 번만 생성되는 것일까? 이는 @Configuration 덕분이다.

 

스프링 컨테이너를 생성할 때 메소드의 인자로 넘겨준 설정 정보 클래스(AppConfig)도 스프링 컨테이너에 빈으로 등록된다. 따라서 다음과 같이 빈으로 등록된 AppConfig를 조회할 수 있다.

ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getCalss()); // bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70

 

그런데 출력 결과를 보면 AppConfig의 클래스 정보는 AppConfig가 아니라 CGLIB가 붙은 다른 클래스이다. 이는 우리가 만든 AppConfig 클래스가 아니라 스프링이 바이트코드 조작 라이브러리를 사용해서 만든임의의 클래스이다. 즉 @Configuration을 붙이면 스프링은 해당 클래스를 상속받는 임의의 클래스를 생성하고, 그 클래스를 스프링 빈으로 등록한다.

그리고 임의의 클래스가 다음과 같이 정의되어 있어서, 싱글톤을 보장하는 것이다.

@Bean
public MemberRepository memberRepository() {
	if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) { 
		return 스프링 컨테이너에서 찾아서 반환;
	} else { //스프링 컨테이너에 없으면
		기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록 
		return 반환
	}
}
  • @Bean이 붙은 메소드에서 반환하려는 객체가 스프링 컨테이너에 등록되어 있으면 ➡️ 스프링 컨테이너에서 찾아서 반환한다.
  • 그렇지 않으면 ➡️ 기존 AppConfig의 로직을 호출해서 새로 객체를 생성하고, 스프링 컨테이너에 등록한 후, 객체를 반환한다.

그렇다면 @Configuration을 적용하지 않고 @Bean만 적용하면 어떻게 될까? 이 경우에는 임의의 클래스가 아닌 우리가 정의한 AppConfig가 스프링 컨테이너에 등록된다. 따라서 AppConfig 내에서 특정 메소드가 중복으로 호출되면, 해당 객체는 싱글톤이 보장되지 않는다. 따라서 위 예제에서 MemoryMemberRepository 객체가 총 3개 생성된다. 결론적으로 @Bean만 사용해도 스프링 빈으로 등록되지만 싱글톤을 보장하지는 않는다.

 

정리하면 다음과 같다.

  • @Configuration을 붙임으로써 우리가 정의한 AppConfig가 아닌 AppConfig를 상속받는 임의의 클래스가 생성되어 스프링 빈으로 등록된다.
  • 그리고 이 임의의 클래스에는 스프링 컨테이너에 해당 객체가 등록되어 있는지를 확인하며, 각 메소드가 반환하는 객체가 싱글톤임을 보장하도록 코드가 작성되어 있다.
  • 따라서 @Configuration을 통해 스프링 컨테이너가 싱글톤 컨테이너임이 보장되는 것이다. 

BeanFactory와 ApplicationContext 

  • BeanFactory
    • 스프링 컨테이너의 최상위 인터페이스이다.
    • 스프링 빈을 관리하고 조회하는 역할을 제공한다.
    • getBean() 메소드 등의 기능을 제공한다.
  • ApplicationContext
    • BeanFactory 기능을 모두 상속받아서 제공한다. 
    • BeanFactory의 빈 관리 기능 뿐만 아니라 다양한 부가 기능을 제공한다.
    • BeanFactory를 직접 사용할 일은 거의 없으며 부가기능이 포함된 ApplicationContext를 사용한다.
    • BeanFactory나 ApplicationContext를 스프링 컨테이너라고 한다.

BeanDefinition

스프링은 자바 기반의 설정 정보 클래스뿐만 아니라 xml 파일도 지원한다. 스프링은 이렇게 다양한 설정 형식을 지원할 수 있는 것은 BeanDefinition 덕분이다.

위와 같이 AnnotationConfigApplicationContext는 AnnotatedBeanDefinitionReader를 사용해서 AppConfig.class 파일을 읽고 BeanDefinition을 생성한다. 마찬가지로 GenericXmlApplicationContext는 XmlBeanDefinitionReader를 사용해서 appConfig.xml 파일을 읽고 BeanDefinition을 생성한다. 이때 BeanDefinition은 빈 설정 메타정보이고, @Bean이나 <bean> 하나당 각각 하나씩 메타정보가 생성된다.

그러면 스프링 컨테이너는 BeanDefinition을 읽어서 이를 기반으로 스프링 빈을 생성한다. 즉 스프링 컨테이너는 BeanDefinition이 어떤 형식의 설정 정보를 통해 생성됐는지 알 필요 없이 BeanDefinition만 알고 있으며 이를 기반으로 스프링 빈을 생성하는 것이다.


Reference

  • 인프런, 스프링 핵심 원리 - 기본편, 김영한