jhhan의 블로그

스프링 코어(7) - 싱글톤(singleton)과 Configuration 본문

Spring

스프링 코어(7) - 싱글톤(singleton)과 Configuration

jhhan000 2021. 4. 27. 14:42

이번에는 Singleton 패턴과 @Configuration 어노테이션에 대해서 알아보겠습니다.

 

이전까지의 예제를 통해서 싱글톤 패턴에 대해서 잘 배운 것 같습니다.

이제 다시 한번 AppConfig 파일을 살펴보겠습니다.

@Configuration
public class AppConfig {

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

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

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

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

}

예전에 작성해둔 코드로 잘 돌아가고 있는 코드입니다.

그리고 싱글톤 패턴에 대해서 배웠으니 한번 적용해 보겠습니다.

(싱글톤 패턴은 스프링 컨테이너에 자동적으로 적용이 된다고 했으니 싱글톤 컨테이너 라고 해도 됩니다.)

 

memberRepository()를 중점적으로 살펴보겠습니다.

  • memberService()가 호출되면 new MemoryMemberRepository()가 호출.
  • orderService()가 호출되면 new MemoryMemberRepository()가 호출.
  • 모두 new로 호출됩니다!
  • 그러면 memberRepository()는 객체가 2개 이상 생성되는 것일까?
  • 만약 객체 2개가 생성이 된다면 싱글톤 패턴이 깨지는 것!

스프링 컨테이너는 아마 이런 점을 해결해줬을 것입니다. 과연 어떻게 했을까요..

 

확인해보기 위해서 테스트 코드를 작성해봅니다.

먼저 다른 부분부터 고쳐봅시다 ;; ㅎㅎ

public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }

    // 테스트 용도
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    // 테스트 용도
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

테스트를 위해 getMemberRepository() 메서드를 추가합니다.

원래라면 MemberService와 OrderService에 추가를 해야하지만(인터페이스에 추가...),

테스트를 위한 것이기 때문에 구현체에 바로 추가해봅니다.

singleton 패키지 밑에 ConfigurationSingletonTest라는 자바 파일을 추가해서 테스트 코드를 작성합니다.

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

public class ConfigurationSingletonTest {

    @Test
    void configurationTest() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        // 원래는 구체 클래스 타입으로 불러오는 것은 안 좋음. get메소드를 사용하기 위해 어쩔 수 없이 사용함.
        MemberServiceImpl memberService1 = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService1 = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        MemberRepository memberRepository1 = memberService1.getMemberRepository();
        MemberRepository memberRepository2 = orderService1.getMemberRepository();

        /**
         * 확인하기 위한 출력 -> 3개 모두 같은 것을 확인 가능!!!
         * memberRepository는 3번 new로 호출되었으니 3개의 값 모두 다른 것이 정상 아닐까? 하지만 아님. why?
         */
        System.out.println("MemberService -> memberRepository1 = " + memberRepository1);
        System.out.println("OrderService -> memberRepository2  = " + memberRepository2);
        System.out.println("memberRepository                   = " + memberRepository);

        assertThat(memberRepository1).isSameAs(memberRepository);
        assertThat(memberRepository2).isSameAs(memberRepository);
    }
}
  • 주석에 작성한 것처럼 구체 클래스 타입을 불러오는 것은 좋지 않습니다.
  • get메서드를 사용하기 위해 어쩔 수 없이 이렇게 구현한 것입니다.
  • 그리고 3개의 값을 모두 확인해 줍니다.
  • memberRepository 객체도 따로 확인해서 3개 모두 다른 값인지 확인해봅니다.

테스트를 실행해본다면? 다음과 같은 결과가 나오게 됩니다.

모두 같은 참조값이 나옵니다.

 

신기합니다... 그러면 3개 객체가 생성되는 것이 아닌 1개의 객체가 공유된다고 봐야겠네요.

그리고 싱글톤 패턴도 지켜지는 것을 알 수 있습니다.

아니면 호출이 되지 않는 것일까요?

AppConfig에서 확인해봅니다.

@Configuration
public class AppConfig {

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

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

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

    @Bean
    public OrderService orderService() {
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

}

memberRepository가 호출되는 곳에 로그가 찍히도록 해봅니다. ㅎㅎ

그리고 다시 테스트 코드를 돌려봅니다!

이렇게 출력이 됩니다.

memberRepository()가 한번 호출이 된것을 알 수 있습니다..

제가 생각했던대로라면...

memberRepository(), memberService(), orderService() 총 3번이 호출되어야 할 것 같은데요...

 

어떤 로직이 있기에 이런 일이 일어날까요?

 

  • 스프링 컨테이너는 싱글톤 컨테이너이기 때문에 
  • 스프링 빈이 싱글톤이 되도록 보장해야 함
  • 하지만 자바 코드까지 조작하는 것은 스프링이 관여하기 어려움
  • 자바 코드만 보면 3번 호출되는 것이 맞음!

이런 문제를 해결하기 위해서 스프링은

바이트 코드를 조작하는 라이브러리를 사용한다고 합니다.

그리고 이것은 @Configuration을 적용한 AppConfig에 있습니다.

 

그러면 AppConfig의 정보를 살펴보기 위한 테스트 코드를 작성해봅니다.

public class ConfigurationSingletonTest {

    ....

    @Test
    void configurationDeep() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        AppConfig bean = ac.getBean(AppConfig.class);
        System.out.println("bean = " + bean.getClass());
    }
}

ConfigurationSingletonTest 자바 파일에서 진행합니다.

그리고 코드를 실행해봅니다.

bean 정보가 출력됩니다.

근데.. bean 정보가 생각한 것과 다릅니다.

class hello.core.AppConfig 가 출력될 것을 예상했기 때문입니다.

근데 뒤에 무엇인가가 더 적혀있네요.

뒤에 적혀있는 것 중 CGLIB이 있습니다.

바로 이것이 원인입니다.

  • 스프링은 CGLIB을 통해 바이트코드 조작 라이브러리를 사용하고
  • AppConfig 클래스를 상속받은 다른 클래스를 만듭니다.
  • 그리고 그 클래스를 스프링 빈으로 등록합니다.
  • 그래서 실제 실행 시 다른 클래스가 작동합니다.
  • 그렇기 때문에 저희가 예상했던 것과 다르게 작동하는 것입니다.

 

정리를 해보겠습니다.

  1. AppConfig라는 클래스가 있습니다.
  2. CGLIB을 통해 AppConfig를 상속하는 임의의 클래스를 생성합니다.
  3. 그리고 이 임의의 클래스가 스프링 빈으로 등록됩니다.
  4. 이름은 AppConfig라고 등록이 되어있지만
  5. 인스턴스 객체는 AppConfig@CGLIB 으로 생성이 됩니다.
  6. 참고로 AppConfig를 상속해서 AppConfig@CGLIB은 자식 클래스가 됩니다.
  7. 그래서 AppConfig 타입으로 조회를 해도 검색이 됩니다.

CGLIB이 모든 것을 가능하게 해줬습니다.

 

 

그럼 @Configuration을 제거하고 @Bean만 사용해도 될까요?

@Configuration을 주석처리 하고 진행해봅니다.

// @Configuration
public class AppConfig {

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

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

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

    @Bean
    public OrderService orderService() {
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

}

아마 @Configuration을 주석 처리해서 빨간 줄이 나타날 수도 있습니다.

테스트 코드 실행에는 전혀 영향을 주지 않기 때문에 신경 안쓰셔도 됩니다.

테스트 코드는 잘 실행됩니다.

그리고 @Configuration을 주석처리 했더니 제가 예상했던 대로 출력이 됩니다.

memberRepository가 3번 호출됩니다!!

그리고 memberRepository는 각각의 참조값이 달라지게 됩니다.

아마 테스트 코드 실행은 실패를 할 것입니다.

그 이유는 

  • assertThat(memberRepository1).isSameAs(memberRepository);
  • assertThat(memberRepository2).isSameAs(memberRepository);

이 2개의 코드 때문이죠. 참조값이 서로 같지 않기 때문에 에러가 발생합니다.

 

@Configuration을 지워버리니 

제가 생각한대로 코드 동작이 되기는 하지만,,

싱글톤 패턴이 지켜지지 않는 것을 확인할 수 있습니다..

 

 

@Bean 만 사용하더라도 스프링 빈 등록에는 문제가 없지만,

싱글톤 패턴이 지켜지지는 않습니다. → 싱글톤을 보장하지 않습니다...

 

즉, 싱글톤 패턴을 지키고 싶다면

@Configuration을 사용하면 됩니다.

꼭 @Configuration을 사용해서 스프링 컨테이너를 만들면 되겠습니다. ㅎㅎ

 

 

 

이렇게 싱글톤과 @Configuration에 대해서 알아봤습니다.

 

포스트를 마칩니다.

 

 

 

출처: 인프런 - 스프링 핵심원리(기본편) by 김영한