본문 바로가기
study/Spring

싱글톤(Singleton)방식에 대하여 - Spring 프레임워크와 싱글톤 컨테이너

by 고기만두(개발자) 2024. 2. 9. 10:06
728x90
반응형

웹 애플리케이션은 여러 고객이 동시에 서비스를 요청하게 된다.

사실 당연하다.

오프라인 패스트푸드 매장 줄 서서 주문하듯이 한 명씩 주문하는 거 아니잖아?

 

여러 명(세션)이 동일한 서비스를 요청하면 프로그램에서는 무슨 일이 벌어질까?

해당 서비스에 관한 객체를 그때마다 생성해야 할까?

 

//20240209 V2.0 수정 - 결론을 상단에 배치 및 장표도 상단으로 당김, 결론 강조

싱글톤 방식은 그런 고민에서 출발했다.

클래스와 인스턴스를 딱 하나만 생성할 수 있도록 보장하는 디자인 패턴이다.

같은 서비스를 여러 사람(세션)이 호출할 때, 굳이 여러 개의 객체를 생성하지 않는다.

한 컷 요약 - 오른쪽이 싱글톤방식

 

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.Member;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

//SingletonTest.java
public class SingletonTest {
    @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);
    }
}
package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

//AppConfig.java
@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService(){   //메소드명에서 역할이 드러나고,
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());//생성자 주입-> 20210926 리팩토링
    }
    @Bean
    public MemberRepository memberRepository() {   //구현하는 부분도 db설정 등에 따라 바뀔때 여기만 바꾸면 되고
        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();
    }

}

 

코드를 이렇게 짜놨으면 그렇다. AppConfig.java 는 요청할 때마다 객체를 새로 생성한다.

그렇다면, 트래픽이 초당 10000이면 초당 10000개의 객체를 생성하고 소멸해야 한다는 얘긴데

거 콤퓨타 양반, 이건 너무 메모리 낭비가 심한 거 아니오?

 

 

728x90

 

package hello.core.singleton;
//SingletonService.java
public class SingletonService {
    private static final SingletonService instance = new SingletonService();

    public static SingletonService getInstance(){
        return instance;
    }

    private SingletonService(){

    }
}


//Singletontest.java 이어서

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

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

        assertThat(singletonService1).isSameAs(singletonService2);
    }

 

SingletonService.java에서 SingletonService()를 호출할 때, private을 사용했다.

그리고 getInstance()를 통해서만 조회가 가능하게 했다.

이런 식으로 코딩하면 무조건 같은 singletonService를 가져올 수밖에 없다.

 

싱글톤은 이런 방식이라는 얘기를 참 길게도 했다.

그렇다면 쌩자바에서 싱글톤 방식을 채용하면 단점은 없을까? 당연 있다.

  • 의존관계상 클라이언트가 구체 클래스에 의존하다 보니, DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트, 내부 속성 변경. 초기화가 까다롭다.
  • private 생성자로 자식 클래스를 만들기 어렵다: 결론적으로 유연성이 떨어진다.

 

//20240209 V2.0 수정 : 관련된 주제의 다른 참고글을 추가하였다.

여기서 DIP / OCP가 뭔지 잘 기억이 안 나면 SOLID 원칙을 복습하자

https://career-gogimandu.tistory.com/27

 

객체지향 설계에서 꼭 필요한 SOLID 5대원칙(SRP/OCP/LSP/ISP/DIP)

SRP : Single Responsibility Principle, 단일책임 원칙 OCP : Open Closed Principle, 개방-폐쇄 원칙 LSP : Liskov Subtitution Principle, 리스코프 치환원칙 ISP : Interface Segregation Principle, 인터페이스 분리 원칙 DIP : Dependency

career-gogimandu.tistory.com

 

그런데, 지금 공부하는 건 쌩자바 방식이 아닌, 스프링 '프레임워크'다.

이런 단점을 해결해 주라고 있는 게 프레임워크 아니었나?

스프링 컨테이너는 싱글톤 패턴을 따로 적용하지 않아도,

싱글톤 컨테이너의 역할을 할 수 있다는 장점을 갖는다.

저런 지저분한 쌩노가다를 굳이 안 해도 된다는 얘기다.

@Test
    @DisplayName("스프링컨테이너와 싱글톤")
    void springContainer(){
        //AppConfig appConfig = new AppConfig();
        ApplicationContext 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);

        //memberService1 != memberService2
        assertThat(memberService1).isSameAs(memberService2);
    }
}
//SingletonTest.java 이어서

 

결과는 memberService1 = memberService2

 

대신, 이게 가능하려면 주의해야 할 게 있다.

여러 클라이언트가 같은 하나의 객체 인스턴스를 공유하려면,

싱글톤 객체는 상태를 가지면 안 된다.

  • 특정 클라이언트에 의존적이어서도 안 되고
  • 특정 클라이언트가 값을 변경해서도 안 되고
  • 가급적 읽기만 가능하면 더욱 좋고
  • 자바에서 공유되지 않는 지역변수/파라미터 등을 사용하는 편이 유리하다.
반응형

 

//작성에 도움받은 자료 [스프링 핵심원리] - 인프런 강의자료

//V2.0 20240209 수정 - 개행 일부 수정 하고 맞춤법 검사를 하였습니다. 생각보다 편하게 써 내려가다 보면 띄어쓰기가 표준에 맞지 않는 경우가 있어요.

 

 

V1.0 글을 풀스크린 캡쳐로 아카이브 해놨는데, 궁금하신 분은 접은 글을 펼쳐 더보기 ▽

 

728x90
반응형

댓글