스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해줘야 한다.

그런데 스프링이 자바 코드까지 어떻게 하기는 어렵다.

AppConfig의  코드를 보면 memberService는 분명 3번 호출되어야 하는 것이 맞다...

 

 

 

 

어떻게 스프링은 싱글톤을 보장해줄 수 있을까? 

 

 

 

 

 

 

이 모든 비밀은 @Configuration에 있다. 

 

 

먼저, AppConfig.class 타입의 빈을 조회해보자 

@Test
void configurationDeep(){
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    // AppConfig도 빈으로 등록된다.
    AppConfig bean = ac.getBean(AppConfig.class);

    System.out.println("bean = " + bean.getClass());
}

 

//실행결과
bean = class hello.core.AppConfig$$SpringCGLIB$$0

 

 

  • @Configuration 으로 등록한 AppConfig 클래스도 빈으로 등록된다. 
  • 그렇다면 bean.getClass( ) 의 결과는 class hello.core.AppConfig 일 것이다. 
  • 하지만 예상과는 다르게 클래스명에 xxxCGLIB가 붙으면서 상당히 복잡해진 것을 볼 수 있다.

 

 

그 이유는 바로, 우리가 만든 AppConfig 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서

AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다 

 

 

                               스프링이 빈을 등록하는 과정에서 조작을 해 다른 것을 빈으로 넣어버렸다 !! 

 

 

 

그렇다면 스프링은 왜 이런 짓을 했을까? 

 

 

 

 

 

참고:  AppConfig@CGLIB는 AppConfig의 자식 타입이므로, AppConfig 타입으로 조회 할 수 있다

 

 

스프링이 우리가 등록하고자 한 AppConfig 대신 AppConfig를 상속받은 임의의 클래스를 빈으로 등록시킨 이유는

싱글톤을 보장해주기 위해서다. 

 

기존의 우리가 작성한 순수 자바코드로 구성된 AppConfig 자체로 스프링 컨테이너를 구성한다면 

memberService가 여러번 호출되어 서로 다른 객체가 여러개 생성될 것이고 이는 싱글톤이 깨지게 된다. 

 

 

그래서 스프링은 우리가 만든 클래스를 상속받고 조작을 통해  빈이 싱글톤으로 관리될 수 있도록 하는 것이다.

 

아마도 다음과 같이 바이트 코드를 조작해서 작성되어 있을 것이다.(실제로는 CGLIB의 내부 기술을 사용하는데 매우 복잡하다.)

 

AppConfig@CGLIB 예상 코드

@Bean
 public MemberRepository memberRepository() {
        if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
               return 스프링 컨테이너에서 찾아서 반환;
         } 
         else { //스프링 컨테이너에 없으면
                  기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
               return 반환
}

 

  • @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고
  • 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.
  • 이렇게 해서 싱글톤을 보장한다. 

 

그래서 앞의 글 에서 memberRepository를 여러번 호출했더라도 

첫 호출로 빈을 등록한 이후에는 스프링 컨테이너에서 찾아서  만들어진 빈을 반환해줬기 때문에 여러번 메서드를 호출했더라도 

모두 같은 객체가 조회됐던 것이다. 

 

 

✅ 위의 내용들을 간단하게 정리해본다면, 스프링은 우리가 @Configuration을 써서 스프링의 설계도로 지정한 클래스를 그대로 사용하는 것이 아닌 , 싱글톤을 보장해주기 위해 우리가 만든 클래스를 상속받은 임의의 클래스를 사용한다. 

 

 

 

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

 

  •  @Configuration을 통해 스프링은 해당 클래스가 설정클래스라는 것을 아는데,  이것이 없다면 @Bean이 있더라도 빈 등록이 안될까? 
  • @Configuration 이 없다면, 우리가 작성한 순수 클래스가 스프링의 설정 클래스로 이용될까? 

 

 

 

 

 

이를 확인하기 위해 AppConfig 클래스의 @Configuration 어노테이션을 주석처리하고 테스트 코드를 다시 실행시켜보자 

 

@Configuration 주석처리

 

// 테스트 실행결과

call AppConfig.memberService
call AppConfig.memberRepository
18:37:52.832 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory --
                Creating shared instance of singleton bean 'memberRepository'
call AppConfig.memberRepository
18:37:52.832 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory --
                Creating shared instance of singleton bean 'orderService'
call AppConfig.orderService
call AppConfig.memberRepository
18:37:52.835 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory --
                Creating shared instance of singleton bean 'discountPolicy'
bean = class hello.core.AppConfig

 

bean = class hello.core.AppConfig

 

출력결과 우리가 만든 순수한  AppConfig 클래스가 출력되었다.  

@Configuration 이 없다면 우리가 작성한 순수 Appconfig클래스가 스프링 컨테이너의 설정클래스가 된다. 

 

또한 @Configuration 이 없더라도 AppConfig의  @Bean이 모두 빈으로 등록된다. 

// 설정된 빈 이름 모두 출력하기
for (String beanDefinitionName : ac.getBeanDefinitionNames()) {
    System.out.println("beanDefinitionName = " + beanDefinitionName);
}

 

//실행결과 
beanDefinitionName = appConfig
beanDefinitionName = memberService
beanDefinitionName = memberRepository
beanDefinitionName = orderService
beanDefinitionName = discountPolicy

 

 

그렇다면 싱글톤 문제는 어떨까? memberRepository 메서드가 여러번 호출 될텐데 이전과  무슨 차이가 있을까?  

 

call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository

 

실행결과, @Configuration 어노테이션이 있을때 와는 다르게 call AppConfig.memberRepository 가 세 번 호출 되었다. 

1번은 스프링 컨테이너에 @Bean을 등록하기 위해서, 2번은 각각 memberRepository( ) 를 호출했기 때문이다. 

 

즉,  싱글톤이 깨진다. 

 

 

 

  • @Configuration 이 없는 경우 memberService를 호출할 때마다 새로운 객체가 생성되는지 테스트 코드의 실행결과를 통해 알아보자
@Test
@DisplayName("@Configuraion과 싱글톤")
void ConfiguraionSingletonTest(){

    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
    OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
    MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);


    System.out.println("memberService -> memberRepository" + memberService.getMemberRepository());
    System.out.println("orderService -> memberRepository" + orderService.getMemberRepository());
    System.out.println("memberRepository = " + memberRepository);

    Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
    Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);


}
//실행결과
memberService -> memberRepositoryhello.core.member.MemoryMemberRepository@52066604
orderService -> memberRepositoryhello.core.member.MemoryMemberRepository@340b9973
memberRepository = hello.core.member.MemoryMemberRepository@56113384

 

테스트 코드 실행결과, 각각의 memberRepository는 모두 다른 객체임을 알 수 있다. 

 

또 다른 문제점은 memberService와 orderService의 memberRepository는 스프링이 관리하는 빈이 아니라는 것이다. 

 

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

 

@Bean으로 등록된 memberRepository는 스프링 빈으로 등록되지만

memberService와 orderService가 호출하는 memberRepository( )는 new MemoryMemberRepository로 새로운 객체를 생성해주는 것이므로 스프링 빈이 아니다. 

 

 

 

 

정리 

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

 

 

 

 

 

 

 

스프링 컨테이너에서 빈을 싱글톤으로 등록하는 방법


AppConfig

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService(){

        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public static MemoryMemberRepository memberRepository() {

        return new MemoryMemberRepository();
    }
    @Bean
    public OrderService orderService(){

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

//        return new FixDiscountPolicy();
        return  new RateDiscountPolicy();
    }
}

 

 

AppConfig 기반의 스프링 컨테이너를 생성한다면  @Bean으로 등록된 메서드들을 한번씩 호출해서 빈으로 등록시킬 것이다. 

 

그런데 코드를 자세히 보면 memberService() 는 여러번 호출 된다. 

 

  • memberService 빈을 만드는 코드를 보면 memberRepository() 를 호출한다.
     이 메서드를 호출하면  new MemoryMemberRepository()를 호출한다.
  • memberRepository 빈을 만들기 위해 memberRepository( ) 를 호출한다.
  • orderService 빈을 만드는 코드도 동일하게 memberRepository()  를 호출한다.
     이 메서드를 호출하면 new MemoryMemberRepository() 를 호출한다. 

 

결과적으로 3번의 new MemoryMemberRepository를 통해 서로 다른 memberRepository 객체가 생성되는것 아닐까? 

하지만 스프링은 객체를 싱글톤으로 관리해준다고 했는데....

 

어떻게 여러번 메서드를 호출했는데 객체는 1개 일 수 있을까? 

 

 

 

 

스프링은 이 문제를 어떻게 해결해주는지 알아보자

 

 

 


 

 

 

 

 

먼저 테스트 코드를 통해 memberService 을 만들었을 때 생성된 memberRepository와 

orderService를 통해 만들어진 memberRepository 와 빈으로 등록된 memberRepository 가 모두 같은 객체인지 확인해보자 

 

 

ConfigurationSingletonTest

  • 테스트를 위해 MemberRepository를 조회할 수 있는 기능을 추가한다. 기능 검증을 위해 잠깐 사용하는 것이니 인터페이스에 조회하는 기능까지 추가하지는 말자
  • MemberServiceImpl 과 OrderServiceImpl 에 해당 클래스의 memberRepository 값을 알려주는 메서드를 추가해주자
public MemberRepository getMemberRepository() {
    return memberRepository;
}

 

public class ConfigurationSingletoneTest {

    @Test
    void ConfiguraionSingletonTest(){

        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        
        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);


        System.out.println("memberService -> memberRepository" + memberService.getMemberRepository());
        System.out.println("orderService -> memberRepository" + orderService.getMemberRepository());
        System.out.println("memberRepository = " + memberRepository);

        Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);


    }

}
//실행결과

memberService -> memberRepositoryhello.core.member.MemoryMemberRepository@45efc20d
orderService -> memberRepositoryhello.core.member.MemoryMemberRepository@45efc20d
memberRepository = hello.core.member.MemoryMemberRepository@45efc20d

 

실행결과 3개의 memberRepository는 모두 같은 객체임을 알 수 있다. 

스프링은 객체를 싱글톤으로 관리한다고 했으므로 당연한 결과일 수 있다. 

 

 

하지만 AppConfig의 코드를 보면  new MemoryMemberRepository를 하니까 호출 할 때마다 새로운 인스턴스가 생성되어야 하는게 아닐까? 

 

어떻게 이런일이 가능할까? 

 

 

AppConfig에 호출 로그 남기기 

@Configuration
public class AppConfig {

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

    @Bean
    public  MemoryMemberRepository 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());
    }
    @Bean
    public DiscountPolicy discountPolicy(){

//        return new FixDiscountPolicy();
        return  new RateDiscountPolicy();
    }
}
  • 각각의 메서드가 몇 번 실행되는지 확인하기 위해 한번 실행될 때 마다 출력화면에 값이 나오도록 호출코드를 추가해주자

 

 

 

ConfigurationSingletonTest를 실행 시킨 결과를 예상해보자면 

 

먼저, 스프링 컨테이너가 각각 @Bean을 호출해서 스프링 빈을 생성한다. 

  • 스프링 컨테이너가 스프링 빈에 등록하기 위해 @Bean이 붙어있는memberRepository() 호출 
  • memberService() 로직에서 memberRepository() 호출 
  • orderService() 로직에서 memberRepository() 호출 
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository

 

물론 순서는 일치하지 않을 수 있지만, "call AppConfig.memberRepository" 가 세번 호출 되지 않을까? 

 

 

 

//실행결과

call AppConfig.memberService
16:47:35.516 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory --
                Creating shared instance of singleton bean 'memberRepository'
call AppConfig.memberRepository
16:47:35.518 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory --
                Creating shared instance of singleton bean 'orderService'
call AppConfig.orderService
16:47:35.520 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory --

 

call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService

 

 

 

실행결과 각각의 메서드들은 모두 한번씩만 호출되었다. 

 

 

어떻게 이런일이 가능할까?

 

코드를 보면  호출을 세 번하는데 왜  실제로는 한번만 호출될 수 있었던 걸까? 

 

 

이와 관련한 내용은 다음 글에서 정리해보자 

 

 

 

싱글톤 방식의 주의점 

 

 

싱글톤 방식 주의점 

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

 

상태를 무상태로 설계한다는 것은 , 쉽게 말해 상태를 가지지 못하도록 설계한다. 즉 값을 변경할 수 없도록 설계한다는 의미이다. 

싱글톤을 설계할 때 이를 구현하는 방법은 크게 Stateful한 설계 방식과 Stateless 설계 방식으로 나눌 수 있다. stateful 싱글톤은 변경 가능한 상태를 가진 싱글톤이고, stateless 싱글톤은 변경 가능한 상태가 없는 싱글톤을 말한다.

 

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

 

스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다!!!

 

코드를 통해서 싱글톤을 상태로 설계했을 때의 문제점을 알아보자. 

 

 

 

 

StatefulService

package hello.core.singletone;

public class StatefulService {
    private int price; // 값이 변경 가능한 필드 (stateful 필드)

    public void order(String name,int price){
        System.out.println("name = " + name+", price = "+price);
        this.price=price; // 여기가 문제!
    }

    public int getPrice() {
        
        return price;
    }
}

 

 

StatefulServiceTest

class StatefulServiceTest {

    @Test
    public void statefulServiceSingletone(){

        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        // ThreadA: 사용자A 10000원 주문
        statefulService1.order("userA",10000);
        // ThreadB: 사용자B 20000원 주문
        statefulService2.order("userB",20000);

        //ThreadA: 사용자A 주문 금액 조회 -> 10000원 예상
        int price = statefulService1.getPrice();
        System.out.println("price = " + price);

        // 하지만 실제값은 20000원으로 조회됨
        assertThat(statefulService1.getPrice()).isEqualTo(20000);

        // 왜? satefulService1 == statefulService2
        assertThat(statefulService1).isEqualTo(statefulService2);

    }
    static class TestConfig{

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

 

 

 

  • StatefulService의 인스턴스를 싱글톤으로 관리하기 위해 TestConfig를 이용하여 스프링 빈으로 등록해주자 .

 

테스트 실행 결과 statefulService1.getprice( ) 를 했을 때 2만원이 조회되는 것을 알 수 있다. 

 

우리는 statefulService1의 price 필드를 만원으로 설정했는데 왜 2만원이 조회되었을까? 

그 이유는 바로 price 필드가 공유되는 필드였기 때문이다.

 

공유되는 필드인 price의 값을 어떤 사용자든 변경 할 수 있었기 때문에 statefulService2가 2만원으로 값을 설정하면서 이전 값을 뒤집어 씌우게 된것이다. 

 

예제코드를 통해서도 알 수 있듯이, 스프링 컨테이너는 객체를 하나만 생성해서 관리하기 때문에 

해당 인스턴스의 필드를 공유되는 필드, 즉 값을 변경할 수 있는 필드로 설정한다면 계속해서 값이 달라지게 될 것이다. 

 

따라서 스프링 빈은 항상 무상태로 설계하자!! 

 

 

 

 

이제 공유되는 필드를 무상태로 설계해보자

package hello.core.singletone;

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;
    }

    /*public int getPrice() {

        return price;
    }*/
}

 

 

 

 

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

지금까지 우리가 학습한 스프링 빈이 바로 싱글톤으로 관리되는 빈이다.

 

스프링 컨테이너는 객체를 1개만 생성해서 가지고 있는다

 

 

싱글톤 컨테이너 

  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
            -  이전에 설명한 컨테이너 생성 과정을 자세히 보자. 컨테이너는 객체를 하나만 생성해서 관리한다.
            -  요청이 들어오면 자신이 미리 생성해서 가지고 있었던 객체를 주기 때문에 응답 받은 객체는 모두 동일한 객체이다.
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을
  • 싱글톤 레지스트리 라고 한다.
  • 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
            -  싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
            -  DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다

 

 

 

 

 

 

 

SingletoneTest

  • 스프링 컨테이너가 객체를 싱글톤으로 관리하는지 코드를 통해 알아보자
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContaioner(){

    // 스프링 컨테이너 생성
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    // 호출1
    MemberService memberService1 = ac.getBean("memberService", MemberService.class);
   // 호출2
    MemberService memberService2 = ac.getBean("memberService", MemberService.class);


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


    assertThat(memberService1).isSameAs(memberService2);
}

 

//실행결과
memberService1 = hello.core.member.MemberServiceImpl@45efc20d
memberService2 = hello.core.member.MemberServiceImpl@45efc20d

 

 

 

 실행 결과 두 객체의 참조값은 동일한 것을 알 수 있다  →   스프링은 객체를 싱글톤으로 관리한다. 

 

 

싱글톤 컨테이너

 

                          

 

 

※ 참고

 

 

 

 

 

 

 

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴
  • 똑같은 객체 타입의 인스턴스가 2개 이상 생성하지 못하도록 막는 것이다.

How?

private 생성자를 사용해서 외부에서 임의로 new를 하지 못하도록 하는 것이다 

 

 

 

SingletoneService

public class SingletoneService {

    // 1. static 영역에 객체를 딱 1개만 설정해둔다
    private static final SingletoneService instance=new SingletoneService();

    // 2. public으로 열어서 객체 인스턴스가 필요한 경우 static 메서드를 통해서만 이용할 수 있도록 한다
    public static SingletoneService getInstance() {
        return instance;
    }
    //3. 생성자를 private으로 선언해서 외부에서 new를 해서 객체를 생성할 수 없도록 한다. 
    private SingletoneService(){}

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

 

 

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

 

이렇게 만들어본 SingletoneService가 제대로 동작하는지 테스트 코드를 작성해보자

 

SingletoneServiceTest

@Test
@DisplayName("싱글톤 패턴을 적용한 객체사용")
void singletoneServiceTest(){
    
    new SingletoneService();
}

 

//실행결과
SingletoneService() has private access in hello.core.singletone.SingletoneService

 

역시나 생성자를 private로 설정했기 때문에 외부에서  새로운 객체를 만드는 것은 불가능하다. 

 

 

다음으로  싱글톤 패턴을 적용하여 얻은 객체들은  모두 동일한 객체인지 확인해보자.

 

@Test
@DisplayName("싱글톤 패턴을 적용한 객체사용")
void singletoneServiceTest(){

    // 조회한 객체가 전부 동일한 객체인지 확인하기
    SingletoneService instance1 = SingletoneService.getInstance();
    SingletoneService instance2 = SingletoneService.getInstance();

    System.out.println("instance1 = " + instance1);
    System.out.println("instance2 = " + instance2);

    assertThat(instance1).isSameAs(instance2);
}

 

// 실행결과
instance1 = hello.core.singletone.SingletoneService@52e677af
instance2 = hello.core.singletone.SingletoneService@52e677af

 

테스트 실행결과 호출할 때 마다 같은 객체 인스턴스를 반환하는 것을 확인할 수 있다.  

 

따라서 우리는 싱글톤 패턴을 통해 순수 DI컨테이너의 문제점을 해결 할 수 있다는 것을 알았다.

 

이제 순수 DI 컨테이너인 AppConfig 클래스에 싱글톤 패턴을 적용시킨다면,  하나의 객체를 공유해서 사용할 수 있을 것이다. 

 

 

 

싱글톤 패턴의 문제점


 

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

 

하지만 싱글톤 패턴은 다음과 같은 수 많은 문제점들을 가지고 있다.

 

 

 

 

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

그렇기에 스프링 컨테이너는 싱글톤 컨테이너 라고도 불린다. 

 

 

 

 

다음 글에서는 스프링 컨테이너 = 싱글톤 컨테이너 에 대해서 더 자세히 알아보자

 

 


☑️ SingletoneServiceTest 전체코드

 

package hello.core.singletone;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class SingletoneTest {


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

        AppConfig appConfig= new AppConfig();
        // 1. 조회: 호출할 때 마다 객체 생성
        MemberService memberService1 = appConfig.memberService();
        // 2. 조회: 호출할 때 마다 객체 생성
        MemberService memberService2 = appConfig.memberService();

        // 참조값이 다른것을 확인
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        // memberService1 != memberService2
        assertThat(memberService1).isNotSameAs(memberService2);
    }

    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체사용")
    void singletoneServiceTest(){

        // 조회한 객체가 전부 동일한 객체인지 확인하기
        SingletoneService instance1 = SingletoneService.getInstance();
        SingletoneService instance2 = SingletoneService.getInstance();

        System.out.println("instance1 = " + instance1);
        System.out.println("instance2 = " + instance2);

        assertThat(instance1).isSameAs(instance2);
    }

}

 

 

 

 

우리는 스프링 애플리케이션을 이용해서 주로 웹 애플리케이션 개발을 한다. 

웹 애플리케이션의 경우 여러 고객이 동시에 요청을 한다. 

 

 

우리가 스프링을 이용하지 않는 순수한 자바 코드로만 만들어본 DI컨테이너를 이용한다면

   (스프링 컨테이너를 생성하지 않고 AppConfig 를 이용해서 객체를 생성하고 주입 받는 방법) 

 

public MemberService memberService(){

    return new MemberServiceImpl(memberRepository());
}

 

 

 

세명의 고객이 요청을 하면  객체도 3개 생성되는 것이다. 

 

만약 만명의 고객이 동시에 요청하면   새로운 객체를 만개를 만들어야 한다는 것인데 이것은 매우 비효율적이며 메모리의 낭비가 심할 것이다. 

 

 

즉, 스프링이 없는 순수한 DI컨테이너는  고객의 요청이 올때마다 새로운 객체를 만들어야한다는 문제점이 있다.

 

 

 

 

 

그렇다면 코드로 고객의 요청이 올때마다 새로운 객체가 생성되는지 알아보자

 

SingletoneTest 

  •   순수 DI 컨테이너는 요청이 올 때 마다 새로운 객체를 생성하는지 알아보는 테스트 코드
public class SingletoneTest {


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

        AppConfig appConfig= new AppConfig();
        // 1. 조회: 호출할 때 마다 객체 생성
        MemberService memberService1 = appConfig.memberService();
        // 2. 조회: 호출할 때 마다 객체 생성
        MemberService memberService2 = appConfig.memberService();

        // 참조값이 다른것을 확인
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        // memberService1 != memberService2
        assertThat(memberService1).isNotSameAs(memberService2);
    }

}

 

// 실행결과 

memberService1 = hello.core.member.MemberServiceImpl@2145433b
memberService2 = hello.core.member.MemberServiceImpl@2890c451

 

 

코드 실행 결과 요청을 받을 때 마다 새로운 객체를 생성한다는 것을 알 수 있다. 

 

그렇다면 요청이 들어올 때 마다 새로운 객체를 만들지 말고, 하나의 객체를 공유해서 사용할 순 없을까? 

이와 관련된 것이 바로 싱글톤 방식이다. 

 

다음 글에서 싱글톤에 대해 알아보자. 

 

 

정리

  • 우리가 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다.
  • 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸된다!    메모리 낭비가 심하다.
  • 해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다 → 싱글톤 패턴



 

 

지금까지 스프링 컨테이너의 설정을  어노테이션 기반의 자바 코드와 XML파일을 이용해서 해보았다. 

 

그런데 스프링은 어떻게 이렇게 다양한 설정 형식을 지원하는 것일까? 

 

 

 

스프링 컨테이너 설정 형식은 어노테이션 기반의 자바 코드도 되고, XML파일로 설정해도 되고, 사용자 임의로 만들어서 설정을 해줘도 된다. 

 

이것이 가능한 이유는 바로 BeanDefinition 이라는 추상화 때문이다. 

 

쉽게말해 역할과 구현을  분리 해줬기 때문에 설정 클래스를 바꿔끼워도 상관이 없었던 것이다. 

 

 

 

스프링 컨테이너는 빈 설정과 관련된 설정을  구체적인 설정 클래스들에 의존하고 있는 것이 아닌, BeanDefinition 이라는 인터페이스에 의존하고 있다. 

 

즉,  스프링 컨테이너는 설정 클래스가 자바 코드인지, XML인지 몰라도 된다. 오직 BeanDefinition만 알면 된다.

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

 

 

 

 

조금 더 자세하게 설명해보자면 

 

 

 

  • AnnotationConfigApplicationContext 는 AnnotatedBeanDefinitionReader 를 이용해 AppConfig.class 를 읽고  BeanDefinition을 생성한다. 
  • GenericXmlApplicationContext는 Xml BeanDefinitionReader를 이용하여 appConfig.xml을 읽고 BeanDefinition을 생성한다. 

 

 

beandefinitionTest 

  • 빈 설정 메타 정보 확인해보는 테스트 코드 작성
public class beanDefinitionTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    GenericXmlApplicationContext gc = new GenericXmlApplicationContext("appConfig.xml");

    @Test
    @DisplayName("자바 코드 기반의 빈 설정 메타정보 확인")
    void findBeanApplicationBean(){
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            // 각각의 빈들의 BeanDefinition 확인하기
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
            if(beanDefinition.getRole()== BeanDefinition.ROLE_APPLICATION){
                System.out.println("beanDefinitionName = " + beanDefinitionName+
                        "beanDefinition = "+beanDefinition);
            }

        }
    }

    @Test
    @DisplayName("XML 기반의 빈 설정 메타정보 확인")
    void findBeanApplicationBeanByXML(){
        String[] beanDefinitionNames = gc.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            // 각각의 빈들의 BeanDefinition 확인하기
            BeanDefinition beanDefinition = gc.getBeanDefinition(beanDefinitionName);
            if(beanDefinition.getRole()== BeanDefinition.ROLE_APPLICATION){
                System.out.println("beanDefinitionName = " + beanDefinitionName+
                        "beanDefinition = "+beanDefinition);
            }

        }
    }

}

 

※  참고로만 알아두기

 

 

정리

  • BeanDefinition 에 대해서는 간단하게만 알아두자
  • 스프링이 다양한 설정 방식을 지원할 수 있는 이유는 구체적인 설정 클래스가 아닌 BeanDefinition 이라는 추상화에 의존하고 있기 때문인 것이다. 
  • 스프링 빈을 등록하는 방법에는 두가지가 있다.
           -  XML 형식으로 직접적으로 등록하거나 
           - 자바 코드(Appconfig.class) 로 factoryBean을  이용하는 것이다.

 

 

XML로 스프링 컨테이너 설정하기 


 

 지금까지는 스프링 컨테이너 설정 방법으로 어노테이션 기반의 자바 설정 클래스를 사용했다. 

 

하지만 스프링 컨테이너는 다양한 형식의 설정 정보를 받아들일 수 있도록 유연하게 설계 되어있다. 

 

자바 코드 뿐만아니라 XML로 스프링 컨테이너를 설정하는 방법에 대해 알아보자.

 

 

어노테이션 기반 자바 코드 설정 

  • 어노테이션 기반의 자바 설정 클래스 
  • new AnnotationConfigApplicationContext(AppConfig.class)
  • 자바로 된 설정 코드를 넘기면 된다.

 

XML 설정 사용

  • 최근에는 스프링 부트를 사용하며 XML기반의 설정은 잘 사용하지 않는다
  • GenericXmlApplicationContext 를 사용하면서 xml 설정 파일을 넘기면 된다.

 

 

XmlAppContext.xml 

기존에 사용하던 어노테이션 기반의 자바 설정 클래스를 xml 파일로 바꿔보자. 

 

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService(){

        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public static MemoryMemberRepository memberRepository() {

        return new MemoryMemberRepository();
    }
    @Bean
    public OrderService orderService(){

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

//        return new FixDiscountPolicy();
        return  new RateDiscountPolicy();
    }
}

 

 설정 경로: resource>new>XML Configuration File> Spring Config 

<?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">

// 직접 빈 등록하는 부분 

<!--memberService 빈 등록하기     -->
    <bean id="memberService" class="hello.core.member.MemberServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository"/>
    </bean>
    
<!-- memberRepository 빈 등록하기 -->
    <bean id="memberRepository" class="hello.core.member.MemoryMemberRepository"/>

<!--orderService 빈 등록하기-->
    <bean id="orderService" class="hello.core.order.OrderServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository"/>
        <constructor-arg name="discountPolicy"   ref="discountPolicy"/>
    </bean>

<!--discountPolicy 빈 등록하기-->
    <bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy">
    </bean>





</beans>

 

  • 빈을 등록하기 위해 <bean> </bean> 태그를 이용한다
<bean id="빈 이름 " class="빈 타입(전체 경로적어주기)">
  • constructor- arg : 의존하는 객체를 생성자를 통해 주입해주는 역할 
         - name :  객체의 이름 
         -   ref    :  참조하고자하는 객체를 지정 
        
<constructor-arg name=" " ref=" "/>

 

 

XmlAppContextTest

  • 위에서 만든 appConfig. xml 을 설정 클래스로 하는 스프링 컨테이너를 만들어보자
  • GenericXml ApplicationContext 를 생성하여 appConfig.xml 을 넘겨주자
public class XmlAppContextTest {

    @Test
    @DisplayName("xml 파일로 스프링 컨테이너 설정하기")
    void xmlAppContext(){
        ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);

    }
}

 

 

BeanFactory와 ApplicationContext의 차이점 

 

 

 

스프링 컨테이너 생성하기

 

 

스프링 컨테이너 생성에 관한 공부를 할때 BeanFactory에 대한 언급이 잠깐 있었다. 

BeanFactory와 ApplicationContext의 차이가 무엇인지 알아보자. 

 

 

 

BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스다.
  • 스프링 빈을 관리하고 조회하는 역할을 담당한다.
  • 지금까지 우리가 사용했던 대부분의 기능은 BeanFactory가 제공하는 기능이다.
  • getBean() 을 제공한다.

 

 

ApplicationContext 

  • BeanFactory 기능을 모두 상속받아서 제공한다.
  • 빈을 관리하고 검색하는 기능을 BeanFactory가 제공해주는데, 그러면 둘의 차이가 뭘까?
  • 애플리케이션을 개발할 때는 빈을 관리하고 조회하는 기능은 물론이고, 수 많은 부가기능이 필요하다.
  • ApplicationContext의 부가기능에는 무엇이 있을까? 

 

 

ApplicationContext 의 부가기능 

 

- 메시지소스를 활용한 국제화 기능 

          예를 들어서 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력

 

- 환경변수

          로컬, 개발, 운영등을 구분해서 처리

 

- 애플리케이션 이벤트

          이벤트를 발행하고 구독하는 모델을 편리하게 지원

 

- 편리한 리소스 조회

           파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회

 

 

 

정리

  • ApplicationContext는 BeanFactory의 기능을 상속받는다.
  • ApplicationContext는 빈 관리기능 + 편리한 부가 기능을 제공한다.
  • BeanFactory를 직접 사용할 일은 거의 없다. 부가기능이 포함된 ApplicationContext를 사용한다.
  • BeanFactory나 ApplicationContext를 스프링 컨테이너라 한다

 

9~11 의 글 동안 스프링 빈을 조회하는 방법에 대해서 알아보았다.

각 글의 내용을 간단하게 정리해보자

 

  • 스프링 빈 조회 - 기본
  • 스프링 빈 조회 - 동일한 타입이 둘 이상
  • 스프링 빈 조회- 상속관계

 

빈 등록

스프링 컨테이너에 빈을 등록하는 방법으로 어노테이션을 기반으로하는 자바 설정 클래스를 이용하였다.

 

@Configuration 으로 등록된 클래스를 설정 클래스, 설계도로 하여

@Bean으로 등록된 메서드들의 이름을 빈 이름으로 하여 저장소에 빈을 등록했다. 

 

 

빈 조회 - 기본

스프링에서 빈을 조회하는 가장 기본적인 방법은 getBean을 이용하는 것이다. 

getBean(빈 이름, 빈 타입)
getBean(빈 이름) 
getBean(빈 타입)

 

 

찾고자 하는 빈이 컨테이너에 등록되어있지 않다면  NoSuchBeanDefinitionException 예외가 발생한다. 

 

 

빈 조회 - 동일한 타입이 둘 이상

getBean( 빈 타입) 을 이용하여 타입만으로 빈을 조회할 수 있었다. 

그런데 만약 같은 타입의 빈이 둘 이상이라면 어떻게 될까? 

 

같은 타입의 빈이 둘 이상인 경우  NoUniqueBeanDefinitionException  중복 오류가 발생했다. 

 

그래서 같은 타입의 빈을 조회하는 경우, 조회하고자 하는 빈을 구분할 수 있게 빈이름도 같이 알려주기로 했다. 

 

같은 타입의 모든 빈들을 조회하고 싶다면 getBeansOfType(빈 타입) 을 이용하면 된다. 

 

조회- 상속관계

 

상속관계에 있는 빈들을 조회하는 방법에는 중요한 원칙이 있다.

바로 부모 타입을 조회하면 자식 타입도 함께 조회하여 알려준다는 점이다. 

 

즉, 부모 타입으로 조회하면 중복오류가 발생할 수 있다는 것인데 이런 경우 빈 이름을 알려주거나 

특정 타입(자식 타입)으로 조회하면 된다. 

 

또한 Object 타입의 경우 모든 자바 객체 클래스의 최상위 부모이기 때문에 

Object 타입으로 조회시 모든 빈들을 조회할 수 있다.

 

+ Recent posts