[Spring] 스프링 컨테이너와 스프링 빈

olsohee·2023년 3월 20일
0

Spring

목록 보기
1/12

1. 관련 개념

1-1. 제어의 역전 (IoC : Inversion of Control)

  • 프로그램의 흐름(객체의 생명주기)을 개발자가 직접 제어하는 것이 아닌, 외부에서 관리하는 것을 말한다.

  • 스프링은 제어의 역전을 지원하여, 개발자 대신 스프링 컨테이너가 객체(Bean)들을 관리해준다.

  • 스프링 컨테이너는 Bean들의 생명주기를 관리하며, 필요에 따라 객체 간 의존성 주입을 해준다.

  • 제어의 역전은 코드의 재사용성과 유지보수성을 높인다.

1-2. 의존관계 주입 (DI : Dependency Injection)

  • 의존관계는 정적인 클래스 의존관계와 동적인 객체(인스턴스) 의존관계로 분류된다.

    • 정적인 클래스 의존관계
      : 애플리케이션을 실행하지 않아도 알 수 있다.
      ex, OrderServiceImplMemberRepository(인터페이스)와 DiscountPolicy(인터페이스)에 의존한다. 그러나 실제 어떤 구현체가 OrderServiceImpl에 주입될지는 알 수 없다.

    • 동적인 객체(인스턴스) 의존관계
      : 애플리케이션 실행 시점에 실제 생성된 객체와의 의존관계이다.
      ex, OrderServiceImplMemoryMemberRepository(구현체), FixDiscountPolicy(구현체)와의 의존성이 주입된다.

  • 의존관계 주입은 애플리케이션 실행 시점에 객체 간 의존성을 개발자가 아닌 외부에서 주입해주는 것을 말한다.

  • 스프링에서는 스프링 컨테이너가 Bean 설정 정보를 바탕으로 각 클래스의 의존관계를 연결해준다.

  • 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상을 변경할 수 있다. 즉 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 변경할 수 있다.

1-3. IoC 컨테이너, DI 컨테이너

  • 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다.

2. 스프링 컨테이너

2-1. 스프링 컨테이너

  • BeanFactoryApplicationContext를 스프링 컨테이너라 한다.

  • BeanFactory

    • 스프링 컨테이너의 최상위 인터페이스이다.

    • 스프링 빈을 관리하고 조회하는 기능을 제공한다. (ex, getBean())

  • ApplicationContext

    • BeanFactory의 기능을 모두 상속받아서 제공한다.

    • 빈을 관리하고 조회하는 기능 외에도 다음과 같은 부가 기능을 제공한다.

      • 메시지소스를 활용한 국제화 기능
        : 예를 들어 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어권으로
      • 환경 변수
        : 로컬, 개발, 운영 등을 구분해서 처리
      • 애플리케이션 이벤트
        : 이벤트를 발행하고 구독하는 모델을 편리하게 지원
      • 편리한 리소스 조회
        : 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회
  • ApplicationContextBeanFactory 기능을 모두 상속하여 Bean 객체를 관리, 조회하는 기능 뿐만 아니라, 메시지, 리소스, 이벤트와 관련된 부가 기능까지 제공한다.

  • 애플리케이션을 개발할 때는 빈 관리, 조회 기능 외에도 수많은 부가 기능이 필요하기 때문에 ApplicationContext를 주로 사용한다.

2-2. 스프링 컨테이너의 다양한 설정 형식 지원

  • 스프링 컨테이너는 다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되어 있다.

  • 스프링 컨테이너는 XML을 기반으로 만들 수도 있고, 애노테이션 기반의 자바 설정 클래스로 만들 수도 있다.

2-3. BeanDefinition

스프링이 이렇게 다양한 설정 형식을 지원할 수 있는 이유는 바로 BeanDefinition (빈 설정 메타정보) 덕분이다.

  • AnnotationConfigApplicationContext
    AnnotatedBeanDefinitionReader를 사용해서 AppConfig.class를 읽고
    @Bean당 하나씩 메타정보 BeanDefinition을 생성한다.

  • GenericXmlApplicationContext
    XmlBeanDefinitionReader를 사용해서 appConfig.xml 설정 정보를 읽고
    <bean>당 하나씩 메타정보 BeanDefinition을 생성한다.

  • 즉 스프링 컨테이너는 자바 코드인지, xml 코드인지 몰라도 된다. 스프링 컨테이너는 오직 BeanDefinition만 알면 되고, BeanDefinition을 기반으로 스프링 빈을 생성한다.


3. 싱글톤 컨테이너

3-1. 싱글톤 패턴

  • 싱글톤 패턴은 클래스의 인스턴스가 딱 한 개만 생성되는 것을 보장하는 디자인 패턴이다.
public class SingletonService {
	
    //static으로 객체를 1개만 생성해둔다.
    private static final SingletonService instance = new SingletonService();
    
    //객체 인스턴스가 필요하면 이 static 메소드를 통해서만 조회할 수 있다.
    public static SingletonService getInstance() {
    	return instance;
    }
    
    //생성자를 private으로 선언해서 외부에서 new 키워드로 객체 생성을 못하도록 막는다.
    private SingletonService() {
    }
}
  • 이렇게 싱글톤 패턴으로 구현하면 고객의 요청이 올 때마다 객체를 새로 생성하지 않고, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다.

  • 그러나 싱글톤 패턴은 다음과 같은 문제를 갖는다.

    • 싱글톤 패턴을 구현하는 코드가 많이 들어간다.

    • 의존관계상 클라이언트가 구체 클래스에 의존한다. (DIP 위반)

    • private 생성자를 갖고 있어 상속이 불가능하다.

    • 테스트하기 어렵다.

3-2. 싱글톤 컨테이너

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

  • 스프링 컨테이너는 싱글톤 컨테이너의 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.

  • 스프링 컨테이너 덕분에 싱글톤 패턴을 위한 코드가 들어가지 않으며, DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.

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

3-3. 싱글톤과 무상태

  • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.

  • 무상태(stateless)로 설계해야 한다.

    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안되고 가급적 읽기만 가능해야 한다.

    • 공유 값을 설정하면 위험하다. 그 대신에 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.

  • 상태를 유지하는 경우는 다음과 같다.

    public class StatefulService {
    
    	private int price; //상태를 유지하는 필드
       
       public void order(String name, int price) {
       	this.price = price;
       }
       
       public int getPrice() {
       	return price;
       }
    }
    public class StatefulServiceTest {
    
    	@Test
       void StatefulServiceSingleton() {
       	ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
           StatefulService service1 = ac.getBean("statefulService", StatefulService.class);
           StatefulService service2 = ac.getBean("statefulService", StatefulService.class);
           
           service1.order("userA", 1000);
           service2.order("userB", 2000);
           
           int price = service1.getPrice(); //사용자 A는 1000원을 기대했지만, 2000이 반환된다.
       }
       
       static class TestConfig {    
       	@Bean
           public StatefulService StatefulService() {
           	return new StatefulService();
           }
       }
    }
  • 위 코드를 무상태로 설계하면 다음과 같다.

    public class StatefulService {
       
       public int order(String name, int price) {
       	return price;
       }
    }
    public class StatefulServiceTest {
    
    	@Test
       void StatefulServiceSingleton() {
       	ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
           StatefulService service1 = ac.getBean("statefulService", StatefulService.class);
           StatefulService service2 = ac.getBean("statefulService", StatefulService.class);
           
           int userAPrice = service1.order("userA", 1000);
           int userBPrice = service2.order("userB", 2000);
       }
       
       static class TestConfig {    
       	@Bean
           public StatefulService StatefulService() {
           	return new StatefulService();
           }
       }
    }

4. @Configuration, @Bean

@Configuration

  • @Configuration이 달린 클래스는 빈 설정을 담당하는 설정 정보 클래스가 된다.
    스프링 컨테이너는 @Configuration이 붙은 클래스를 설정 정보로 사용한다.

@Bean

  • @Configuration이 달린 클래스 내에서, 메소드에 @Bean을 적용하면 메소드가 반환하는 객체가 스프링 빈으로 등록된다.

@Configuration과 바이트코드 조작

@Configuration
public class AppConfig {

	public MemberService memberService() {
    	return new MemberServiceImpl(memberRepository());
    }
   
    public OrderService orderService() {
    	return new OrderServiceImpl(memberRespository(), discountPolicy());
    }

	public MemberRepository memberRespository() {
    	return new MemoryMemberRepository();
    }
   
    public DiscountPolicy discountPolicy() {
    	return new FixDiscountPolicy();
    }
}
  • AppConfig의 코드를 보면, memberRespository()가 총 3번 호출되어야 한다.
    그런데 스프링 컨테이너는 싱글톤을 보장하기 때문에 memberRespository()는 딱 한 번만 호출되고, 이때 생성된 MemoryMemberRepository의 객체가 공유되어 사용된다.
  • 이것에 가능한 이유는 AppConfig에 @Configuration을 적용했기 때문이다.
    @Configuration 덕분에 객체를 싱글톤으로 관리할 수 있다.
  • AppConfig에 @Configuration을 붙이면, AppConfig도 스프링 빈에 등록된다. 그리고 등록된 AppConfig를 조회해보면 hello.core.AppConfig가 아닌 클래스 명에 xxxCGLIB가 붙은 것을 알 수 있다.
@Test
void configurationDeep() {
	ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
   
    AppConfig bean = ac.getBean(AppConfig.class);
   
    System.out.println("bean = " = bean.getClass());
}
출력 결과
bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
  • 스프링은 CGLIB라는 바이트코드 조작 라이브러리를 사용해서, AppConfig를 상속받는 임의의 다른 클래스를 생성하고, 그 클래스를 스프링 빈으로 등록한다. 그리고 그 임의의 클래스가 싱클톤을 보장해준다.
AppConfig@CGLIB 예상 코드
@Bean
public MemberRepository memberRepository() {

	if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
		return 스프링 컨테이너에서 찾아서 반환;
	} else { //스프링 컨테이너에 없으면
		기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
		return 반환
	}
}
  • @Bean이 붙은 메소드마다 이미 스프링 빈이 존재하면 해당 빈을 반환하고, 스프링 빈이 없으면 생성해서 빈으로 등록하는 코드가 만들어진다. 이 임의의 클래스 덕분에 싱글톤이 보장된다.
  • AppConfig@CGLIBAppConfig의 자식 타입이기 때문에, AppConfig 타입으로 조회할 수 있는 것이다.

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

  • AppConfig에 @Configuration를 삭제하고 @Bean만 적용하면 어떻게 될까?
  • 스프링 빈으로 등록된 AppConfig의 클래스 정보를 조회해보면, AppConfig의 클래스 정보는 hello.core.AppConfig이다. 즉 CGLIB 기술 없이 순수한 AppConfig로 스프링 빈에 등록된다.
  • 따라서 memberRepository()도 3번 호출되고, 3개의 MemoryMemberRepository 객체가 만들어진다. 즉 싱글톤이 보장되지 않는다.
  • 결론적으로 @Bean만 적용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다. 따라서 스프링 설정 정보에는 항상 @Configuration을 적용하자!
profile
공부한 것들을 기록합니다.

0개의 댓글