Notice
Recent Posts
Recent Comments
Link
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

공hannah부

싱글톤 (Singleton) 본문

공부/백엔드

싱글톤 (Singleton)

Hannah0226 2023. 9. 16. 18:47

웹 애플리케이션과 싱글톤

 

웹 애플리케이션은 보통 여러 고객이 동시에 요청

스프링이 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때마다 객체 새로 생성

메모리 낭비

해당 객체가 1개만 생성되고, 공유하도록 설계

싱글톤 패턴

 

싱글톤 패턴

 

싱글톤 패턴이란?

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴
  • private 생성자를 사용해 외부에서 new 키워드를 사용하지 못하도록 막아야 한다.

 

구현

  • static 영역에 객체 instance를 미리 하나 생성
  • 이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회 가능 → 항상 같은 인스턴스 반환
  • 딱 한개의 객체 인스턴스만 존재해야 하므로 생성자를 private으로 막음
package hello.core.singleton;

public class SingletonService {
	private static final SingletonService instance = new SingletonService(); //자기자신을 생성하여 instance에 넣는다.

	public static SingletonService getInstance() { //조회할 때 쓴다.
		return instance;
	}

	private SingletonService() {

	}

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

 

Test 코드로 확인

  • 스프링이 없는 순수한 DI 컨테이너에서는 각각의 참조값이 다름을 확인할 수 있다.
  • 싱글톤 패턴을 적용한 객체를 사용하면 참조값이 같음을 확인할 수 있다.
    • 즉, 싱글톤 패턴을 적용하면 고객의 요청이 올 때마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다!
package hello.core.singleton;

import hello.core.member.MemberService;
import hello.core.order.AppConfig;
import org.assertj.core.api.Assertions;
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();

		//2. 조회 = 호출할 때 마다 객체를 생성
		MemberService memberService2 = appConfig.memberService();

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


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

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

		//private으로 생성자를 막아두었기 때문에 new SingletonService()으로 객체를 생성한다면 컴파일 오류가 난다. 
		SingletonService singletonService1 = SingletonService.getInstance();
		SingletonService singletonService2 = SingletonService.getInstance();
        
		//같은 값을 반환함을 확인
		System.out.println("singletonService1 = " + singletonService1);
		System.out.println("singletonService2 = " + singletonService2);
        
		Assertions.assertThat(singletonService1).isSameAs(singletonService2);
		
	}
}

 

싱글톤 패턴의 문제점

  • 구현하는 코드 多
  • 의존관계상 클라이언트가 구체 클래스에 의존 → DIP 위반, OCP 원칙을 위반할 가능성 ↑
  • 테스트 어려움
  • 내부 속성 변경 및 초기화 어려움
  • Private 생성자로 자식 클래스 만들기 어려움
  • 유연성 ↓

        → 스프링 컨테이너는 다 해결!!

 

스프링 컨테이너

 

스프링 컨테이너란?

  • 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리한다.
  • 컨테이너 내에서 특정 클래스에 대해 @Bean이 정의되면, 그 클래스에 대해 딱 한 개의 인스턴스를 생성하고, @Bean이 호출될 때마다 생성된 공유 인스턴스 리턴한다.
  • 싱글톤 객체를 생성하고 관리하는 기능 → 싱글톤 레지스트리

장점

  • 싱글톤 패턴의 단점을 해결하면서 객체를 싱글톤으로 관리 가능
    • 코드 간단
    • DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤 사용 가능

 

싱글톤 방식의 주의점

싱글톤 객체는 유지(stateful)가 아닌 무상태(stateless)로 설계해야 한다!
  • 특정 클라이언트에 의존적인 필드가 있으면 X
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 X
  • 가급적 읽기만 가능해야 한다
  • 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다
    • 스프링 빈의 필드에 공유 값을 설정하면 큰 장애가 발생할 수 있다.

 

예시 코드

public class StatefulService {
	private int price; // 상태를 유지하는 필드

	public void order(String name, int price) {
		System.out.println("name = " + name + "price = " + price);
		this.price = price;
	}

	public int getPrice() {
		return price;
	}
}

 

Test 코드

	@Test
	void statefulServiceSingleton() {
		ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
		StatefulService statefulService1 = ac.getBean(StatefulService.class);
		StatefulService statefulService2 = ac.getBean(StatefulService.class);

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

		//ThreadA : 사용자A 주문 금액 조회
		System.out.println("price = " + userAPrice); 
		//ThreadB의 값인 20000원이 나온다. -> statefulService 1 이든, 2이든 다 같은 객체를 참조하고 있는 싱글톤이기 때문
	}

 

해결 방법

  • 전역변수로 변경하기!
	public int order(String name, int price) {
		System.out.println("name = " + name + "price = " + price);
		return price;
	}

 

자바 싱글톤 vs 스프링 싱글톤

 

자바 싱글톤: 클래스가 ClassLoader에 의해 단 한번만 인스턴스화 하는 것을 이용하여 구현 됨

  • 생성자를 private 으로 선언: 외부에서 클래스의 오브젝트 생성 X
  • 참조는 static 으로 정의: 어느 영역에서든 접근 가능

 

스프링 싱글톤: 스프링 컨테이너에 의해 구현 됨

  • 컨테이너 내에서 특정 클래스에 대해 @Bean이 정의되면, 스프링 컨테이너는 그 클래스에 대해 한 개의 인스턴스를 만듦
  • 이 공유 인스턴스는 설정 정보에 의해 관리되고, bean이 호출 될 때마다 스프링은 생성된 공유 인스턴스를 리턴 시킴