Post

스프링 컨테이너, 빈의 생성 과정

스프링 부트의 자바 빈은 어떻게 생성될까?

스프링 컨테이너에 등록되는 빈은 어떤 방식으로 생성되는지 궁금해졌다.

그래서 한 번 인텔리제이로 코드를 따라가봤다. 아마 틀렸을 수도 있다…

싱글톤이란?

일단 스프링은 기본적으로 싱글톤으로 빈을 생성한다. 그렇다면 싱글톤 패턴이 무엇인지 잠시 알아보자.

생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 이와 같은 디자인 유형을 싱글턴 패턴이라고 한다. 주로 공통된 객체를 여러개 생성해서 사용하는 DBCP(DataBase Connection Pool)와 같은 상황에서 많이 사용된다.

라고 위키백과에 정의되어 있다.

핵심은 하나의 객체만 생성되어야 한다는 것이고, 이를 통해 효율적인 자원 사용을 하겠다는 것이 목표다.

구현 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {
    private static Singleton instance;

    private Singleton(){

    }

    public static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }

        return instance;
    }
} 

가장 단순한 구현법이다.

하지만 여러 스레드가 동시에 접근했을 때 하나의 인스턴스가 생성된다는 명제를 보장할 수 없다는 문제점이 있다.

getInstance() 메소드에 synchronized 와 같은 키워드를 걸어서 해당 상황을 방지할 수는 있으나, 이는 성능 저하라는 또다른 문제가 생기게 된다.

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
    private final static Singleton instance = new Singleton();

    private Singleton(){

    }

    public static Singleton getInstance(){
        return instance;
    }
} 

이를 개선하기 위해 static 멤버의 특성을 이용, JVM이 최초 로딩 때 클래스를 로딩함과 동시에 인스턴스를 만들어 버리는 방법도 존재한다.

하지만 이는 효율적인 자원 활용과는 거리가 존재한다. 해당 인스턴스를 사용 안 할 경우에도 생성되기 때문이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton {
    private static class singleInstanceHolder {
        private static final Singleton INSTACE = new Singleton();
    }

    public static Singleton getInstance(){
        return singleInstanceHolder.INSTANCE;
    }
} 


public enum SingletonEnum {
    INSTANCE;

    public void helloWorld(){

    }
}

가장 유명하면서 많이 사용되는 두 가지 방법이다.

별도의 홀더 클래스를 만들어 인스턴스를 호출할 때 static 변수에 객체를 할당하거나, enum 클래스로 만드는 방법이다.

빈은 어떻게 생성되는가?

1
2
3
4
5
6
7
8
@SpringBootApplication
public class HelloApplication {

    public static void main(String[] args) {
        SpringApplication.run(Hello.class, args) // -> 해당 함수 추적;
    }

}

이제 스프링이 어떻게 컨테이너에 빈을 등록시키는지 본격적으로 알아보자.

필자는 메인 클래스의 SpringApplication.run() 메소드를 중점적으로 추적해가며 코드를 가져왔다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class SpringApplication {

    // .....

    public ConfigurableApplicationContext run(String... args) {
		long startTime = System.nanoTime();
		DefaultBootstrapContext bootstrapContext = createBootstrapContext();
		ConfigurableApplicationContext context = null;
		configureHeadlessProperty();
		SpringApplicationRunListeners listeners = getRunListeners(args); // -> 해당 함수 추적;
		listeners.starting(bootstrapContext, this.mainApplicationClass);
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			context = createApplicationContext();
			context.setApplicationStartup(this.applicationStartup);
			prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
			}
			listeners.started(context, timeTakenToStartup);
			callRunners(context, applicationArguments);
		}

    }


    private SpringApplicationRunListeners getRunListeners(String[] args) {
		Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
		return new SpringApplicationRunListeners(logger,
				getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args), // -> 해당 함수 추적;
				this.applicationStartup);
	}


    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
        ClassLoader classLoader = getClassLoader();
        // Use names and ensure unique to protect against duplicates
        Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
        List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names); // -> 해당 함수 추적;
        AnnotationAwareOrderComparator.sort(instances);
        return instances;
    }

    private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes,
                ClassLoader classLoader, Object[] args, Set<String> names) {
        List<T> instances = new ArrayList<>(names.size());
        for (String name : names) {
            try {
                Class<?> instanceClass = ClassUtils.forName(name, classLoader);
                Assert.isAssignable(type, instanceClass);
                Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
                T instance = (T) BeanUtils.instantiateClass(constructor, args); // -> 해당 함수 추적;
                instances.add(instance);
            }
            catch (Throwable ex) {
                throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, ex);
            }
        }
        return instances;
    }

    // .....
}

먼저 SpringApplication 클래스에서 빈 생성 관련으로 추정되는 코드를 확인해봤다.

BeanUtils라는 클래스에서 관련 작업을 하는 듯 하여 해당 클래스로 이동해봤다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public abstract class BeanUtils {

    // ......

    public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws BeanInstantiationException {
        Assert.notNull(ctor, "Constructor must not be null");
        try {
            ReflectionUtils.makeAccessible(ctor);
            if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass())) {
                return KotlinDelegate.instantiateClass(ctor, args);
            }
            else {
                Class<?>[] parameterTypes = ctor.getParameterTypes();
                Assert.isTrue(args.length <= parameterTypes.length, "Can't specify more arguments than constructor parameters");
                Object[] argsWithDefaultValues = new Object[args.length];
                for (int i = 0 ; i < args.length; i++) {
                    if (args[i] == null) {
                        Class<?> parameterType = parameterTypes[i];
                        argsWithDefaultValues[i] = (parameterType.isPrimitive() ? DEFAULT_TYPE_VALUES.get(parameterType) : null);
                    }
                    else {
                        argsWithDefaultValues[i] = args[i];
                    }
                }
                return ctor.newInstance(argsWithDefaultValues); // -> 해당 함수 추적;
            }
        }
            // ......
    }

    // ......
}


public final class Constructor<T> extends Executable {
    // ......

    public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
        {
        if (!override) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, clazz, modifiers);
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }
}

스프링 부트는 리플렉션을 통해 빈을 생성하고 있는 것을 확인할 수 있었다.

리플렉션은 힙 영역에 로드된 Class 타입의 객체를 이용해 원하는 클래스의 인스턴스를 생성, 인스턴스의 필드와 메소드를 접근 제어자와 상관 없이 사용할 수 있도록 지원하는 API이다.

리플렉션 API를 이용해서 특정 어노테이션이 붙은 요소들을 탐색(@Component, @Service 등등…)하고, 스프링 컨테이너에 관련 빈들을 등록한다.

또한 SpringApplication.run() 함수 쪽을 보면 먼저 빈을 탐색, 등록시킨 후에 의존성 주입 작업이 이루어지고 있다는 것을 추측할 수 있었다.

이제 빈이 어떻게 생성되는지도 보았고, 스프링의 방식과는 약간 차이가 있지만 기본적인 싱글톤이 무엇인지도 살펴보았다. 그렇다면 왜 스프링은 기본적으로 싱글톤을 이용해 빈을 생성하는 것일까?

빈이 싱글톤인 이유

당연히 효율성 때문이다.

스프링이 처음 설계됐던 대규모 엔터프라이즈 서버환경은 서버 하나당 최대로 초당 수십~수백 번씩 브라우저나 여러 시스템으로부터 요청을 받아 처리할 수 있는 높은 성능이 요구되는 환경이었다.

이런 상황에서 매번 new 연산을 이용해 객체를 생성하는 작업은 매우 값비싼 작업이며, 서버에 엄청난 부하가 걸릴 것이다. 이럴 때 스프링 컨테이너에서 미리 정의된 싱글톤 객체를 호출하기만 한다면 훨씬 효율적인 작업 수행이 가능하다.

또한 웹에서 주로 쓰이는 HTTP의 무상태성, 비연결성이라는 특징은 싱글톤을 활용하기에 매우 적절한 환경이기도 하다.

참고

  • 위키백과, 싱글턴 패턴, https://ko.wikipedia.org/wiki/%EC%8B%B1%EA%B8%80%ED%84%B4_%ED%8C%A8%ED%84%B4
  • 큰돌의 터전, 제대로 이해하는 싱글톤패턴 - 실습#1, https://www.youtube.com/watch?v=3rfbnQYOCFA
  • 큰돌의 터전, 제대로 이해하는 싱글톤패턴 - 실습#2, https://www.youtube.com/watch?v=4Sk9dzXgKwo
  • 느리더라도 꾸준하게, [Java] Reflection 개념 및 사용 방법, https://steady-coding.tistory.com/609
  • minwest.log, 스프링은 빈을 왜 싱글톤으로 생성할까?, https://velog.io/@minwest/Spring-%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%80-%EB%B9%88%EC%9D%84-%EC%99%9C-%EC%8B%B1%EA%B8%80%ED%86%A4%EC%9C%BC%EB%A1%9C-%EC%83%9D%EC%84%B1%ED%95%A0%EA%B9%8C
This post is licensed under CC BY 4.0 by the author.

© . Some rights reserved.

Using the Jekyll theme Chirpy