본문 바로가기

Backend/Spring

[Spring] 싱글톤 컨테이너 1) 싱글톤 컨테이너의 필요성

728x90

 

목차

  • 싱글톤 컨테이너 - 웹 애플리케이션과 싱글톤 
  • 싱글톤 컨테이너 - 싱글톤 패턴 (디자인패턴에서의..)
  • 싱글톤 컨테이너 - 싱글톤 컨테이너 -> 스프링 컨테이너가 자동으로 싱글톤 패턴을 적용해줌 
  • 싱글톤 컨테이너 - 싱글톤 방식의 주의점
  • 싱글톤 컨테이너 - @Configuration과 싱글톤
  • 싱글톤 컨테이너 - @Configuration과 바이트코드 조작의 마법

 

 

 

싱글톤 패턴의
필요성

 

싱글톤?

"객체"가 현재 나의 자바 JVM메모리 안에 하나만있어야하는 패턴 

 

웹어플리케이션에서 왜 싱글톤이 주로 사용되는가?

애플리케이션은 종류가 매우 많은데(데몬, 배치, 웹 등등) , 스프링은 보통 웹이다 (물론 웹아닌 애플리케이션도 가능)

웹 애플리케이션은 보통 여러 고객이 동시 요청 이 매우 많이 들어옴 ( 초당 n만번 등)

 

현재 코드의 문제점(스프링 컨테이너를 사용하지 않고 appConfig에서 메소드를 호출하는 경우) :

고객이 요청할때 마다 객체를 계속 new해서생성하게됨 

 

실제로 계속 만들어지는것을 코드로 확인해보기 

 

먼저 singleton에 관한 패키지와 클래스를 생성한다.

 

SingletonTest 

package hello.core.singleton;

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

public class SingletonTest {


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

        //1. 조회 : 호출할 때 마다 객체 생성
        MemberService memberService1 = appConfig.memberService();//memberService 메소드 호출 -> new로 객체생성 후 반환
        //2. 조회 : 호출할 때 마다 객체 생성
        MemberService memberService2 = appConfig.memberService(); //memberService 메소드 호출 -> new로 객체생성 후 반환

        //참조값이 다른 것을 확인 ( 객체가 2개 생성됨 )
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);
        
                //테스트 성공을 위해서는 사실 아래와 같이 하는게 맞음
        //memberService != meberService2
        //assertThat(memberService1).isNotSameAs(memberService2);
    }


}

서로 다른 클라이언트가 요청을 보냈다고 생각하고,  memberService에 대해 2번의 조회를 연달아 만든다.

결과를 보면, 다음과 같이 같은 종류의 객체가 두번 생성됨을 확인할 수 있다.

 

즉, 이런 방식은 고객이 초당 100회의 요청을 보내면, 100개의 객체가 생성되고 소멸되기 때문에, 메모리 낭비가 매우 심하다.

해결 방안은 객체가 딱 1개만 생성되고, 이를 공유하도록 설계하는 것이다. (싱글톤 패턴)

 

 

 

 

싱글톤 패턴

 

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

객체 인스턴스를 2개이상 생성하지 못하도록 막는 것

(private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.)

 

그렇게 되도록 일단 코드를 작성해 보도록 한다.

간단하다. static 변수로 생성하면 되고, private생성자를 통해 외부에서 임의로 new키워드를 사용하지 못하도록 막으면 된다.

 

static 제어자에 대해서는 여기를 참고

2021.09.30 - [LANGUAGE/JAVA] - [Java] 제어자(modifier)

 

[Java] 제어자(modifier)

제어자란? 제어자란 클래스, 변수, 메소드 선언부 앞쪽에 함께 사용되어 부가적의미를 부여한다. 종류 접근제어자 public, protected, default, private 그 외의 제어자 static, final, abstract, native, transie..

iagreebut.tistory.com

 

singleton 패키지 하위에 다음 클래스를 생성한다. 

 

SingletonService

package hello.core.singleton;

public class SingletonService {


    //static으로 선언하여, 공유할 수 있도록 만들고 -> 자바가 올라갈 때 생성되는 변수
    private static final SingletonService instance = new SingletonService(); //자기자신을 내부에 private 객체로 가짐 -> 특히 static으로 선언하면, 클래스 레벨에 올라가 1개만 존재하게 됨


    // 이 메서드를 이용해서 해당 공유변수를 조회한다.
    public static SingletonService getInstance(){
        return instance;
    }


    //private 생성자를 이용해 다른 클래스에서(내부 클래스에서는 당연히 호출가능) new로 새로운 객체를 생성하는 것을 막는다.
    private SingletonService(){

    }

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



}
  • static 변수로 공유할 수 있도록 생성
  • 메서드를 통해 조회를 가능하도록 함
  • private생성자를 만들어 다른 클래스에서 new로 생성이 불가능하도록 하기 

 

 

 

이제 이를 실제로 불러와 테스트해보며, 2번 호출해도 서로 다른 객체가 생성되지 않는지 확인해본다.

singletonTest 클래스에서 다음을 추가해 테스트해본다.

    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용(SingletonService)")
    void singletonServiceTest(){
//        private access불가능하기 때문에 new로 호출 불가능 (컴파일 에러)
//        new SingletonService();

        //1. 조회
        SingletonService singletonService1 = SingletonService.getInstance();//getInstance로 static변수 호출 -> 새로운 객체가 생성되지 않음
        //2. 조회
        SingletonService singletonService2 = SingletonService.getInstance();//getInstance로 static변수 호출 -> 새로운 객체가 생성되지 않음


        // 서로 같은 객체가 출력됨
        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);


        //실제 인스턴스가 같은지 비교해야 하므로 isSameAs
        assertThat(singletonService1).isSameAs(singletonService2);


    }

 

결과는 다음과 같이 새로운 객체가 생성되지 않았음을 확인할 수 있다! 

 

 

이제 호출 시 마다 새로운 객체가 생성되는 비효율성을 막는 방법을 알아냈으니, AppConfig의 4가지 @Bean를 위와 같이 변경하면 될 것이다.

하지만, 직접해줄 필요는 없다. 스프링 컨테이너가 자동으로 싱글톤으로 등록해준다. 

 

 

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

 

 

 

 

싱글톤 패턴의 문제점

 

여러 문제점을 가진다.

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다. 
    • getInstance()를 사용하기 위해서 구체 클래스에 의존해야한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다. 
  • 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다. (DI사용이 어려움)
  • 안티패턴으로 불리기도 한다.

 

스프링 컨테이너는 위와 같은 단점은 전부 해결하고, 싱글톤 패턴의 장점만을 가진 싱글톤 컨테이너라는 방식을 사용하게 해준다. 

 

 

 

 

싱글톤 컨테이너

 

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

 

따라서 위의 코드처럼 직접 AppConfig객체를 만들고 내부에 있는 메소드를 호출하지 않고,

처음부터 스프링 컨테이너를 생성하여 이를 가지고 만들면 

위처럼 싱글톤 패턴으로 디자인할 필요없이, 싱글톤 패턴이 적용된다. 

등록시 객체를 생성 해둬 저장한 후 이를 사용한다. (객체를 하나만 만든다!)

 

SingletonTest

 

    @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer(){

        //        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);

        //실제 인스턴스가 같은지 비교해야 하므로 isSameAs
        assertThat(memberService1).isSameAs(memberService2);


    }

 

실행 결과를 확인해 보면, 기대한 대로 인스턴스가 같게 나온다.

 

 

싱글톤이 아닌, 요청시 마다 새로운 객체를 반환하게 할 수 있는 방법도 있지만, 사실 거의 쓰지 않는다.

728x90