싱글톤 패턴

Ango·2023년 6월 10일

SPRING

목록 보기
5/13

싱글톤 패턴
클래스의 인스턴스가 1개만 생성되는것을 보장하는 디자인 패턴이다.

최초의 한번만 객체를 생성하기 때문에 추후 해당 객체에 접근할 때 메모리 낭비를 방지할 수 있다.

java 코드를 통해 싱글톤 패턴을 적용한 예제이다.

public class SingletonService {
   //static 영역에 객체를 딱 1번만 생성 
   pirvate static final SingletoneService instance = new SingletonService();

//2. public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록
허용한다.
   public static SingletonService getInstance(){
       return instance; 
   }
   
// 3. 생성자를 prviate로 선언해서 외부에서 new 키워드로 객체를 생성하지 못하게 한다. 
   private SingletonService() {
   }

}

위의 코드를 테스트 해보자 .

@Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    void singletonServiceTest() {
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);

        Assertions.assertThat(singletonService1).isSameAs(singletonService2);
    }

테스트 실행겨과 다음과같이 같은 객체가 생성됨을 볼 수 있다.

하지만
1. 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
2. 의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다.
3. 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
4. 테스트하기 어렵다.
5. 내부 속성을 변경하거나 초기화 하기 어렵다.
6. private 생성자로 자식 클래스를 만들기 어렵다.
7. 결론적으로 유연성이 떨어진다.

위와 같이 많은 단점들이 있다고 한다.

역시 이것을 해결해주는것이 Spring의 기능이자 역할이다.

  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
@Test
    @DisplayName("싱글톤 컨테이너와 싱글톤")
    void springContainer() {
//        AppConfig appConfig = new AppConfig();
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
//        MemberService memberService1 = appConfig.memberService();
//        MemberService memberService2 = appConfig.memberService();
        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        Assertions.assertThat(memberService1).isSameAs(memberService2);
    }

이전 글에서 봤던 AppConfig를 구성정보로 참고하는 스프링의 ApplicationContext 컨테이너에서 관리하는 객체를 호출해보면

싱글톤 패턴으로 디자인을 설계한적이 없는데도 같은 객체를 호출 하는것을 볼 수있다.

@Configuration과 싱글톤
@Configuration 어노테이션을 사용하면 CGLIB 이라는 바이트코드 조작 라이브러리를 사용하여
AppConfig 클래스를 상속받은 AppConfig@CGLIB라는 클래스를 만들고 이를 스프링 빈으로 등록한다.
그때 그 클래스가 @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드를 동적으로 만드는 방식으로 싱글톤 패턴을 유지해준다.

호출 로그를 통해서 알아보자.

@Configuration
public class AppConfig {

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

    @Bean
    public OrderService orderService(){
        System.out.println("AppConfig.orderService");

        return new OrderServiceImpl(memberRepository(),discountPolicy());
    }

    @Bean
    public MemberRepository memberRepository() {

        System.out.println("AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

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

}

만약 meberService(),orderService() , memberRepository를 차례로 실행하면 몇개의 로그가 찍힐까?

우리가 상식 예상할 수 있는 결과는
1. meberService()를 호출할때 - "call AppConfig.memberService" 와 "AppConfig.memberRepository"

  1. orderService()를 호출할때 "AppConfig.orderService"와 "AppConfig.memberRepository"

  2. memberRepository()를 호출할 때 "AppConfig.memberRepository"

총 5개의 로그 그리고 memberRepository()가 3번 호출되므로 동일한 로그가 3개 찍히는 것을 예상 할 수 있다.

테스트로 알아보자 .

@Test
    void configurationTest() {
        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);
        //모두 같은 인스턴스를 참고하고 있다.

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

        assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }

하지만 예상과 다르게 AppConfig.memberRepository 로그는 단 한번만 호출된다.
그 이유는 바로 @Configuration이 @Bean이 붙은 메서드를 스프링 빈에 등록하고 이후에 이미 빈으로 등록된 메서드라고 판단했을시에는 이미 존재하는 빈을 반환하는 방식으로 관리해준다.

그 증거로, @Configuration 어노테이션을 삭제하고 @Bean 만을 남겨뒀을 때

다음과 같이 memberRepository 로그가 3번 중복돼서 출력된다.

이를 통해 스프링빈에 이미 등록된 객체는 이를 참조하여 바로 리턴타입을 반환해주는 것을 확인 할 수 있다!!!!!!!!!

정리

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

0개의 댓글