[Spring] 싱글톤 패턴 (singleton pattern)

유아 Yooa·2023년 6월 6일
0

Spring

목록 보기
9/18
post-thumbnail

Singleton pattern

  • 소프트웨어 디자인 패턴에서 싱글톤 패턴에 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다.
  • 즉, 클래스의 인스턴스가 Java JVM 내의 단 하나만 존재하는 디자인 패턴을 뜻한다.

Singleton 왜 사용하는데?

  • 웹 어플리케이션은 수많은 클라이언트에서 서비스를 요청받게 되는데 만약 서버에서 클라이언트의 요청을 받을때마다 클래스 인스턴스를 생성하게되면 JVM 메모리 사용량이 증가하게 되고 서버는 부하를 감당할 수 없게 될 것이다.

ExampleConfig.java

@Configuration
public Class ExampleConfig {

	@Bean
    public ExamBean exam() {
    	return new ExamBean();
    }
}

SingletonTest.java

public class SingletonTest {
	
    @Test
   	void pureContainer() {
    	ExampleConfig examConfig = new ExampleConfig();
        
        // 호출할 때마다 객체를 생성
    	ExamBean exam1 = examConfig.exam();
        ExamBean exam2 = examConfig.exam();
        
        System.out.println("exam1 = " + exam1);
        System.out.println("exam2 = " + exam2);
        
    	Assertions.assertThat(exam1).isNotSameAs(exam2);

    }
}

결과

exam1 = com.example.demo.ExamBean@26c16c02
exam2 = com.example.demo.ExamBean@52b02322
  • Spring이 아닌 Java 코드를 이용한 ExampleConfig는 요청이 들어올 때마다 새로운 ExamBaen 클래스의 인스턴스를 생성하게 되고 그만큼 JVM의 메모리 사용량이 증가하게 되는 것이다.
  • ExampleConfig에서 생성한 ExamBean 객체를 공유하면 해결할 수 있다.

이번에는 싱글톤 패턴을 적용해보자.

  • 원초적인 방법은 클래스 내부에서 private static final 키워드로 객체를 만들면 외부에서는 해당 클래스의 객체를 새로 생성할 수 없으므로 싱글톤 패턴 조건을 만족하게 된다.

SingletonService

public class SingletonService {

    private static final SingletonService instance = new SingletonService();

    public static SingletonService getInstance() {
        return instance;
    }

    private SingletonService() {
        
    }
}

Test.java

public static void main(String[] args) {
    SingletonService singletonService = new SingletonService();
}
  • 'SingletonService()' has private access in... 에러 확인 가능
  • SingletonService 클래스 객체를 getInstance 메서드를 통해서만 접근할 수 있다.

SingletonTest.java

@Test
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);
}
  • static 객체를 통해서 해당 객체 1개만 생성할 수 있도록 지정한다.
  • private 생성자를 통해서 외부에서 임의로 new 연산자로 객체를 생성하는 것을 제한한다.
  • getInstance 메소드를 통해서만 조회할 수 있고, 항상 동일한 객체가 반환된다.

순수 Java 코드로 싱글톤 패턴을 적용하는 방법은 실제로 사용하기에는 불편하고 실질적인 문제점들이 존재한다.

  1. 싱글톤 패턴을 구현하기 위한 코드가 늘어난다.
  2. 의존 관계상 클라이언트가 구체 클래스에 의존한다. (DIP,OCP 위반)
  3. 내부 속성을 변경, 초기화하기 어렵다.
  4. private 생성자로 자식 클래스를 생성하기 어렵다.
  5. 유연성이 떨어진다.
  6. 테스트마다 데이터를 초기화를 해주어야 하므로 까다롭다.

( DIP(Dependency Inversion Principle, 의존 역전 원칙) : 구현체에 의존하지 말고, 역할(인터페이스)에 의존해야 한다는 원칙)
(
OCP(Open Closed Principle, 개방 폐쇄 원칙) : 소프트웨어 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다는 원칙)

Singleton Container

  • 스프링에서는 사용자가 일일이 구현하지 않고 싱글톤 패턴의 문제점을 보완해주며 싱글톤 패턴으로 클래스의 인스턴스를 사용하게 해준다.
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 하며 싱글톤 객체를 생성, 관리하는 주체를 싱글톤 레지스트리라고 부른다.
  • 스프링 컨테이너의 기능으로 인해 순수 Java에서 싱글톤 패턴을 구현하기 위한 코드를 사용하지 않아도 되고 OCP 원칙을 위배하지 않게 된다.

SingleTest.java

@Test
void springContainer() {

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

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

}

결과

memberService1 = com.example.springdemostudy.member.MemberServiceImpl@78e16155
memberService2 = com.example.springdemostudy.member.MemberServiceImpl@78e16155
  • 스프링에서 싱글톤 관련 코드는 작성하지 않아도 bean 객체를 1개 설정한다.
  • 객체를 새롭게 생성해도 같은 객체를 생성한 결과를 보인다.

Singleton 방식 주의점

  • 싱글톤 패턴은 여러 클라이언트가 객체를 공유하므로 공유되는 객체는 어떠한 상태를 유지(stateful)되게 설계하면 안 되고 다음과 같은 사항을 반드시 지켜야 한다.
    • 특정 클라이언트에 의존적인 필드가 있으면 안 된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안 된다.
    • 읽기만 가능해야 한다.
    • 필드 대신 Java에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.

StatefulService.java

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;
    }
}
  • price는 공유되는 필드이기 때문에 특정 클라이언트가 값을 변경할 수 있다.

StatefulaServiceTest.java

class StatefulServiceTest {

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

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

        // ThreadA : 사용자 A 주문금액 조회
        int price = statefulService1.getPrice();
        System.out.println("price = " + price);

        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }

    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }

}
  • userA가 자신의 구매가격을 호출했는데도 10000원이 아닌 20000원이 반환되었다.
  • 싱글톤 패턴에서 공유 필드를 접근하다보면 예기치 못한 장애가 발생할 수 있다.
    • stateless하게 설계하는 것이 중요하다.
profile
기록이 주는 즐거움

0개의 댓글