프로그래밍 언어/Java

Java Singleton 패턴이란? (feat. 스프링)

happy_life 2022. 4. 20. 17:42

목차

  1. 정의
  2. 사용 배경 
  3. 싱글톤 코드 예제
  4. 싱글톤 코드 특징
  5. 싱글톤 코드 단점
  6. 스프링에서의 싱글톤 지원

 

1. 정의

-말그대로 하나의 객체만을 생성해 이후 호출되는 곳에서는 생성된 객체를 반환하여 프로그램 전반에서 하나의 객체만 사용하게 하는 패턴

 

2. 사용 배경

 -단순한 일을 하는 어떤 클래스가 있다고 가정해봅시다. 사용자는 이를 사용하기위해 객체를 만들 것입니다. 하지만 사용자가 만명이라면?? 만 개의 객체가 메모리에 할당되어야합니다. 이런식으로의 메모리 사용 낭비를 막기위해 싱글톤 패턴을 사용합니다. static 으로 메모리에 한번 올려놓고 하나를 돌려쓰는 것입니다.

 

3. 싱글톤 코드 예제

package hello.core.singleton;

public class SingletonService {

    private static final SingletonService instance = new SingletonService();

    public static SingletonService getInstance() {
        return instance;
    }

   private SingletonService() {
   }

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

 

4. 싱글톤 코드 특징

 

1. 생성자를 private으로 하여 다른 클래스에서 객체를 생성하지 못하게 막음

private SingletonService() {
   }

2. method 를 public 으로 하여 미리 생성해 둔 객체를 외부에서 참조할 수 있게함.

public static SingletonService getInstance() {
        return instance;
    }

 

 

Q) 메소드에 static은 왜 붙나요?

메소드 앞에 static을 붙이면 객체 생성없이 클래스를 통해 메서드를 직접 호출할 수 있습니다.

앞서 말한 것처럼 싱글톤 패턴은 외부에서 객체 생성을 할 수 없게 생성자가 private으로 되어있습니다. 만약 static이 없다면 외부에서는 객체를 생성하지 못하기 때문에 메소드 또한 사용하지 못하게 됩니다. 그러면 instance 를 return 받을 방법이 없게 됩니다. 따라서 static을 붙이게 됩니다.

 

 

5. 싱글톤 패턴의 단점

1.private 생성자를 갖고 있어 상속이 불가능하다.

-상속을 통해 다형성을 적용하기 위해서는 다른 기본 생성자가 필요하므로 객체 지향의 장점을 적용할 수 없다. 또한 싱글톤을 구현하기 위해서는 객체지향적이지 못한 static 필드와 static 메소드를 사용해야 한다.

 

2. 객체지향의 SOLID 원칙을 지키지 못한다.

Singleton은 구현에 의존하고 있다.

 

3.MultiThread 환경에서 문제가 발생한다.

-두 개 이상의 스레드가 인스턴스를 획득하기 위해 접근하는 과정에서 값이 달라질 수 있다.

예시 코드

- 상황: 사용자1이 10000원을 주문하고, 사용자 2가 20000원을 주문 한 경우, price는 10000원이 아닌 20000원이 나온다.

  이유: 같은 객체를 공유하고 있기 때문


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;
    }
}
class StatefulServiceTest {
    
    @Test
    void statefulServiceSingleton() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        //ThreadA: 사용자1 10,000원 주문
        statefulService1.order("userA", 10000);
        //ThreadB: 사용자2 10,000원 주문
        statefulService2.order("userB", 20000);

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

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

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

 

결론: 공유 필드 조심해야함. 스프링 빈은 항상 무상태(stateless) 로 설계해야 함.

해결방안: 지역변수 등 활용하기

 

해결 코드

public class StatefulService {

//    private int price; // 상태를 유지하는 필드

    public int order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);
        return price; // 여기가 문제
    }
class StatefulServiceTest {
    
    @Test
    void statefulServiceSingleton() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        //ThreadA: 사용자1 주문 금액 조회
        int userAPrice = statefulService1.order("userA", 10000);;
        int userBPrice = statefulService2.order("userB", 20000);;
        System.out.println("userAPrice = " + userAPrice);
        System.out.println("userBPrice = " + userBPrice);

        assertThat(userAPrice).isNotEqualTo(userBPrice);

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

 

 

6. 스프링에서의 싱글톤 지원

	@Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer() {
//        AppConfig appConfig = new AppConfig();
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        //1.조회: 호출때마다 객체 생성
        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);

        //참조값이 다른 것을 확인
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);
        //memberService1 != memberService2
        assertThat(memberService1).isSameAs(memberService2);
    }

출력값

따로 싱글톤 관련 코드들을 짜지 않았지만, 호출을 할 때 싱글톤 방식이 적용된 객체가 튀어나오는 것을 알 수 있다.