본문 바로가기
Framework & Lib & API/스프링

[스프링 1-4] 스프링 IOC 컨테이너, 싱글톤 레지스트리 (feat. Kotlin)

by 코딩공장공장장 2024. 2. 4.

이전 포스팅 동일타입 빈이 여러개인 경우에 이어

 

이번 포스팅에서는 스프링의 IOC Context와 싱글톤 레지스트리에 대해 알아보겠다.

 

목차

스프링의 IOC Context는 스프링 빈을 생성 관리하는 컨텍스트이다.

 

스프링 빈이란 스프링에서 생성하고 제어권을 가지고 관리하는 객체를 말한다.

 

즉, 스프링 IOC Context가 빈을 생성하고 DI 객체를 결정하고 DI를  하는 것이다.

 

우선 스프링 IOC Context에서 접근하여 스프링의 모든 빈이 등록되어있는지 확인하겠다.

 

@SpringBootTest
class IocContextTest {
    @Autowired
    private lateinit var context: ApplicationContext

    @Test
    fun `스프링 IOC Context 접근`() {
        val beanNameArr = context.beanDefinitionNames
        for (beanName in beanNameArr) println(beanName)
    }
}

 

출력값

더보기

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
org.springframework.boot.test.mock.mockito.MockitoPostProcessor$SpyPostProcessor
org.springframework.boot.test.mock.mockito.MockitoPostProcessor
springStudyApplication
org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory
ACustomerInfoExtractDI
appConfig
beanNameDuplicate
beanTypeDuplicateImpl
beanTypeDuplicateImpl2
beefPatty
chickenPatty
circularReferenceA
circularReferenceB

스프링에서 기본으로 제공해주는 객체들과 내가 직접 등록한 빈들이 출력되는 것을 확인할 수 있다.

 

특정 빈을 IOC Context에서 접근할 수 있다.

interface Patty {
    fun price(): Int
}

@Component
class BeefPatty() : Patty {
    override fun price(): Int {
        return 2000
    }
}

@Component
class ChickenPatty() : Patty {
    override fun price(): Int {
        return 1500
    }
}

@Component
class PorkPatty() : Patty {
    override fun price(): Int {
        return 1800
    }
}

 

위와 같이 빈을 등록한 후 컨텍스트를 통해 빈에 접근해보자.

 

빈 타입으로 접근한 경우

 @Test
fun `스프링 IOC Context 특정 빈 접근`() {
    val beanNameArr = context.getBeansOfType(Patty::class.java)
    for (beanName in beanNameArr) println(beanName)
}

 

출력결과

더보기

beefPatty=com.yojic.springstudy.toby.chap1.di.BeefPatty@506aa618
chickenPatty=com.yojic.springstudy.toby.chap1.di.ChickenPatty@4b6b5352
porkPatty=com.yojic.springstudy.toby.chap1.di.PorkPatty@1d6713dd

 

빈 이름으로 접근한 경우

@Test
fun `스프링 IOC Context 빈 이름으로 접근`() {
   val beefPatty = context.getBean("beefPatty")
   println(beefPatty)
}

 

출력결과 

더보기

com.yojic.springstudy.toby.chap1.di.BeefPatty@506aa618

빈 타입으로 접근한 경우 해당 빈의 모든 구현체에 접근할 수 있고, 빈 이름으로 접근한 경우 해당 빈에 바로 접근할 수 있다. 

 

(@Autowired의 DI 대상을 상위 타입으로 설정하고 해당 구현체 빈이 여러개인 경우 DI 대상의 우선순위를 결정하는 방식에 대해서는 이전 포스팅에서 다뤘으니 참고 바란다.)

 

간단하게 스프링 IOC Context에서 스프링 빈을 관리해주는 것에 대해서는 직접 구동을 통해 알아보았으니 IOC Context의 상속관계에 대해 알아보고 IOC Context의 역할에 대해 알아보겠다.

 

 

스프링 IOC Context

IOC Context는 일반적으로 ApplicationContext라고도 불린다. 

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
		MessageSource, ApplicationEventPublisher, ResourcePatternResolver

 

ApplicationContext의 상속관계는 위와 같다.

 

가장 중요한 것은 BeanFactory이다.

 

하지만 하나씩 알아보겠다. 

 

  • EnvironmentCapable
    이 인터페이스는 Environment getEnvironment(); 메서드로만 이루어진 인터페이스이다.
    Environment 인터페이스는 profileproperties정보를 나타내는 인터페이스이다. 
    즉, 구현체에서 profile정보과 application.properties 또는 application.yml 정보를 담고 스프링에서 이를 활용할 수 있다.

  • ListableBeanFactory
    위 인터페이스는 BeanFactory 인터페이스를 상속 받고 있다.
    인터페이스 정의서에서 BeanFactory와 달리 모든 빈을 낱낱이 셀 수 있다고 설명하고 있다.
    그래서인지 메서드에 등록된 빈의 갯수를 알려주는 int getBeanDefinitionCount(); 메서드나 모든 빈의 이름을 배열로 알려주는 String[] getBeanDefinitionNames();와 같은 메서드가 있다. 이 두 메서드는 BeanFactory에 정의된 메서드와 달리 메서드의 아규먼트가 없다. BeanFactory의 모든 메서드는 아규먼트가 있고 빈의 이름이나 타입을 알고 있어야만 아규먼트에 넣어 빈을 가져올 수 있지만 Listable은 모든 빈을 출력해볼 수 있다. Factory 내에서 정렬을 통해 빈을 관리하는 인터페이스인 것으로 보인다. (인터페이스는 직접 문서나 IDE를 통해 확인하기 바란다. 내용이 길어져서 추가하지 않겠다.)

  • HierarchicalBeanFactory
    BeanFactory에도 계층 구조를 만들 수 있다. 부모-자식 관계를 만드는 것인데 객체간의 상속개념이 아니라 그냥 자식 계층에서 부모 계층 빈 팩토리 설정을 덮어쓰는 것으로 생각하면 될 것이다.
     
    예제를 통해 보겠다.
@Configuration
class ParentConfig {
    @Bean
    fun parent() = Person("parent")

    @Bean
    fun overrideTest() = Person("is overridden?")
}

@Configuration
class ChildConfig {
    @Bean
    fun child() = Person("child")

    @Bean
    fun overrideTest() = Person("Yes, it is overridden")
}

 

위와 같이 설정하고

    @Test
    fun `BeanFactory 계층 구조 확인`() {
        // config 계층 설정
        val parentContext = AnnotationConfigApplicationContext(ParentConfig::class.java)
        val childContext = AnnotationConfigApplicationContext()
        childContext.parent = parentContext
        childContext.register(ChildConfig::class.java)
        childContext.refresh()

        val parent = childContext.getBean("parent", Person::class.java)
        val child = childContext.getBean("child", Person::class.java)
        val overrideTest = childContext.getBean("overrideTest", Person::class.java)
        
        println(parent.name)
        println(child.name)
        println(overrideTest.name)

        childContext.close()
        parentContext.close()
    }

 

출력결과

parent
child
Yes, it is overridden

 

AnnotationConfigApplicationContext는 HierarchicalBeanFactory를 구현한 구현체이다. 

위와 같은 config파일을 사용하는 빈팩토리를 정의하고 계층구조(부모-자식 관계)를 설정하였다.

테스트 코드를 보면 childContext에서 parentContext에 정의한 빈을 가져온걸 확인할 수 있고 동일한 빈이름을 가진 경우 childContext에서 정의한 설정으로 빈이 관리되는것을 알 수 있다.

빈 팩토리의 계층구조를 설정하면 중복되는 부분은 자식 빈팩토리 설정으로 덮어쓰기 하여 빈을 관리한다.

 

AnnotationConfigApplicationContext 를 직접 사용하여 DI를 하는게 아니라면 계층구조를 사용할 일을 거의 없어 보인다.
@Autowired를 통해 DI를 하게 된다면 이전 포스팅에서 얘기한 것 처럼 빈 타입, 이름 중복으로 인한 충돌만 조심하면 될 것 같다.

 

  • BeanFactory
    ListableBeanFactory와 HierarchicalBeanFactory가 상속하는 인터페이스가 BeanFactory이다. 
    대부분의 메서드가 getBean과 관련된 메서드로 대부분이 이루어져있다.

    BeanFactory 구현체 중 가장 대표적인 AbstractAutowireCapableBeanFactory은 getBean을 통해 빈팩토리에 빈이 존재하면 해당 빈을 가져오고 존재하지 않는다면 빈을 생성한다.
    (스프링은 BeanDefinition에 빈의 정의가 있는데 싱글톤 레지스트리에 실제 빈이 존재하지 않는다면 빈을 생성한다.
    BeanDefinition과 싱글톤 레지스트리는 완전히 분리되어있고 둘다 빈의 식별자인 빈 이름을 가지고 있기에 완전히 분리된 두곳을 참조할 수 있다.)
@Override
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
		throws BeanCreationException {
    ...
    
	try {
		Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
		if (bean != null) {
			return bean;
		}
	}
	catch (Throwable ex) {
		throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName,
				"BeanPostProcessor before instantiation of bean failed", ex);
	}

     ...
	try {
		Object beanInstance = doCreateBean(beanName, mbdToUse, args);
		if (logger.isTraceEnabled()) {
			logger.trace("Finished creating instance of bean '" + beanName + "'");
		}
		return beanInstance;
	}
	catch (BeanCreationException | ImplicitlyAppearedSingletonException ex) {
		throw ex;
	}
	catch (Throwable ex) {
		throw new BeanCreationException(
				mbdToUse.getResourceDescription(), beanName, "Unexpected exception during bean creation", ex);
	}
}

 

다른 부분은 생략하고 resolveBeforeInstantiation쪽만 보겠다. 이 메서드를 통해 빈이 이미 초기화 되었는지 확인하고
초기화되었다면 해당 빈을 리턴한다. 초기화되어있지 않다면 doCreate 메서드를 통해 빈을 생성한다.

위와 같은 방식으로 빈을 생성한다.

 

물론 스프링 빈은 디폴트 방식이 싱글톤이고 아래와 같은 관리 방식이 있다.
- @Scope("prototype") : 빈 요청마다 새롭게 만듦

- @Scope("request") : HTTP 요청마다 빈 인스턴스 생성(웹환경)

- @Scope("session") : HTTP 세션마다 빈 인스턴스 생성(웹환경)
위와 같이 어노테이션을 @Component나 @Service, @Bean과 같은 스프링 빈 등록 어노테이션에 함께 붙이면 위 적용을 사용할 수 있다.

 

  • MessageSource
    다국어 처리할 메시지를 만들어주는 getMessage 메서드로 이루어져있는 인터페이스이다

    예제를 통해 알아보자.

messages.properties

name=Hello, {0} nice to meet you

 

messages_ko_KR.properties

name=Hello, {0} nice to meet you

 

위와 같이 파일을 만들고 

@Autowired
lateinit var messageSource: MessageSource

@Test
fun `메시지 테스트`() {
    Locale.setDefault(Locale("en", "US"))
    println(messageSource.getMessage("name", arrayOf("tony"), Locale.getDefault()))
    println(messageSource.getMessage("name", arrayOf("토니"), Locale.KOREA))
}

 

테스트코드를 작성하면

Hello, tony nice to meet you
안녕, 토니 만나서 반가워

 

이와 같이 LOCAL에 따라 다국적 메시지를 출력할 수 있다.

운영환경이라면 서버 Local 설정을 그대로 따라갈 것이다.

 

  • ApplicationEventPublisher
    이벤트 퍼블리시와 리스너를 사용할 수 있게 하는 인터페이스이다. 
    publishEvent(Object event)와 같은 메서드로 이루어져있는데 이 메서드는 event객체를 인자로 이벤트를 발행하면 
    event객체를 메서드의 인자값으로 가지고 있는 모든 @EventListener에게 해당 event객체가 전달된다.
    ApplicationEventPublisher의 구현체 또한 빈으로 등록되기에 DI를 통해 주입받을 수 있다.

    예제를 통해 알아보자.
class AppleEvent(
    val name: String,
)

@Component
class FruitEventListener {
    @Order(1)
    @Async
    @EventListener
    fun handleAppleEvent(apple: AppleEvent) {
        println("과일의 한 종류인 ${apple.name} 이벤트 발행")
    }
}

@Component
class AppleEventListener {
    @Order(2)
    @Async
    @EventListener
    fun handleAppleEvent(apple: AppleEvent) {
        println("사과의 한 종류인 ${apple.name} 이벤트 발행")
    }
}

 

이와 같이 event 객체와 @EventListener 어노테이션을 붙여 eventListener를 구성하고 

Config 파일에 @EnableAsync 어노테이션을 붙여주자. 비동기로 이벤트를 전달할 수 있게 해준다.

위 소스의 @Order는 EventListener의 순서를 결정할 수 있게 한다.

@SpringBootTest
class ObserverTest {

    @Autowired
    lateinit var eventPublisher: ApplicationEventPublisher

    @Test
    fun `이벤트 발행-구독 옵저버 패턴 테스트`() {
        eventPublisher.publishEvent(AppleEvent("청송사과"))
    }
}

 

출력결과

과일의 한 종류인 청송사과 이벤트 발행
사과의 한 종류인 청송사과 이벤트 발행

 

EventPublisher와 EventListener를 통한 이벤트 방식은 구독-발행 방식으로 잘 알려져 있는 옵저버 패턴으로 EventListener들이 event 객체의 상태 변화를 관찰하며 EventPublisher에 의한 Event 발행시 모든 Listener들이 이벤트를 전달받는 방식이다.  이벤트 발행은 모듈간의 의존성이 없는 경우 Event를 발행하여 각 모듈에서 작업을 처리하는데 용이하다.

  • ResourcePatternResolver
    ResourcePatternResolver 는 ResourceLoader를 상속받은 인터페이스이다. resource 정보를 제공해주는 인터페이스이고 ResourcePatternResolver는 배열로 전달해주는 인터페이스 하나 추가된 것이다. resouce는 파일로 생각하면 된다. 

@SpringBootTest
class ResourcesTest {

    @Autowired
    lateinit var resourcePatternResolver: ResourcePatternResolver

    @Test
    fun `리소스 파일 추출`() {
        val pattern = "classpath*:com/yojic/springstudy/toby/chap1/observer/**"
        val resources: Array<Resource> = resourcePatternResolver.getResources(pattern)

        for (resource in resources) {
            println(resource.filename)
        }
    }
}

 

위와 같이 접근하면 해당 패키지 하위의 모든 파일을 출력한다.

ApplicationContext는 위와 같이 BeanFactory 이외에도 다양한 기능을 제공한다.
하지만 가장 중요한 것은 빈의 생성과 관리를 제공하는 BeanFactory이다.

 

인터페이스인 ApplicationContext에 대해서만 알아보았는데 구현체를 통해 상속관계를 보겠다.

 

 

다른 부분은 제쳐두고 ApplicationContext와 관련된 상속관계의 구현체만 보겠다.

 

추상클래스의 가장 상위에는 AbstractApplicationContext가 있고 구현체의 가장 상위는 GenericApplicationContext가 있다.

 

물론 위의 그림에 나와있지 않은 구현체들이 훨씬 많이 존재한다.

 

AbstractApplicationContext와 GenericApplicationContext를 보면 흥미로운 부분이 있다.

 

AbstractApplicationContext는 상위 인터페이스의 대다수의 추상메서드를 오버라이딩이 했음에도 불구하고 

 

MessageSource, Environment, ResourcePatternResolve 등등을 속성으로 가지고 있으며

 

GenericApplicationContext는 BeanFactory를 속성으로 가지고 있다.

 

상속을 통해 추상메서드를 구현을 하였기에 구현 메서드를 사용할 수 있음에도 불구하고 상위 타입의 구현체들을 속성으로도 포함하고 있다.

 

이는 런타임 환경에서 구현체를 유연하게 바꾸며 사용할 수 있음을 나타낸다.

 

무조건적으로 오버라이딩한 메서드만 사용하는 것이 아니라 속성으로 정의한 객체들을 가져와 다양한 구현체들로 사용할 수 있다. 

 

is-a관계인 상속의 강한 결합을 has-a관계인 합성을 통해 좀 더 확장성 있게 다양한 구현체를 사용할 수 있도록 지원하는 것이다.

아래의 테스트코드를 사용하여 스프링부트의 디폴트 context가 무엇인지 확인하고 빈이 어떻게 생성되고 등록되어 DI 되는지 확인해보겠다.

 

@SpringBootTest
class IocContextTest {
    @Autowired
    private lateinit var context: ApplicationContext

    @Test
    fun `스프링 IOC Context 구현체 확인`() {
        println(context::class.qualifiedName)
    }
}

 

출력결과

org.springframework.context.annotation.AnnotationConfigApplicationContext

 

 

AnnotationConfigApplicationContext의 빈 등록 과정

1. 빈 스캔

public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry {

	...

	private final ClassPathBeanDefinitionScanner scanner;

	...

}

 

AnnotationConfigApplicationContext는 ClassPathBeanDefinitionScanner를 속성으로 가지고 있는데 이 객체가 componentscan(디폴트는 부트스트랩 클래스 하위)에 지정한 범위에 존재하는 @Component, @Controller, @Service, @Repository와 같이 어노테이션이 붙은 클래스를 스캔한다.

 

public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {
	
    ...
    
	protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
		Assert.notEmpty(basePackages, "At least one base package must be specified");
		Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
		for (String basePackage : basePackages) {
			Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
			for (BeanDefinition candidate : candidates) {
				ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
				candidate.setScope(scopeMetadata.getScopeName());
				String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
				if (candidate instanceof AbstractBeanDefinition abstractBeanDefinition) {
					postProcessBeanDefinition(abstractBeanDefinition, beanName);
				}
				if (candidate instanceof AnnotatedBeanDefinition annotatedBeanDefinition) {
					AnnotationConfigUtils.processCommonDefinitionAnnotations(annotatedBeanDefinition);
				}
				if (checkCandidate(beanName, candidate)) {
					BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
					definitionHolder =
							AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
					beanDefinitions.add(definitionHolder);
					registerBeanDefinition(definitionHolder, this.registry);
				}
			}
		}
		return beanDefinitions;
	}
    
    ...
    
}

 

위의 doScan 메서드를 통해 basePackages 범위 내의 빈 후보들을 찾고 빈 정보를 BeanDefinition 집합을 만들어낸다.

 

BeanDefinition에는 스코프와 빈 클래스 타입, 의존관계에 있는 빈 정보, primary 여부를 담고 있다. 

 

무엇보다 중요한 건 '의존관계에 있는 빈 정보' 이다. 이를 통해 의존관계가 설정될 수 있다.

 

최종적으로는 BeanDefinitionHolder를 리턴하는데 BeanDefinition의 래퍼객체로 생각하면 되고, 빈 이름과  alias(별칭) 정보를 함께 가지고 있다.

 

 

2. 빈의 등록

public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
    
    ...
    
    private final DefaultListableBeanFactory beanFactory;

    ...
    
}

 

AnnotationConfigApplicationContext의 상위 타입이 GenericApplicationContext이고 GenericApplicationContext에 DefaultListableBeanFactory타입의 빈 팩토리를 속성으로 가지고 있다. 이전에 빈팩토리를 설명하며 createBean을 통해 빈을 생성했던 AbstractAutowireCapableBeanFactory의 구현체가 DefaultListableBeanFactory이다. 

 

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
		implements AutowireCapableBeanFactory {
     
     ...
     
     @Override
     protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
       throws BeanCreationException {
          ...
     }
     ...
     
}

 

따라서 이전에 설명했던 createBean을 통해  beanName과 beanDefinition의 빈정보를 바탕으로 빈을 생성한다.

 

 

3. 빈의 주입

BeanDefinition에 빈들의 의존관계가 모두 성립되어있으니 스프링 프로젝트를 구동한다면 빈들이 생성되고 해당 빈들이 의존하고 있는 빈 객체 주소도 주입된다. 사실 빈 주입은 별도로 진행한다기 보다는 빈을 만들면서 의존관계를 모두 설정하여 만드는 것이다.

 

 

싱글톤 레지스트리

우선 싱글톤에 대해서 알아보자.

public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {}

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

 

위의 방식은 생성자를 private으로 막아 외부에서 객체 생성을 못하게 하였다.

 

허나 생성자의 접근지정자를 private으로 선언하게 되는 경우 상속을 사용하지 못한다.

 

그렇다고 protected로 사용하면 싱글톤이 깨질 수 있다.

 

싱글톤 방식의 장점은 객체를 하나만 생성하기 때문에 메모리를 효율적으로 사용할 수 있다.

 

단점은 구현하기 어렵고 동시성 문제를 해결해야한다.

 

private 생성자를 사용하지도 않았고 동시성 문제를 고려하지도 않았는데 어떻게 싱글톤 방식으로 사용될까?

 

우선 동시성 문제는 먼저 해결하고 넘어가자.

 

동시성 문제란 공유자원에 대해 여러 스레드가 동시에 변경시도를 하여 원치않는 결과를 얻는 경우인데

 

스프링 빈은 속성이 없고 메서드로만 이루어져있기에 동시성 문제가 생길 일이 없다.

 

이제 본격적으로 private 생성자를 사용하지 않음에도 싱글톤으로 관리되는 이유를 알아보자.

우선 우리의 IOC Container인 AnnotationConfigApplicationContext의 속성인 DefaultListableBeanFactory가 SingletonBeanRegistry를 상속받고 있음을 알 수 있다.

 

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
	private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
	...
    
	@Override
	public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {
		Assert.notNull(beanName, "Bean name must not be null");
		Assert.notNull(singletonObject, "Singleton object must not be null");
		synchronized (this.singletonObjects) {
			Object oldObject = this.singletonObjects.get(beanName);
			if (oldObject != null) {
				throw new IllegalStateException("Could not register object [" + singletonObject +
						"] under bean name '" + beanName + "': there is already object [" + oldObject + "] bound");
			}
			addSingleton(beanName, singletonObject);
		}
	}
    
	protected void addSingleton(String beanName, Object singletonObject) {
		synchronized (this.singletonObjects) {
			this.singletonObjects.put(beanName, singletonObject);
			this.singletonFactories.remove(beanName);
			this.earlySingletonObjects.remove(beanName);
			this.registeredSingletons.add(beanName);
		}
	}

	@Nullable
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		// Quick check for existing instance without full singleton lock
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			singletonObject = this.earlySingletonObjects.get(beanName);
			if (singletonObject == null && allowEarlyReference) {
				synchronized (this.singletonObjects) {
					// Consistent creation of early reference within full singleton lock
					singletonObject = this.singletonObjects.get(beanName);
					if (singletonObject == null) {
						singletonObject = this.earlySingletonObjects.get(beanName);
						if (singletonObject == null) {
							ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
							if (singletonFactory != null) {
								singletonObject = singletonFactory.getObject();
								this.earlySingletonObjects.put(beanName, singletonObject);
								this.singletonFactories.remove(beanName);
							}
						}
					}
				}
			}
		}
		return singletonObject;
	}
    ...
}

 

 

Map타입인 singletonObjects필드에 스프링 빈을 등록하는 것을 볼 수 있다.

 

객체 자체를 Map에 보관하여 key-value로 관리하니 빈으로 등록하는 객체가 private 생성자가 아니여도 싱글톤으로 관리될 수 있다.

 

earlySingletonObjects필드는 아직 생성되지 않았으며 생성되어야 할 빈 정보를 담고 있다.

 

getSingleton 메서드를 보자.

 

첫번째 if문은 빈이 null인지 체크하고 있다. null이 아니라면 빈을 바로 리턴한다.

 

스프링 빈이 존재한다면 바로 리턴하기에 스프링 빈에 대한 접근은 동시에 여러 스레드가 접근할 수 있다.

 

따라서 스프링 빈에는 가변속성이 존재하면 동시성 문제가 발생할 수 있다.

 

if문을 타고 타고 들어가다보면 synchronized 키워드가 존재함을 볼 수 있다.

 

해당 부분을 자세히 살펴보면 earlySingletonObjects필드에 빈 정보를 등록하는 로직에 동기화 처리를 하고 있다.

 

빈이 존재하지 않다면 earlySingletonObjects필드에 등록하여 빈 생성을 요청하는 것인데

 

earlySingletonObjects에 같은 빈이 여러개 등록되면 싱글톤이 아닌 여러개의 빈이 생성될 수 있기에 동기화 처리를 한 것이다.

 

getSingleton메서드에서도 synchronized 키워드가 적용된 로직은 싱글톤으로 생성하기 위한 것이지 스프링 빈에 락을 걸어넣고 하나의 스레드가 빈을 소유하여 사용하고 반환하는 방식이 아니다.

 

실제 빈을 가져오는 로직은 동기화 처리를 하지 않았다. 

 

따라서 스프링 빈을 사용할 때 가변 속성을 주지 않는 것이 중요하다.

 

 

이렇게 해서 IOC Container와 싱글톤 레지스트리에 대해 알아보았다.

 

소스코드를 직접 보면 신기한 것도 많고 추상화와 객체지향적 설계가 참 잘되어있고 이렇게 사용되기도 하구나 싶은 부분이 많다.

보다 자세한 내용에 대해 궁금하다면 상속관계에 있는 인터페이스와 추상클래스, 구체 클래스들을 확인해보고 테스트코드나 디버깅으로 직접 어떻게 동작되는지 확인하는것도 도움이 많이 될 것이다.

반응형