본문 바로가기
공부/springboot

(5) 스프링 컨테이너 - 싱글톤

by 샤샤샤샤 2023. 5. 14.

 스프링은 태생적으로 웹 어플리케이션에 특화되어 있는데, 웹 어플리케이션은 보통 같은 요청이 동시다발적으로 들어오곤 한다.

 만약 100명의 고객이 동일한 요청을 한다고 가정해보자. 그러면 동일한 역할을 하는 객체를 100개를 생성하게 될 것이다. 그런데 그 요청이 모든 고객에게 공통적인 사항이라면 (ex - 공지사항 출력, 판매 상품 출력 등) 100개의 객체를 생성하는 것보다 1개의 객체를 100명이 공유하는게 메모리 측면에서 더 효율적일 것이다. 즉, 싱글톤을 사용해야 한다.

요청에 새로운 객체를 생성해서 반환
요청에 싱글톤 객체를 반환

 순수 싱글톤 만들기

 자바 코드를 사용하지 않고 싱글톤 패턴을 만들수 있다.

 핵심은 아래와 같다.

 1. static키워드를 사용해서 미리 자신의 인스턴스를 만들어두고, final키워드를 더해 재할당이 안되도록 만든다.

 2. 매개변수가 없는 생성자함수를 private로 지정하여 외부에서 new키워드로 생성이 불가능하게 만든다.

 3. 1에서 미리 만들어진 인스턴스에 접근할수 있도록 get함수를 만든다.

 

코드로 보면 아래와 같다.

public class SingletonTest {

    // static 키워드를 통해 프로그램 실행시 자신의 인스턴스 생성
    public static final SingletonTest singletonInstance = new SingletonTest();
    
    // private 상태 메소드는 현재 클래스 내부에서만 사용 가능함으로 외부에서 인스턴스 생성 불가
    private SingletonTest() {
    }
    
    // get 메소드를 통해 조회 가능
    public SingletonTest getSingletonInstance(){
        return singletonInstance;
    }
}

 

그러나 싱글톤 패턴은 안티패턴으로 불릴 정도로 개발자적 관점에서 보면 문제가 많다.

1. 코드가 길어진다.

2. 구현체 클레스에 의존해야 한다.  - DIP위반

3.  DIP위반으로 인해 OCP위반될 가능성이 높다.

4. 내부 속성 변경이 어렵다.

5. 상속하기 어렵다.

6. 테스트하기 어렵다.

 

스프링 싱글톤 컨테이너

 스프링 컨테이너는 위와 같은 싱글톤 패턴의 문제를 모두 해결하면서, 객체를 싱글톤과 같이 관리해주는 싱글톤 레지스트리라는 기능을 제공한다.

 빈으로 등록된 객체를 싱글톤처럼 관리해줘서, 해당 빈을 불러올 시, 그 사용자가 다르더라도 같은 객체를 반환해준다.

 이전 글의 예제를 통해 살펴보자.

public interface Alphabet {
    void className();
}
public class A implements Alphabet {

    @Override
    public void className() {
        System.out.println("A");
    }
}
public class B implements Alphabet {
    @Override
    public void className() {
        System.out.println("B");
    }
}

# 빈 등록

@Configuration
public class Config {
    @Bean
    public Alphabet alphabet(){
        return new B();
    }
}

#싱글톤 확인

	@Test
	void beanSingletonTest(){
		ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
		Alphabet alphabet1 = ac.getBean("alphabet", Alphabet.class);
		Alphabet alphabet2 = ac.getBean("alphabet", Alphabet.class);
		System.out.println("alphabet1 = " + alphabet1);
		System.out.println("alphabet2 = " + alphabet2);
	}

결과:

주소값이 같다.

주소값을 통해 빈으로 등록되면 싱글톤처럼 관리된다는 것을 알 수 있다.

 

싱글톤의 주의사항

 싱글톤 객체는 무상태성(stateless)를 유지해야 한다.

 무상태성을 정리하면 다음과 같다.

 1. 특정 요청자(클라이언트)에 의존적인 필드가 있으면 안된다.

 2. 요청자(클라이언트)가 값을 변경할수 있는 필드가 있으면 안된다.

 3. 가급적 읽기만 가능해야 한다.

 4. 특정 값을 받아야 한다면 필드 대신 공유되지 않는 지역변수, 파라미터 등을 사용해야 한다.

 

이유:

싱글톤으로 관리되는 객체의 경우, 100개의 요청이 들어오더라도 1개의 객체에서 요청이 처리된다. 따라서 만약 사용자가 입력한 값을 필드로 관리하게 된다면, A사용자가 값을 입력하고 B가 뒤이어 값을 입력할시, 덮어씌워져 A의 정보가 날아가는 상황이 발생할수도 있는 것이다.

따라서 싱글톤은 항상 무상태성으로 설계해야만 한다.

 

@Configuration과 CGLIB

아래의 코드를 살펴보자.

public class TowAlphabet {
    private Alphabet a;
    private Alphabet b;

    public TowAlphabet(Alphabet a, Alphabet b) {
        this.a = a;
        this.b = b;
    }
}

# Counfiguration 클래스

@Configuration
public class Config {

    @Bean
    public Alphabet a(){
        System.out.println("A호출됨");
        return new A();
    }

    @Bean
    public Alphabet b(){
        System.out.println("B호출됨");
        return new B();
    }

    @Bean
    public TowAlphabet towAlphabet(){
        System.out.println("towAlphabet호출됨");
        return new TowAlphabet(new A(), new B() );
    }
 }

# 테스트 코드

	@Test
	void cglibTest(){
		ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
		Alphabet a = ac.getBean("a", Alphabet.class);
		Alphabet b = ac.getBean("b", Alphabet.class);
		TowAlphabet towAlphabet = ac.getBean("towAlphabet", TowAlphabet.class);
	}

이 결과는 어떻게 나올까?

일반적인 흐름에서는

A호출됨
B호출됨
towAlphabet호출됨
A호출됨
B호출됨

과 같이 출력되야 할 것이다. 또한 towAlphabet의 필드 변수 a, b와, 기존의 a, b는 다른 주소값을 가진 객체여야 한다.

그러나 실제 결과는 아래와 같다.

A호출됨
B호출됨
towAlphabet호출됨

이 이유는 먼저 생성된 a,b와, towAlphabet의 a,b 모두 같은 주소값을 가진 싱글톤 객체이기 때문이다.

 

이런 일이 벌어지는 이유는 @Configuration에 존재한다.

스프링 빈에 등록된 클래스는, 엄밀히 말하자면 우리가 등록한 그 클래스가 아니다. 

우리가 스프링에 어떤 클래스를 등록할때, 스프링은 자동적으로 해당 클래스를 상속받은 임의의 클래스를 만든다. 이 클래스는 (클래스명) + CGLIB라는 이름으로 만들어지는데 이 코드가 바로 싱글톤을 보장해주는 역할을 수행한다. 이 모든 과정은 바이트 코드 조작을 통해 이뤄지기 때문에 사용자가 직접 CGLIB클래스를 자바 코드로 보거나 조작하는 것은 불가능하다.

아마 이 Config@CGLIB클래스 내부에는 다음과 같은 코드가 저절로 생성되었을 것이다.

    @Bean
    public TowAlphabet towAlphabet(){
 
        ...
        if (A가 이미 스프링 컨테이너에 등록되어 있으면?) {
            return 스프링 컨테이너에서 찾아서 반환;
        } else { //스프링 컨테이너에 없으면
            기존 로직을 호출해서 A를 생성하고 스프링 컨테이너에 등록
            return 반환
        }
        if (B가 이미 스프링 컨테이너에 등록되어 있으면?) {
            return 스프링 컨테이너에서 찾아서 반환;
        } else { //스프링 컨테이너에 없으면
            기존 로직을 호출해서 B를 생성하고 스프링 컨테이너에 등록
            return 반환
        }
        ...

    }
 }

 

 만약 @Configuration 어노테이션을 붙이지 않는다면 CGLIB기술이 사용되지 않고 순수한 Config 클래스가 스프링 빈으로 등록된다. 이런 경우에는 a나 b의 싱글톤이 보장되지 않게된다.