스프링 컨테이너 그리고 싱글톤

Dev_owl ·2023년 8월 9일

Spring

목록 보기
4/4
post-thumbnail

BeanFactory

  • 빈을 생성하고 의존관계를 설정하는 기능을 담당하는 가장 기본적인 IoC 컨테이너이자 클래스
  • 빈을 생성하고 관계를 설정하는 IoC 기본 기능에 초점
  • 스프링 컨테이너에 접근하기 위한 최상위 인터페이스이다.

Application Context

ApplicationContext applicationContext = 
										new AnnotationConfigApplicationContext(AppConfig.class);
  • BeanFactory의 구현체, 사실상 확장 버전
  • 스프링 컨테이너XML을 기반으로 만들 수 있고, 애노테이션 기반의자바 설정 클래스 AppConfig 로도 만들 수 있다.
  • 더 정확히는 스프링 컨테이너를 부를 때 BeanFactoryApplicationContext 로 구분한다.
    • BeanFactory 를 직접 사용하는 경우는 거의 없다.
    • 일반적으로 ApplicationContext 를 스프링 컨테이너라한다.
  • 별도의 정보를 참고해서 빈의 생성, 관계 설정 등의 제어를 총괄하는 것에 초점을 맞춤
  • 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성)정보로 사용한다.
  • @Bean이 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.
    • 스프링 빈 = 스프링 컨테이너에 등록된 객체
  • 스프링 빈은 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다.

Application Context의 두가지 종류

  • root-applicationContext
    • Service, Repository를 스캔하고 DB 관련 객체를 생성 → ContextLoaderListener에 의해 실행
  • servlet-applicationContext
    • Controller, RestConroller를 스캔한다. → DispatcherServlet에 의해 설정
  • 스프링 부트에서 컴포넌트 스캔을 할 때 src 이하의 폴더를 전부 스캔한다.
  • 어떤 객체가 필요한지 안필요한지는 어노테이션을 통해 알아차린다.
  1. DB커넥션과 관련된 컴포넌트 등 공통으로 써야하는 컴포넌트는 ContextLoaderLisener를 통해서 미리 업데이트 (root_applicationContext 파일)
  2. 웹 요청마다 서블릿은 스레드를 만들어 컨테이너에 띄어준다.

ApplicationContext의 추가 기능

Environment

  • 프로파일을 설정, 어떤 것을 사용할지 선택할 수 있게함
  • 소스 설정 및 프로퍼티 값을 가져온다.

MessageSource

  • 메시지에 대한 국제화 (i18n)을 제공하는 인터페이스
  • 메시지 설정 파일을 모아 국가마다 로컬라이징
i18n
  • 국제화의 약칭
  • 소프트웨어가 언어에 종속적이지 않고 한국어든, 영어든 동시에 입력해서 사용할 수 있어야하는 요구사항을 만족시킴

ApplicationEventPublisher

  • 애플리케이션 이벤트를 이용하여 이벤트를 발행, 구독하는 모델을 편리하게 지원

ResourceLoader를 이용하여 편리하게 파일, 클래스패스 등의 리소스를 조회한다.

빈을 생성한 후 로딩하는 방식의 차이

Lazy-Loading : 빈 팩토리 방식

  • 메서드나 클래스가 빈 로딩 요청을 받는 시점에 인스턴스를 만들고 로딩한다.
  • 자주 사용되지 않는 빈이라면 해당 방식이 낫다.

Pre-Loading : Application Context 방식

  • 모든 빈들과 설정 파일들이 Application Context에 의해 로드 요청이 될 때 인스턴스로 만들어지고 로드된다.

공식 문서는 Application Context 사용을 권장한다.

  • 문제가 있는 Bean 객체가 있을 때 Lazy-Loading을 사용할 경우 Bean이 사용되는 시점이 되기 전까지 해당 오류의 유무를 파악하기 어렵다.
  • Eager Loading을 사용하여 객체의 초기화 시점에 오류를 잡아내면 좋다.

Lazy-Loading을 사용하는 시점

  • 통합 테스트 진행시 Eager Loading을 사용하게 될 경우 빈이 한번에 컨테이너에 올라가기 때문에 실행시간이 많이 소요된다.
  • 필요한 부분에 별도로 Lazy-Loading을 설정함으로써 개선할 수 있다.

생성 과정 (싱글톤 레지스트리)

  • 별다른 설정을하지 않을 경우 내부에서 생성하는 빈은 전부 싱글톤임

1. 스프링 컨테이너 생성

ApplicationContext applicationContext = 
										new AnnotationConfigApplicationContext(AppConfig.class);
  • 스프링 컨테이너를 생성할 때는 구성 정보를 지정해야한다.
  • 예제에서는 AppConfig.class 를 구성정보로 지정했다.

2. 스프링 빈 등록

  • 스프링 컨테이너는 @Bean 어노테이션을 참조하여 스프링 빈을 등록한다.
  • 빈 이름
    • 빈 이름은 기본적으로 메서드 이름을 사용한다.
    • 빈 이름을 직접 부여할 수도 있다. @Bean(name="원하는이름")
    • 빈 이름은 항상 다른 이름을 부여해야한다.
    • 같은 빈 이름을 부여할 경우, 다른 빈이 무시되거나, 기존 빈을 덮어버리거나, 설정에 따라 오류가 발생한다.

3. 스프링 빈 의존관계 설정 - 준비

  • 스프링 빈 객체가 생성된다.

4. 스프링 빈 의존관계 설정 - 완료

  • 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입한다.
  • 단순히 자바코드를 호출하는 것이 아니다. (뒤 싱글톤에서 설명)

싱글톤 컨테이너

웹 애플리케이션과 싱글톤

  • 스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다.
    • 대부분의 스프링 앱은 웹이다.
  • 웹 애플리케이션은 보통 여러 고객이 동시에 요청한다.
    • 싱글톤이 없다면 고객이 올때마다 요청해야한다.
  • 스프링이 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때마다 객체를 새로 생성한다.
  • 트래픽이 초당 100이 나오면 초당 100개의 객체를 생성하고 소멸 ⇒ 메모리 낭비가 심하다.
  • 싱글톤 패턴
    • 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하자

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴
  • 인스턴스를 2개 이상 생성하지 못하도록 막아야한다.
    • private 생성자를 이용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.

예시

public class SingletonService {

    private static final SingletonService instance = new SingletonService();

    public static SingletonService getInstance(){
        return instance;
    }

    public void logic(){
        System.out.println("싱글톤 객체 로직 호출");
    }

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

실제 사용

@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
public void singletonServiceTest() throws Exception{
    SingletonService singletonService1 = SingletonService.getInstance();
    SingletonService singletonService2 = SingletonService.getInstance();
    //같은 객체 인스턴스를 출력한다. 
    System.out.println(singletonService1);
    System.out.println(singletonService2);

}
  • 호출할 때마다 같은 객체 인스턴스를 반환한다.

싱글톤 패턴의 문제

  • 싱글톤 패턴을 구현하는 코드의 장황함
  • 의존관계상 클라이언트가 구현체에 의존함 ⇒DIP를 위반한다.
  • 테스트하기가 어렵다.
    - 만들어지는 방식이 제한적이라 Mock객체로 대체하기가 힘들다.
    • 초기화 과정에서 객체를 동적으로 주입하기도 힘들다.
    • 필요한 객체는 직접 만들어 사용할 수 밖에 없다.
  • 내부 속성을 변경하거나 초기화하기 어려움
  • private때문에 자식 클래스 만들기가 어렵다.
    - 상속과 이를 이용한 다형성을 적용할 수 없다.
  • static으로 인해 전역상태를 갖고 이는 불안정함
  • 정작 분산환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
    - 서버에서 클래스 로더를 어떻게 구성하고 있느냐에 따라 하나 이상의 오브젝트가 만들어질 수 있음

싱글톤 컨테이너의 등장 (=싱글톤 레지스트리)

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

싱글톤 컨테이너

  • 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
    • 컨테이너 생성과정을 자세히 보면, 컨테이너는 객체를 하나만 생성해서 관리한다.
  • 스프링 컨테이너는 싱글톤 컨테이너 역할은 한다.
    • 싱글톤 레지스트리 : 싱글톤 객체를 생성하고 관리하는 기능
  • 싱글톤 패턴의 단점을 해결
    • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
    • DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.
@Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    public void springContainer() throws Exception{
//        AppConfig appConfig = new AppConfig();
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberService memberService1= ac.getBean("memberService", MemberService.class);

        MemberService memberService2 = ac.getBean("memberService", MemberService.class);

        System.out.println("MemberService1 = "+memberService1);
        System.out.println("MemberService2 = "+memberService2);

    }
  • 고객의 요청이 들어올 때마다 객체를 생성하는 것이 아닌, ⇒ 이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있다.

스프링의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아니다.

⇒ 요청할 때 마다 새로운 객체를 생성하여 반환하는 기능 또한 있다.

싱글톤 방식의 주의점

  • 여러 클라이언트가 하나의 같은 객체를 공유
  • 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.
  • 무상태(stateless)로 설계해야한다!
    • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
    • 가급적 읽기만 가능해야한다.
    • 필드 대신에 자바에서 공유되지 않는 지역변수 , 파라미터 , ThreadLocal 등을 사용해야한다.
  • 스프링 빈 필드에 공유값을 설정하면 절대 안된다.

상태를 유지할 경우 발생하는 문제점 예시

public class StatefulService {
    private int price;

    public void order(String name, int price){
        System.out.println("name= "+ name+" price = "+ price);

        this.price = price;
    }

    public int getPrice(){
        return price;
    }
}
@Test
public void statefulServiceSingleton() throws Exception{

    ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
    StatefulService statefulService1 = ac.getBean(StatefulService.class);
    StatefulService statefulService2 = ac.getBean(StatefulService.class);

    **//A가 10000원 주문하고 있는 와중에 B가 20000으로 주문한다면 어떻게 될까??

    //스레드 1: A 사용자 10000원 주문
    statefulService1.order("A", 10000);

    //스레드 2: B 사용자 20000원 주문 
    statefulService2.order("B", 20000);

    int price = statefulService1.getPrice();
    //A의 주문 결과 확인 : 원래는 10000원을 주문해야하지만 20000원으로 갱신되어 있다.
    System.out.println(price);
}**

static class TestConfig{
    @Bean
    public StatefulService statefulService(){
        return new StatefulService();
    }
}

리팩토링

  • 지역변수와 파라미터 사용
public class StatefulService {
//    private int price;

    public int order(String name, int price){
        System.out.println("name= "+ name+" price = "+ price);

//        this.price = price;

        **return price;**
    }

}
@Test
public void statefulServiceSingleton() throws Exception{

    ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
    StatefulService statefulService1 = ac.getBean(StatefulService.class);
    StatefulService statefulService2 = ac.getBean(StatefulService.class);

    //A가 10000원 주문하고 있는 와중에 B가 20000으로 주문한다면 어떻게 될까??

    **//스레드 1: A 사용자 10000원 주문
    int priceA = statefulService1.order("A", 10000);

    //스레드 2: B 사용자 20000원 주문
    int priceB = statefulService2.order("B", 20000);

	   //정상적으로 10000원 출력됨 
    System.out.println(priceA);**
}

static class TestConfig{
    @Bean
    public StatefulService statefulService(){
        return new StatefulService();
    }
}

@Configuration은 싱글톤을 보장한다.

@Test
    public void configurationDeep() throws Exception{
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        AppConfig bean = ac.getBean(AppConfig.class);

        System.out.println(bean.getClass());
    }
  • 해당 코드의 예상 반환값은 class hello.core.AppConfig
  • 하지만 실제로는 class hello.core.AppConfig$$EnhancerBySpring**CGLIB**$$713455cd 를 출력

원리

  1. 스프링이 CGLIB 라는 바이트 조작 라이브러리를 사용
  2. AppConfig클래스를 상속받은 임의의 다른 클래스를 만든다.
  3. 그 클래스를 빈으로 등록한다.

AppConfig@CGLIB 예상코드

@Beanpublic MemberRepository memberRepository() {
		if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {

			return 스프링 컨테이너에서 찾아서 반환;

		} else{ 

			1. 스프링 컨테이너에 없으면기존 로직을 호출
			2. MemoryMemberRepository를 생성 
			3. 스프링 컨테이너에 등록

			return 반환
		}
}
  • @Bean 이 붙은 메서드마다
    1. 이미 스프링 빈이 존재하면 존재하는 빈을 반환

    2. 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어짐

      ⇒ 싱글톤이 보장된다.

AppConfig@CGLIBAppConfig 의 자식 타입이므로, AppConfig 타입으로 조회할 수 있다.

@Configuration 을 적용하지 않고, @Bean 만 적용하면 어떻게 될까??

@Bean
public MemberService memberService(){
    System.out.println("call AppConfig.memberService");
    return new MemberServiceImpl(memberRepository());
}

@Bean
public MemberRepository memberRepository() {
    System.out.println("call AppConfig.memberRepository");
    return new MemoryMemberRepository();
}

@Bean
public OrderService orderService(){
    System.out.println("call AppConfig.orderService");
    return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Test
public void configurationTest() throws Exception{
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
	

		**//@Bean 코드 내부에서 생성한다 -> 얘만 스프링 컨테이너가 관리한다.** 
    MemberRepository original = ac.getBean("memberRepository", MemberRepository.class);	
		
		**//나머지는 자바 코드 흐름에 따라 생성되기 때문에 스프링 컨테이너가 관리하지 않는다.** 
    MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
    OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);

    MemberRepository memberRepository = memberService.getMemberRepository();
    MemberRepository memberRepository1 = orderService.getMemberRepository();

    System.out.println("memberService -> memberRepository"+ memberRepository);
    System.out.println("orderService -> memberRepository"+memberRepository1);
    System.out.println(original);
}

Untitled

  • 중복된 명칭의 인스턴스가 전부 다르게 생성됨
  • 스프링 컨테이너가 관리하는 객체가 서로 다르다.(스프링 컨테이너가 관리하지 않을 수도 있다.)

정리

  • @Bean 만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
    • new MemberServiceImpl(memberRepository());처럼 의존관계 주입이 필요해서 직접 호출할 때 싱글톤 보장 x
  • 스프링 설정 정보는 항상 @Configuration 을 사용하자

0개의 댓글