Spring 과 Singleton

namkun·2022년 3월 10일
0

Spring

목록 보기
5/18

해당 내용은 '스프링 입문을 위한 자바 객체 지향의 원리와 이해'와 인프런 김영한님의 '스프링 핵심 원리 - 기본편' 강의를 참고하였습니다.


스프링은 애초에 기업용 온라인 서비스를 지원하기 위해서 탄생했다.

그리고 이러한 온라인 서비스는 여러명의 고객이 한번에 접속해서 사용하는 법이다.

그런데 이러한 온라인 서비스에서 고객의 요청이 올때마다 매번 인스턴스를 생성해서 사용한다면...어떨 것 같은가?

소규모의 서비스라면 모를까 매번그런다면 자원의 낭비가 계속이뤄질 것이고, 이는 서비스를 운영하는 입장에서는 굉장히 별로인 상황이다.

싱글톤 패턴은 인스턴스를 하나만 만들어 재사용하게 하는 패턴이다.

그러니 객체의 인스턴스는 2개 이상이 생성되는걸 막아야 한다.

그러기위한 조건은 다음과 같다.

  • new를 실행할 수 없도록 생성자에 private 접근 제어자를 지정
  • 유일한 단일 객체를 반환할 수 있는 정적 메서드가 필요
  • 유일한 단일 객체를 참조할 정적 참조 변수가 필요

구현해보자.

SingletonServcie.java

public class SingletonService{
    // 유일한 단일 객체를 참조할 정적 참조 변수가 필요
    private static final SingletonService instance = new SingletonService();

    // 유일한 단일 객체를 반환할 수 있는 정적 메서드가 필요
    public static SingletonService getInstance() {
        return instance;
    }

    // new를 실행할 수 없도록 생성자에 private 접근 제어자를 지정
    private SingletonService() {
    }
}

자 3가지 조건을 모두 만족시켰고, 이걸 외부에서 한번 호출해보자.

Client.java

public class Client{
    // 에러나는 코드
    // SingletonService service = new Singleton();

   	SingletonService service1 = Singleton.getInstance();
 	SingletonService service2 = Singleton.getInstance();
    
    System.out.println("singletonService1 = " + singletonService1);
    System.out.println("singletonService2 = " + singletonService2);
}

자 위의 코드를 보면 new 를 통한 인스턴스 생성은 private 접근 제어자 때문에 막혀있음을 알 수 있다.

저렇게 해서 나온 결과를 보면

singletonService1 = singleton.SingletonService@5fc11620
singletonService2 = singleton.SingletonService@5fc11620

동일한 인스턴스를 가르키고 있음을 알 수 있다.

이러한 기능을 스프링에서는 스프링 컨테이너가 대신 해주고 있다.

우리는 스프링에서 굳이 우리가 싱글톤 패턴을 구현하지 않아도 알아서 객체 인스턴스를 싱글톤으로 관리한다.

전에 우리가 공부했던 걸 생각해보자.

스프링은 스프링컨테이너에 config 클래스를 읽어서 @Bean 어노테이션이 붙어있는 것들에 대해 모두 컨테이너 안에 스프링 빈으로 등록한다.

우리는 이를 통해서 등록된 bean을 사용하였고, 이는 우리가 방금 봤던 것 처럼 이미 만들어진 객체 (bean)을 가져다 사용한 것과 다름 없다.

궁금하다면 직접 코드를 짜보자. 전의 자동차 예제때 사용했던 ApplicationConfig를 사용하겠다.

Test.java

public class Test{
        public static void main(String[] args) {
            ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ApplicationConfig.class);
        Car car1 = applicationContext.getBean("carFuelTank", Car.class);
    	Car car2 = applicationContest.getBean("carFuelTank", car.class);
    
    	System.out.println("car1 = " + car1);
        System.out.println("car2 = " + car2);
        }
}

결과

car1 = hello.core.car.car.CarImpl@353a797c
car2 = hello.core.car.car.CarImpl@353a797c

동일하다.

싱글톤 방식의 주의점

객체 인스턴스를 한개만 생성해서 공유해서 사용하는 싱클톤 패턴도 물론 만능은 아니며 주의해야할 점이 있다.

이는 여러 곳에서 같은 객체 인스턴스를 공유해서 사용하기에 해당 객체는 상태를 유지하게 설계하면 안된다.

쉽게 설명하면 싱글톤 객체를 한 클라이언트가 사용하고 그곳에 특정 값을 지정해두었다.

그러면 다른 클라이언트들은 지정해둔 그 값을 공유해서 사용하게 된다. 물론 그 전 클라이언트가 값을 지정하였으니, 다른 클라이언트도 값을 지정할 수 있다.

사용하다가 맨 처음 사용했던 클라이언트가 지정했던 값을 되찾으려고 한 순간 돌아오는 값은 다른 클라이언트가 지정했던 값일 수도 있게 되는 것이다.

그렇기에 싱글톤 객체는 무상태 (Stateless) 로 설계해야한다.

그러기 위한 조건은 다음과 같다.

  • 특정 클라이언트에 의존적인 필드가 존재하지 않아야한다.
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 없어야한다.
  • 가급적 읽기(Read-Only)만 가능해야 한다.
  • 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.

스프링의 Bean의 필드에 공유값을 설정하면...정말 큰 일이 발생할 수 있다.

다음 코드를 보자.

StateFullService.java

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

    public void join(String name, int passwd){
        System.out.println("name = " + name);
        System.out.println("passwd = " + passwd);
        this.passwd = passwd; // 상태를 지정
    }

    public int getPasswd(){
        return passwd;
    }
}

회원가입 하는 하나의 서비스를 만들었다. 이걸 이제 외부에서 호출해서 사용해보자.

StateFullTest.java

class StateFullServiceTest {

    @Test
    void statefullServiceSingleton(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StateFullService stateFullService1 = ac.getBean(StateFullService.class);
        StateFullService stateFullService2 = ac.getBean(StateFullService.class);

        stateFullService1.join("user A", 12345);
        stateFullService2.join("user B", 67890);

        System.out.println("stateFullService1.getPasswd() = " + stateFullService1.getPasswd());
        System.out.println("stateFullService2.getPasswd() = " + stateFullService2.getPasswd());
    }

    static class TestConfig{
        @Bean
        public StateFullService stateFullService(){
            return new StateFullService();
        }
    }

}

결과

name = user A
price = 12345
name = user B
price = 67890
stateFullService1.getPasswd() = 67890
stateFullService2.getPasswd() = 67890

가장 마지막에 값을 지정한 user B 의 비밀번호로 통일되어버린 상황이다.

실제 서비스에서 이렇게 사용했다간...정말 큰 일이 발생할 것이다.

공유 필드는 정말정말 조심해야한다!

스프링 빈은 항상 무상태(stateless)로 설계하자.

profile
개발하는 중국학과 사람

0개의 댓글