[Spring] 싱글톤 컨테이너

Hoon·2022년 10월 15일

Spring

목록 보기
5/18

싱글톤 패턴 (Singleton Pattern)

싱글톤 패턴: 클래스의 인스턴스가 단 1개만 생성되는 것을 보장하는 디자인 패턴

아래는 싱글톤 패턴을 적용한 코드다.

public class SingletonService {
    // 1. static 영역에 객체를 딱 1개만 생성해둔다.
    private static final SingletonService instance = new SingletonService();
    
    // 2. public으로 열어서 객체 인스터스가 필요하면 이 static 메서드를 통해서만 조회하도록허용한다.
    public static SingletonService getInstance() {
        return instance;
    }
    
    // 3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
    private SingletonService() {
    }
    
    public void logic() {
        System.out.println("싱글톤 객체 로직 호출");
    }
}

위의 코드를 테스트하는 코드는 아래와 같다.

@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
public void singletonServiceTest() {
	// private으로 생성자를 막아두었다. 컴파일 오류가 발생한다.
    // new SingletonService();
        
    // 1. 조회: 호출할 때 마다 같은 객체를 반환
    SingletonService singletonService1 = SingletonService.getInstance();
        
    // 2. 조회: 호출할 때 마다 같은 객체를 반환
    SingletonService singletonService2 = SingletonService.getInstance();
        
    // 참조값이 같은 것을 확인
    System.out.println("singletonService1 = " + singletonService1);
    System.out.println("singletonService2 = " + singletonService2);
        
    // singletonService1 == singletonService2
    assertThat(singletonService1).isSameAs(singletonService2);
    singletonService1.logic();
}

하지만 위의 코드는 문제가 있다.

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.

싱글톤 컨테이너

스프링 컨테이너는 이런 문제들을 해결해준다. 스프링 컨테이너에서 관리하는 bean들은 모두 싱글톤으로 관리된다.

즉 스프링 컨테이너는 싱글톤 컨테이너이기도 하다.

스프링 컨테이너는 bean을 컨테이너에 저장해 놓았다가 사용 요청이 오면 이미 만들어진 객체공유하면서 사용하게 된다.

싱글톤 사용시 주의점

  • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!
  • 가급적 읽기만 가능해야 한다.
  • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, 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;
    }
  
}

// Bean 등록
class TestConfig {
	
    @Bean
    public StatefulService statefulService() {
    	return new StatefulService();
    }
}

위의 코드는 order함수를 쓰면 price의 값이 변할 수 있다. 이렇게 되면 어떤 문제가 있는지 확인해보자.

public class StatefulServiceTest {

    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new
                AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean("statefulService",
                StatefulService.class);
        StatefulService statefulService2 = ac.getBean("statefulService",
                StatefulService.class);
                
        //ThreadA: A사용자 10000원 주문
        statefulService1.order("userA", 10000);
        //ThreadB: B사용자 20000원 주문
        statefulService2.order("userB", 20000);
        //ThreadA: 사용자A 주문 금액 조회
        int price = statefulService1.getPrice();
        //ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
        System.out.println("price = " + price);
        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }
    // StatuefulService Bean 등록
    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}

만약 userA가 order함수를 사용해서 1000원으로 값을 변경했다고 해보자.
그후 userB가 order함수를 사용해서 2000원으로 값을 변경한다.

그리고 userA는 getPrice함수로 값을 확인한다. 우리는 1000원이 나오기를 기대했지만 2000원으로 출력이 된다. 왜냐하면 StatefulService는 싱글톤으로 관리되는 bean이기 때문이다.

그러므로 스프링 빈은 항상 무상태(stateless)로 설계해야 한다.

@Configuration

하지만 @Configuration을 클래스에 쓴다면 이야기가 달라질 것이다.
class hello.core.AppConfig에서 바이트코드를 조작하여
class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70란 임의의 클래스를 만든다.

이런식으로 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다!

덕분에 싱글톤이 보장되는 것이다.

참고 AppConfig@CGLIB는 AppConfig의 자식 타입이므로, AppConfig 타입으로 조회 할 수 있다.

Reference

스프링 핵심 원리 - 기본편 (김영한님)

profile
개발이 즐거워

0개의 댓글