지난 포스팅에서는 메인 메소드에 진입한 순간, 떠있는 쓰레드들의 정보를 살펴보았다. 이번 포스팅부터는 본격적으로 스프링 부트가 도와주는 부팅 과정을 하나하나 따라가볼 것이다.
가장 먼저 메인 메소드에서는 SpringApplication 이라는 public class의 run 메소드에 현재 psvm 메인 메소드를 보유하고 있는 중심이 되는 클래스의 Class 값을 넘긴다. 이때, Step Into 단축키인 F7을 누르면, 단순히 함수를 호출하는 과정임에도 불구하고, 굉장히 긴 시간이 걸리는데 그 이유를 솔직히 정확히는 모르겠지만, 추측을 해보자면 SimpleBoardProjectApplication 클래스의 Class 값을 가져오는데? 걸리는 시간이 아닐까 추측해본다.
위 사진 처럼, SimpleBoardProjectApplication.class에 들어있는 Class의 값을 살펴보면, 정말 장난아니게 많은 정보들이 들어있다. 특히, classLoader 인스턴스에 많은 정보가 들어있는데, 지금 당장 분석하고 싶지는 않아서 일단은 메인 클래스의 Class 값에, 특히 classLoader 인스턴스에 프레임워크와 관련된 정보가 정말 많이 들어있다라고만 생각하고 넘어가겠다.
(추후에 클래스로더를 공부하고 알게된 사실이지만, SimpleBoardProjectApplication.class 에 들어있는 값 자체가 많다기보다는, SimpleBoardProjectApplication.class를 로딩한 시스템 클래스로더에 많은 값이 들어있다고 보는게 맞는 것 같다)
우리의 메인 클래스의 Class 값을 인자로받는 SpringApplication 생성자를 생성한 후, args 값을 인자로하는 run 메소드를 호출하는 모습이다. 함수의 설명을 보면 입력받은 primarySources와 args를 default 세팅으로 로딩하는 Static 메소드이다.
위 생성자는 다음 생성자를 호출한다.
자.. 굉장히 복잡한 부분이다. 먼저 함수 설명부터 살펴보면, SpringApplication 인스턴스를 생성하는 생성자이며 application context가 입력받은 primarySources로부터 빈들을 로딩한다고 한다. 이 인스턴스는 run(String...) 생성자를 호출하기 전에 커스터마이징할 수 있다고한다. 무슨 말인지 감이 오지 않는다. 코드를 보자
전체적인 함수의 흐름을 보면, SpringApplication 인스턴스의 resourceLoader / primarySources / webApplicationType / bootstrapRegistryInitializers / mainApplicationClass 필드를 초기화하며, Initializers와 Listeners를 set한다.
일단 resourceLoader는 null 값이 들어갈 것이며, primarySource 배열은 null이 아니므로, 정상적으로 통과된다. 그 후, primarySource 배열을 리스트로 바꾼다음 해당 리스트를 토대로 링크드해시셋을 생성한다.
WebApplicationType은 열거형 클래스로, 스프링 부트에서 지원하는 웹 어플리케이션의 종류와 관련된 클래스이다.
총 3가지로 나뉘는 듯하다. 웹 어플리케이션이 아니며 임베디드 웹 서버 또한 실행되지 않는 NONE / servlet 베이스의 웹 어플리케이션으로, 임베디드 서블릿 웹 서버가 실행되어야만하는 SERVLET / 리액티브 웹 어플리케이션으로, 임베디드 리액티드 웹 서버가 실행되어야만하는 REACTIVE가 존재한다.
자, 그럼 위에서 살펴봤던, 상수들과 여러 문자열 정보를 바탕으로 위 함수를 분석해보자.
ClassUtils 클래스의 isPresent 함수는 인자로 들어온 문자열의 클래스가 클래스로더에서 땡겨올 수 있는지의 여부를 인자의 classLoader를 통해 확인한다. 만약에 classLoader가 따로 설정되지 않은 null 값이라면, default class loader를 사용한다고한다.
즉, 정리하자면 deduceFromClasspath 함수는, 설정된 클래스 로더에서 DispatcherServlet를 끌어올 수 있는지, reactive.DispatcherHandler를 끌어올 수 있는지 확인한 후, 현재 웹 어플리케이션 타입이 무엇인지를 반환하는 함수라고 볼 수 있다.
BootStrapRegistryInitializers라는 함수형 인터페이스를 구성 타입으로하는, 배열 리스트를 생성한다. 이때, 리스트 생성자의 인자로 희한한 함수를 호출하는 모습이다.
그 후, SpringFactoriesLoader의 static 메소드인 forDefaultResourceLocation 함수를 호출하는데, 인자로 getClassLoader() 함수를 결과 값을 넘긴다.
위 함수는 만약 현재 SpringAppliction 클래스의 resourceLoader가 널이 아니면, 다시 ClassUtils.getDefaultClassLoader() 함수를 호출한다.
위 함수는, 디폴트 ClassLoader를 사용하기 위해 호출되며, 해당 디폴트 클래스로더는 thread context ClassLoader라고한다. thread context ClassLoader가 뭔가하니, 클래스로더는 크게 3가지로 분류된다. 부트스트랩 클래스로더/익스텐션or플랫폼 클래스로더/시스템 클래스로더 순으로, 앞에 있는 클래스로더가 뒤의 부모이다. 이때, 계층 구조로 어떤 클래스 로딩에 대한 요청이 전가되는데, 예를 들어, 만약 시스템 클래스로더에서 어떤 클래스를 로딩해야한다면, 바로 부모인 플랫폼 클래스로더에게 요청을 하며, 부모 클래스로더가 찾는 것에 실패하면 그때 로딩을 시도한다.
이러한 클래스로더의 위임 전략 때문에, 부모 클래스로더가 더 하위의 클래스로더만이 가져올 수 있는 클래스가 필요할때에는 가져올 방법이 없는 문제가 발생한다. 이를 해결하기 위해, 쓰레드를 생성할 때 해당 쓰레드를 생성한 누군가가 ContextThreadLoader를 제공하며, 이 클래스로더를 통해 클래스를 로딩하게 된다. 만약 누군가가 제공하지 않았다면, 쓰레드를 생성한 부모 쓰레드의 컨텍스트 클래스로더를 물려받는다고한다.
즉, 정리하면 getDefaultClassLoader 함수는 제일 먼저 쓰레드에게 부여된 thread context classloader를 찾고, 존재하지 않다면, ClassUtils를 로딩한 클래스로더를 찾는다. 부트스트랩 클래스로더는 자바로 구현된 부분이 아니기에, 자바 객체 참조 값이 존재하지 않아 null을 반환할 수도 있다. 따라서, 만약 ClassUtils.class가 부트스트랩 클래스로더에 의해 로딩되었다면, null을 반환하게되고, 시스템(어플리케이션) 클래스로더를 함수의 반환 값으로 반환한다.
자, 다시 돌아와보자. 클래스로더를 가져온 후, forDefaultResourceLocation 함수를, 가져온 클래스로더를 인자삼아, 호출한다.
함수의 설명을 살펴보면, SpringFactoriesLoader 인스턴스를 생성하는 함수이며, 이때 이 SpringFactoriesLoader는 디폴트로 설정된 FACTORIES_RESOURCE_LOCATION에 설정된 디렉토리에 있는 리소스들을 가져올 때 사용되는 경로이고, 넘겨주는 클래스로더를 통해 로딩 및 인스턴스화한다.
본격적으로 SpringFactoriesLoader를 만드는 작업을 하는 함수이다. 넘겨받은 resourceLocation과 클래스로더를 통해 SpringFactoriesLoader를 만든다.
먼저 resourceLocation의 값와 클래스로더의 값을 확인한다.
그 후, Map 타입 cache의 computeIfAbsent라는 함수를 호출하는데,
만약 현재 맵에, 함수 인자로 들어온, key 값을 키로 갖는 쌍이 존재하지 않는 경우, 인자로 넘어온 익명 객체인 mappingFunction에 존재하는, 람다식으로 넘어온, apply 함수를 실행하여 나온 값을 key-value 쌍 중, value로 설정한다. 그리고 value 값을 반환한다.
cache에, 인자로 넘어온, resourceClassLoader 값을 키로하는 쌍이 존재한다면, 해당 쌍의 value를 반환하고, 그렇지 않다면 새로운 맵을 만들어 resourceClassLoader-새로운 맵의 쌍을 새로 cache에 넣은 후, 새로운 맵을 반환한다. 즉, 현재 loaders에는 텅빈 ConcurrentReferenceHashMap이 들어있다. ConcurrentReferenceHashMap에 대해서 잘 모르지만, 추후에 정리하고 일단은 넘어가겠다.
그 후, 텅 빈 loaders에 똑같이 computeIfAbsent 함수를 호출한다. 위에서 살펴본 과정에 따르면, resourceLocation과 new SpringFactoriesLoader(classLoader, loadFactoriesResource(resourceClassLoader, resourceLocation)) 쌍이 loaders에 삽입될 것이며, 반환 값은 new SpringFactoriesLoader(classLoader, loadFactoriesResource(resourceClassLoader, resourceLocation)) 가 된다. 주목할 점은, cache가 보유하고 있던 쌍 중, value에 들어있던 맵과 현재 loaders의 참조 값이 일치하므로, cache의 구성도 변화가 생긴다.
맵을 반환하는 함수이다. 설정된 classLoader에게 resourceLocation("META/INF/spring.factories")을 인자로 getResources 함수를 호출한다.
name이 널인지 확인한 후, 만약 현재 클래스로더의 부모 클래스로더가 존재한다면, 부모 클래스로더의 getResources함수의 반환 값을 Enumeration<URL> 배열의 첫번째 원소에 집어넣는다. 참고로 Enumeration 인터페이스는 객체에 존재하는 요소들이 연속된 순서를 가지는 것을 추상화한 명세서이다. 이때 요소는 URL이 될 것이다. 만약 부모가 null이라면, 부모는 부트스트랩 클래스로더일 가능성이 있다는 이야기이며, BootLoader 클래스의 findResources 함수를 호출한다.
BootLoader 클래스는, 부트클래스 로더 혹은 Xbootclasspath에 설정된 부트클래스 path에 의해 정의되는 리소스나 패키지를 찾는 클래스라고한다.
- Xbootclasspath란?
https://www.ibm.com/docs/en/sdk-java-technology/8?topic=options-xbootclasspath
AppClassLoader/ PlatformClassLoader/ BootClassLoader 의 부모 클래스인 BuiltInClassLoader 함수의 findResources 함수이다. 이 함수를 호출한 클래스로더의 class path 또는 정의된 모듈에 주어진 이름(현재 META/INF/spring.factories)의 리소스가 존재하는 URL 열거형 클래스를 반환한다.
URL 타입의 리스트를 생성한 후, 현재 이 함수를 실행한 클래스로더의 모듈의 패키지 정보에 META/INF 패키지가 존재하는지 찾는다. 우리가 찾는 META/INF 패키지는 존재하지 않는다. 솔직히 이 부분이 어떻게 동작하는지는 진짜 모르겠다. packageToModule 변수는 맵 타입의 인스턴스를 담고 있으며, 패키지명과 모듈명이 key-value 쌍으로 들어가있다.
대부분 jdk 관련된 모듈과 패키지들이 쌍으로 들어있으며, 외부 프레임워크랑은 큰 연관이 없어보인다.
이번에는 모듈이 아닌 클래스로더에 정의된 class path로부터 주어진 name의 리소스를 얻고자한다. 그렇게 URL 타입의 Enumeration 타입의 e를 정의한 뒤, Enumeration 인터페이스를 구현하는 익명 객체를 반환한다. 이때, 주목할 점은 익명 객체에서 함수 지역 변수인 e를 사용한다는 것이다. 이렇게 될 경우, 익명 객체에 e가 복사되어 암묵적으로 존재한다고한다.
- 익명 객체에서 지역 변수를 사용할 경우? (신뢰도가 떨어지므로 참고만하기)
주어진 name의 리소스를 class path를 통해 찾아주는 함수다. 주목할만한 점은, bootClassLoader와 platformClassLoader는 class path 자체가 존재하지 않는다는 점이다. 내 추측으로는, 아까 jdk는 다 모듈화 되어 로딩되었기 때문에, jdk에 들어있는 클래스를 로딩하는 클래스로더로 리소스에 접근할 때는 module 관련된 것으로 이미 접근할 수 있기에, 굳이 class path 정보까지 담지 않았다고 감히 생각해본다. 반면, AppClassLoader는 jdk 외에도 여러 클래스들을 외부에서 끌어오므로, class path가 존재한다라고 추측해본다.
실제로 AppClassLoader의 class path에는 자바와 관련된 라이브러리는 하나도 없다.
어쨋든, 그렇게 class path에서 주어진 리소스가 존재하는지를 확인하기 위해, ucp.findResources 함수를 호출한다. 여기서 속으면 안되는 점은 해당 함수의 반환 값은 그냥 넘겨준 name과 false를 참조하는 익명 Enumeration 객체를 반환할 뿐이다. 즉, 익명 객체에 name과 false를 복사하여 포함시키고, 인터페이스를 구현한 것 그 이상 그 이하도 아니다. 그리고 그렇게 반환받은 익명 객체를 또 findResource - 2 함수에서 반환하는 익명 객체가 복사해서 가져간다.
결과적으로, tmp 배열의 첫번째 원소에는 부모로부터 받은 CompoundEnumeration 타입의 인스턴스가 들어있을 것이고, 두 번째 원소에는 Enumeration 인터페이스를 구현한 익명 객체를 감싸고 있는 익명 객체가 들어간다. 그래서 다음과 같은 재귀적 형태를 띈다.
참고로, CompoundEnumeration 클래스는 ClassLoader의 이너클래스다. 따라서, BuiltInClassLoader는 ClassLoader의 후손이여서 CompoundEnumeration 타입이 BuiltInClassLoader$1 처럼 표기되는 것 같다.
드디어 urls를 구했다. 자 이제, while 문안으로 들어가보자. urls.hasMoreElements() 함수에 주목하자. urls의 현재 구현 클래스는 CompoundEnumeration 클래스이며, CompoundEnumeration의 hasMoreElements() 함수는 재귀적으로 자신의 가장 깊숙한 Enumeration 익명 객체에 원소가 존재하는지 여부를 물어본다.
CompoundEnumeration 클래스의 요청을 받은 Enumeration 익명 객체 A(B를 복사해서 가지고 있음)가 자신의 안에 있는 익명 객체 B(아까 name과 false를 복사해서 가지고있음)의 hasMoreElements() 함수와 nextElement() 함수를 사용하여, A 익명 객체의 next 필드를 초기화하고있다.
흐름을 정리하자면, urls.hasMoreElements() => CompoundEnumeration 구현체.hasMoreElements() => 익명 객체 A.hasMoreElements() => 익명 객체 B.hasMoreElements() & 익명 객체 B.nextElement() 순으로 호출된다. 익명 객체 B는 생성 될 때, 가지고 있던 name(META/INF/spring.factories) 정보를 활용하여, 현재 ClassPath에서 리소스를 찾아 url 필드를 채우고 리소스 찾기 성공 여부를 반환한다.
urls.nextElement() 함수는 결국 익명 객체 B가 찾은 url을 익명 객체 A에게, 또 urls의 구현체에게 넘겨주는 과정이라고 볼 수 있다.
이후 과정은 모든 spring.factories 에 들어있는 다음과 같은 값들
의 key-value 쌍을 해석해서, result에 저장하고, value 리스트의 중복을 제거하는 과정이라고 볼 수 있을 것 같다. (추측임 귀찮아서 분석안함 ㅠㅠ)
이렇게 생성된 result를 SpringFactoriesLoader의 객체가 자신의 factories 필드로 가져간다.
자! 점점 끝이 보이는 것 같다. 위에서 열심히 생성한 SpringFactoriesLoader 인스턴스의 참조 값을 사용하여, load 함수를 호출한다. 참고로 현재 argumentResolver는 null이다.
먼저 load 함수의 설명부터 살펴보자. "META/INF/spring.factories" 경로로부터 설정된 타입의 팩토리 구현체를 로딩하고 인스턴스화한다고한다. 이때, 설정된 클래스로더와 argument resolver를 사용한다. 참고로, 클래스로더는 아까 new 뭐시기할 때, 설정되었고, argument resolver는 현재 null이다.
아까 우리가 구했던 factories의 모든 key-value 쌍에게, 현재 필요로하는 팩토리 타입과 일치하는 key가 존재하면, value를 반환하고 그렇지 않다면 defaultValue인 빈 리스트를 반환한다. 그렇게 거름망으로 필요한 클래스들만 쫙 뽑아낸 후, 로그 메시지를 trace 레벨로 로거에 저장하는 듯하다. 그 후, 팩토리를 인스턴스화하는 과정에서 예외를 처리할 FailureHandler 인스턴스를 저장한 후, 본격적으로 풀네임으로 저장되어 있는 각 팩토리들을 인스턴스화한다.
먼저, 가져올 클래스의 풀네임과 SpringFactoriesLoader 객체를 생성할 때, 설정했던 클래스로더를 ClassUtils.forName 함수의 인자로 넘긴다. 그 후, Class 값이 우리가 원하는 타입에 속하는지 확인한다.
그렇게 얻은 Class 값을 FactoryInstantiator.forClass 함수로 넘긴다.
Class 값에 일치하는 생성자가 존재하는지 찾은 후, 해당 생성자를 인자로하는 FactoryInstantiator 인스턴스를 생성한다.
생성자와 그 생성자의 클래스의 제어자를 통해 접근 가능한지 확인 후, 정말 필요한 경우에만 생성자를 접근 가능하게 설정한다. 그렇게 하지 않고 무분별하게 setAccessible(true)를 호출하면 불필요한 충돌이 발생할 수 있다고한다. 그 후, FactoryInstantiator의 constructor 필드에 값을 설정한다.
넘겨줄 인자를 resolve한 후, 생성자를 통해 인스턴스를 생성한다.
인스턴스화 한 팩토리 구현체들을 result에 집어넣은 다음 정렬을 한다. 정렬하는 방식은 생략하겠다. (OrderComparator의 compare 함수를 보면 될듯하다)
드디어, SpringApplication의 생성자로 돌아왔다.
SpringApplication 생성자의 남은 부분이다. 이 부분은 그래도 위에서 살펴본 내용들을 하나씩 적용하면 쉽게 헤쳐나갈 수 있을 것 같다.
'들여다보기' 카테고리의 다른 글
채팅 서버 프로젝트 정리 (0) | 2024.04.06 |
---|---|
프로젝트 들여다보기 - 1편 (0) | 2024.03.15 |