스프링 컨테이너와 스프링 빈, 싱글톤 컨테이너

현민·2022년 1월 20일
0

스프링 공부

목록 보기
7/9

인프런 김영한님의 스프링 강의를 듣고 정리한 내용입니다. 출처


스프링 컨테이너 생성 과정

  1. 스프링 컨테이너 생성
ApplicationContext applicationContext =
			new AnnotationConfigApplicationContext(AppConfig.class);
  • ApplicationContext를 스프링 컨테이너라고 한다.
  • ApplicationContext는 인터페이스이다.
  • 스프링 컨테이너는 XML을 기반으로 만들 수 있고 애노테이션 기반의 자바 설정 클래스로 만들 수 있다.
  1. 스프링 빈 등록
  • 스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록한다.
  • 빈 이름은 메서드 이름을 사용하고 빈 이름을 직접 부여할 수도 있다. @Bean(name="memberService2")
  1. 스프링 빈 의존관계 설정
  • 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입(DI)한다.
  • 단순히 자바 코드를 호출하는 것 같지만 차이가 있다.

스프링 빈 조회

기본

public class ApplicationContextBasicFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName() {
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("이름 없이 타입으로만 조회")
    void findBeanByType() {
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("구체 타입으로 조회")
    void findBeanByName2() {
        MemberService memberService = ac.getBean("memberService", MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("빈 이름으로 조회 X")
    void findBeanByNameX() {
        assertThrows(NoSuchBeanDefinitionException.class,
                () -> ac.getBean("xxxx", MemberService.class));
    }
    
    @Test
    @DisplayName("애플리케이션 빈 출력하기")
    void findApplicationBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            //Role ROLE_APPLICATION: 직접 등록한 애플리케이션 빈
            //Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + " object = " + bean);            }
        }
    }

}

스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회 방법
ac.getBean(빈이름, 타입)
ac.getBean(타입)
조회대상 스프링 빈이 없으면 예외 발생, NoSuchBeanDefinitionException

동일한 타입이 둘 이상인 경우

public class ApplicationContextSameBeanFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 잇으면, 중복 오류 발생함.")
    void findBeanByTypeDuplicate() {
        assertThrows(NoUniqueBeanDefinitionException.class,
                () -> ac.getBean(MemberRepository.class));
    }

    @Test
    @DisplayName("타입으로 조회시 같은 타임이 둘 이상 있으면, 빈 이름을 지정하면 된다.")
    void findBeanByName() {
        MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class);
        assertThat(memberRepository).isInstanceOf(MemberRepository.class);
    }

    @Test
    @DisplayName("특정 타입을 모두 조회하기")
    void findAllBeanByType() {
        Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
        System.out.println("beansOfType = " + beansOfType);
        assertThat(beansOfType.size()).isEqualTo(2);
    }

    @Configuration
    static class SameBeanConfig {

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

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

}

타입으로 조회시 같은 스프링 빈이 둘 이상이면 오류가 발생한다. 이때는 빈 이름을 지정하자.
ac.getBeansOfType()을 사용하면 해당 타입의 모든 빈을 조회 할 수 있다.

상속 관계

public class ApplicationContextExtendsFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName("부모 타입으로 조회시 자식이 둘 이상 있으면, 중복 오류가 발생한다")
    void findBeanByParentTypeDuplicate() {
        assertThrows(NoUniqueBeanDefinitionException.class,
                () -> ac.getBean(DiscountPolicy.class));
    }

    @Test
    @DisplayName("부모 타입으로 조회시 자식이 둘 이상 있으면, 빈 이름을 지정하면 된다")
    void findBeanByParentTypeBeanName() {
        DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
        assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("특정 하위 타입으로 조회")
    void findBeanBySubType() {
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
        assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기")
    void findAllBeanByParentType() {
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        assertThat(beansOfType.size()).isEqualTo(2);
    }

    @Configuration
    static class TestConfig {

        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }

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

}

부모타입으로 조회하면, 자식타입도 함께 조회한다.
모든 자바 객체의 최고 부모인 Object타입으로 조회하면 모든 스프링 빈을 조회한다.

BeanFactory와 ApplicationContext

BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스이다.
  • 스프링 빈을 관리하고 조회하는 역할을 담당한다.
  • getBean()을 제공한다.

ApplicationContext

  • BeanFactory 기능을 모두 상속받아서 제공한다.
  • 빈을 관리하고 조회하는 기능뿐만아니라 여러 부가기능을 제공한다.

  • 메세지소스를 통한 국제화 기능
  • 환경변수 : 로컬, 개발, 운영 등을 구분해서 처리
  • 애플리케이션 이벤트 : 이벤트를 발행하고 구독하는 모델을 편리하게 지원
  • 편리한 리소스 조회 : 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회

BeanFactory를 직접 사용할 일은 거의 없고 부가기능이 포함된 ApplicationContext를 사용한다.
BeanFactory나 ApplicationContext를 스프링 컨테이너라고 한다.

다양한 설정 형식 지원 - 자바코드 , XML

애노테이션 기반 자바코드 설정 사용

지금까지 했던 것들이 애노테이션 기반임.

XML 설정 사용

최근에는 스프링 부트를 많이 사용하면서 XML기반 설정은 잘 사용하지 않는다.
하지만 많은 레거시 프로젝트들이 XML로 되어있고 XML을 사용하면 컴파일 없이 빈 설정 정보를 변경 할 수 있는 장점도 있다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://
www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="memberService" class="hello.core.member.MemberServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
    </bean>

    <bean id="memberRepository" class="hello.core.member.MemberRepository"/>

    <bean id="orderService" class="hello.core.order.OrderServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository"/>
        <constructor-arg name="discountPolicy" ref="discountPolicy"/>
    </bean>

    <bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy" />

</beans>
public class XmlAppContext {

    @Test
    void xmlAppContext() {
        ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

스프링 빈 설정 메타 정보 - BeanDefinition

스프링이 이렇게 다양한 설정 형식을 지원하는 것의 중심에는 BeanDefinition이라는 추상화가 있다.

BeanDefinition을 빈 설정 메타정보라고 하고, @Bean, <bean>당 각각 하나씩 메타 정보가 생성된다.
스프링 컨테이너는 이 메타 정보를 기반으로 스프링 빈을 생성한다.

싱글톤 컨테이너

public class SingleTonTest {

    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer(){
        AppConfig appConfig = new AppConfig();

        MemberService memberService1 = appConfig.memberService();
        MemberService memberService2 = appConfig.memberService();

        assertThat(memberService1).isNotSameAs(memberService2);
    }
}

대부분의 스프링 애플리케이션은 웹 애플리케이션이고, 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다.
위 테스트코드를 통해 알 수 있듯이 전 글에서 만들었던 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다.
이렇게 된다면 만약 고객 트래픽이 초당 100이 나오면 초당 100개의 객체가 생성되고 소멸되는데 메모리 낭비가 너무 심하다. 따라서 이 문제를 해결하기위해 해당 객체가 딱 1개만 생성되고 공유되도록 설계하는 싱글톤 패턴을 이용한다.

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴
public class SingleTonService {

    private static final SingleTonService instance = new SingleTonService();

    public static SingleTonService getInstance() {
        return instance;
    }

    private SingleTonService(){
    }
}

static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.
이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다.
딱 1개의 객체 인스턴스만 존재해야 하므로 생성자를 private으로 막아서 혹시라도 외부에서 객체 인스턴스가 생성되는 것을 막는다.

싱글톤 패턴의 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존 관계상 클라이언트가 구체 클래스에 의존하기때문에 DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP를 위반할 가능성이 높다.
  • 테스트하기 어렵고 내부 속성을 변경하거나 초기화하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.

결론적으로 유연성이 떨어지고 안티패턴으로 불리기도 한다.

싱글톤 컨테이너

스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리한다.

  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다.
  • DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용 할 수 있다.

싱글톤 방식의 주의점

객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다. 무상태(stateless)로 설계해야 한다.

  • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
  • 가급적 읽기만 가능해야 한다.
  • 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal등을 사용해야 한다.

@Configuration과 싱글톤

@Configuration
public class AppConfig {

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

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

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

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

AppConfig 코드에서 memberService와 orderService 빈을 만드는 코드를 보면 memberRepository()를 호출하고 new MemoryMemberRepository()를 호출한다.
결과적으로 각각 다른 2개의 MemoryMemberRepository가 생성되면서 싱글톤이 깨지는 것처럼 보인다.
하지만 테스트를 해보면 그렇지 않다. 스프링 컨테이너는 이것을 하나의 객체로 유지되게 해준다.

스프링은 클래스의 바이트 코드를 조작하는 라이브러리를 사용해서 스프링 빈이 싱글톤이 되도록 보장해준다.
비밀은 @Configuration을 적용한 AppConfig에 있다.

AppConfig bean = ac.getBean(AppConfig.class);
 
 System.out.println("bean = " + bean.getClass())

AppConfig 스프링 빈을 조회해서 클래스 정보를 출력해보면
class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70 가 나온다.

그림처럼 @Configuration을 적용한 AppConfig 클래스는 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다.

그 임의의 다른 클래스가 @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드를 동적으로 만들어줘서 싱글톤이 보장되도록 해준다.

@Configuration을 적용하지 않고 @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지는 않는다.

0개의 댓글