S E P H ' S

[Spring] 2. Spring 기초, 핵심 원리 이해 본문

Programing & Coding/Spring

[Spring] 2. Spring 기초, 핵심 원리 이해

yoseph0310 2023. 4. 28. 16:41

오래전에 작성한 Spring에 대한 포스트에 이어서 Spring Framework에 대한 이해를 높이기 위해 다시 포스트를 시작했다. Spring을 사용해서 4개의 프로젝트를 진행했음에도 여전히 깊은 이해는 부족하다고 생각이 들었다. 그래서 Spring에 대해서 더욱 깊게 알아보려고 한다.


Spring의 등장

기본적인 구조와 특징은 간략하게 1. 스프링(Spring)이란? 에서 다루었다. 지난 글을 가볍게 읽고 넘어온다면 Spring에 대한 이해가 더욱 잘될 것이라 생각된다. 스프링이 등장하기 전, 자바 개발자들은 EJB(Enterprise Java Beans)를 주 프레임워크로 사용했다. 2002년에 로드 존슨이 출판한 도서 "Expert One-on-One J2EE Desing and Development"에 선보인 코드가 Spring의 근간이 되었다고 한다. 이 도서를 읽은 개발자들이 로드 존슨의 허가를 받고 프레임워크로 발전시켰고 2003년 6월 Spring이 Apache 2.0 라이선스로 공개됐다.


Spring Framework의 특징

가장 큰 특징으로는 IoC(제어의 역전 : Inversion of Control), DI(의존성 주입 : Dependency Injection) 이 언급되는데 이외의 특징들도 알아보도록 하자.

 

1. 경량 컨테이너로서 자바객체를 직접 관리한다.

Spring은 모든 객체의 생성, 소멸과 같은 라이프 사이클을 관리하며 Spring으로부터 필요한 객체를 얻어올 수 있다.

 

 

2. POJO(Plain Old Java Object) 방식의 프레임워크이다.

POJO는 특정 기술에 종속되지 않는 순수한 자바 객체를 의미한다. 기본적인 Java 기술만을 사용한, 외부 프레임워크에 종속되지 않은 객체를 말하는 것이다. 토비의 스프링에서는 POJO를 "진정한 POJO란 객체지향적인 원리에 충실하면서, 환경과 기술에 종속되지 않고 필요에 따라 재활용될 수 있는 방식으로 설계된 오브젝트를 말한다."라고 하는데 사실 이 말이 더욱 잘 와닿는다.

 

 

3. IoC(Inversion of Control)을 지원한다.

클래스 내부의 객체 생성 -> 의존성 객체의 메소드 호출이 아닌,  스프링에게 제어를 위임하여 스프링이 만든 객체를 주입 -> 의존성 객체의 메소드 호출 구조입니다. 스프링에서는 모든 의존성 객체를 스프링이 실행될 때 만들어주고 필요한 곳에 주입한다.

 

 

4. DI(Dependency Injection)을 지원한다.

어떤 객체(B)를 사용하는 주체(A)가 객체(B)를 직접 생성하는 것이 아닌 객체를 외부(Spring)에서 생성해서 사용하려는 주체 객체(A)에 주입시키는 방식이다. 사용하는 주체(A)가 사용하려는 객체(B)를 직접 생성하는 경우 의존성(변경사항이 있는 경우 서로에게 영향을 많이 준다)이 높아지게 된다. 하지만, 외부(Spring)에서 직접 생성하여 관리하는 경우에는 A와 B의 의존성이 줄어든다.

 

 

5. 영속성과 관련된 다양한 서비스를 지원한다.

iBatis, Hibernate 등 이미 완성도가 높은 데이터베이스 처리 라이브러리와 연결할 수 있는 인터페이스를 제공한다.

 

 

6. 확장성이 높다.

Spring에서 통합하기 위해 기존 라이브러리를 간단하게 감싸는 정도로 Spring에서 사용이 가능하기 때문에 수많은 라이브러리가 지원되고, 별도로 분리하기도 용이하다.


Spring의 핵심 3대 요소

이번 포스트와 다음 포스트 두번에 걸쳐서 Spring의 핵심 3대 요소를 알아볼 것이다. 먼저 3대 요소들을 살펴보기 전에 기초적인 Spring에서 가장 중요하다고 판단되는 것들을 자세히 다뤄보면서 구조들에 대해서도 조금씩 알아볼 것이다. 가장 먼저 알아볼 것은 스프링 컨테이너이다.

 

스프링 컨테이너 (Spring Container)

스프링 컨테이너는 스프링의 빈(Bean)을 생성하고 관리한다. 여기서 스프링 빈이란 쉽게 말하면 객체인데, 스프링 IoC 컨테이너가 관리하는 자바 객체를 빈이라고 부른다. 위에서 간략하게 IoC에 대해서 정리했는데 "스프링에게 제어를 위임하여 스프링이 만든 객체를 주입"이라고 한 부분이 있었다. 스프링 컨테이너가 바로 제어를 위임받고 객체를 생성하는 주체이다. 스프링 컨테이너는 IoC Container 혹은 DI Container라고 불리는데 스프링 컨테이너가 IoC, DI를 도맡아 진행하는 주체이기 때문이다. 다시 말해, 스프링 컨테이너는 스프링 빈을 생성하고 이들의 의존 관계를 연결해주는 역할을 한다. 구조를 그림으로 한번 살펴보자.

 

BeanFactory와 Application 구조 (출처 - 인프런 김영한 강의)

더보기
BeanFactory.java

public interface BeanFactory {

	/**
	 * Used to dereference a {@link FactoryBean} instance and distinguish it from
	 * beans <i>created</i> by the FactoryBean. For example, if the bean named
	 * {@code myJndiObject} is a FactoryBean, getting {@code &myJndiObject}
	 * will return the factory, not the instance returned by the factory.
	 */
	String FACTORY_BEAN_PREFIX = "&";


	/**
	 * Return an instance, which may be shared or independent, of the specified bean.
	 * <p>This method allows a Spring BeanFactory to be used as a replacement for the
	 * Singleton or Prototype design pattern. Callers may retain references to
	 * returned objects in the case of Singleton beans.
	 * <p>Translates aliases back to the corresponding canonical bean name.
	 * <p>Will ask the parent factory if the bean cannot be found in this factory instance.
	 * @param name the name of the bean to retrieve
	 * @return an instance of the bean
	 * @throws NoSuchBeanDefinitionException if there is no bean with the specified name
	 * @throws BeansException if the bean could not be obtained
	 */
	Object getBean(String name) throws BeansException;

	/**
	 * Return an instance, which may be shared or independent, of the specified bean.
	 * <p>Behaves the same as {@link #getBean(String)}, but provides a measure of type
	 * safety by throwing a BeanNotOfRequiredTypeException if the bean is not of the
	 * required type. This means that ClassCastException can't be thrown on casting
	 * the result correctly, as can happen with {@link #getBean(String)}.
	 * <p>Translates aliases back to the corresponding canonical bean name.
	 * <p>Will ask the parent factory if the bean cannot be found in this factory instance.
	 * @param name the name of the bean to retrieve
	 * @param requiredType type the bean must match; can be an interface or superclass
	 * @return an instance of the bean
	 * @throws NoSuchBeanDefinitionException if there is no such bean definition
	 * @throws BeanNotOfRequiredTypeException if the bean is not of the required type
	 * @throws BeansException if the bean could not be created
	 */
	<T> T getBean(String name, Class<T> requiredType) throws BeansException;

	/**
	 * Return an instance, which may be shared or independent, of the specified bean.
	 * <p>Allows for specifying explicit constructor arguments / factory method arguments,
	 * overriding the specified default arguments (if any) in the bean definition.
	 * @param name the name of the bean to retrieve
	 * @param args arguments to use when creating a bean instance using explicit arguments
	 * (only applied when creating a new instance as opposed to retrieving an existing one)
	 * @return an instance of the bean
	 * @throws NoSuchBeanDefinitionException if there is no such bean definition
	 * @throws BeanDefinitionStoreException if arguments have been given but
	 * the affected bean isn't a prototype
	 * @throws BeansException if the bean could not be created
	 * @since 2.5
	 */
	Object getBean(String name, Object... args) throws BeansException;

	/**
	 * Return the bean instance that uniquely matches the given object type, if any.
	 * <p>This method goes into {@link ListableBeanFactory} by-type lookup territory
	 * but may also be translated into a conventional by-name lookup based on the name
	 * of the given type. For more extensive retrieval operations across sets of beans,
	 * use {@link ListableBeanFactory} and/or {@link BeanFactoryUtils}.
	 * @param requiredType type the bean must match; can be an interface or superclass
	 * @return an instance of the single bean matching the required type
	 * @throws NoSuchBeanDefinitionException if no bean of the given type was found
	 * @throws NoUniqueBeanDefinitionException if more than one bean of the given type was found
	 * @throws BeansException if the bean could not be created
	 * @since 3.0
	 * @see ListableBeanFactory
	 */
	<T> T getBean(Class<T> requiredType) throws BeansException;

	/**
	 * Return an instance, which may be shared or independent, of the specified bean.
	 * <p>Allows for specifying explicit constructor arguments / factory method arguments,
	 * overriding the specified default arguments (if any) in the bean definition.
	 * <p>This method goes into {@link ListableBeanFactory} by-type lookup territory
	 * but may also be translated into a conventional by-name lookup based on the name
	 * of the given type. For more extensive retrieval operations across sets of beans,
	 * use {@link ListableBeanFactory} and/or {@link BeanFactoryUtils}.
	 * @param requiredType type the bean must match; can be an interface or superclass
	 * @param args arguments to use when creating a bean instance using explicit arguments
	 * (only applied when creating a new instance as opposed to retrieving an existing one)
	 * @return an instance of the bean
	 * @throws NoSuchBeanDefinitionException if there is no such bean definition
	 * @throws BeanDefinitionStoreException if arguments have been given but
	 * the affected bean isn't a prototype
	 * @throws BeansException if the bean could not be created
	 * @since 4.1
	 */
	<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;

	/**
	 * Return a provider for the specified bean, allowing for lazy on-demand retrieval
	 * of instances, including availability and uniqueness options.
	 * <p>For matching a generic type, consider {@link #getBeanProvider(ResolvableType)}.
	 * @param requiredType type the bean must match; can be an interface or superclass
	 * @return a corresponding provider handle
	 * @since 5.1
	 * @see #getBeanProvider(ResolvableType)
	 */
	<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);

	/**
	 * Return a provider for the specified bean, allowing for lazy on-demand retrieval
	 * of instances, including availability and uniqueness options. This variant allows
	 * for specifying a generic type to match, similar to reflective injection points
	 * with generic type declarations in method/constructor parameters.
	 * <p>Note that collections of beans are not supported here, in contrast to reflective
	 * injection points. For programmatically retrieving a list of beans matching a
	 * specific type, specify the actual bean type as an argument here and subsequently
	 * use {@link ObjectProvider#orderedStream()} or its lazy streaming/iteration options.
	 * <p>Also, generics matching is strict here, as per the Java assignment rules.
	 * For lenient fallback matching with unchecked semantics (similar to the ´unchecked´
	 * Java compiler warning), consider calling {@link #getBeanProvider(Class)} with the
	 * raw type as a second step if no full generic match is
	 * {@link ObjectProvider#getIfAvailable() available} with this variant.
	 * @return a corresponding provider handle
	 * @param requiredType type the bean must match; can be a generic type declaration
	 * @since 5.1
	 * @see ObjectProvider#iterator()
	 * @see ObjectProvider#stream()
	 * @see ObjectProvider#orderedStream()
	 */
	<T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);

	/**
	 * Does this bean factory contain a bean definition or externally registered singleton
	 * instance with the given name?
	 * <p>If the given name is an alias, it will be translated back to the corresponding
	 * canonical bean name.
	 * <p>If this factory is hierarchical, will ask any parent factory if the bean cannot
	 * be found in this factory instance.
	 * <p>If a bean definition or singleton instance matching the given name is found,
	 * this method will return {@code true} whether the named bean definition is concrete
	 * or abstract, lazy or eager, in scope or not. Therefore, note that a {@code true}
	 * return value from this method does not necessarily indicate that {@link #getBean}
	 * will be able to obtain an instance for the same name.
	 * @param name the name of the bean to query
	 * @return whether a bean with the given name is present
	 */
	boolean containsBean(String name);

	/**
	 * Is this bean a shared singleton? That is, will {@link #getBean} always
	 * return the same instance?
	 * <p>Note: This method returning {@code false} does not clearly indicate
	 * independent instances. It indicates non-singleton instances, which may correspond
	 * to a scoped bean as well. Use the {@link #isPrototype} operation to explicitly
	 * check for independent instances.
	 * <p>Translates aliases back to the corresponding canonical bean name.
	 * <p>Will ask the parent factory if the bean cannot be found in this factory instance.
	 * @param name the name of the bean to query
	 * @return whether this bean corresponds to a singleton instance
	 * @throws NoSuchBeanDefinitionException if there is no bean with the given name
	 * @see #getBean
	 * @see #isPrototype
	 */
	boolean isSingleton(String name) throws NoSuchBeanDefinitionException;

	/**
	 * Is this bean a prototype? That is, will {@link #getBean} always return
	 * independent instances?
	 * <p>Note: This method returning {@code false} does not clearly indicate
	 * a singleton object. It indicates non-independent instances, which may correspond
	 * to a scoped bean as well. Use the {@link #isSingleton} operation to explicitly
	 * check for a shared singleton instance.
	 * <p>Translates aliases back to the corresponding canonical bean name.
	 * <p>Will ask the parent factory if the bean cannot be found in this factory instance.
	 * @param name the name of the bean to query
	 * @return whether this bean will always deliver independent instances
	 * @throws NoSuchBeanDefinitionException if there is no bean with the given name
	 * @since 2.0.3
	 * @see #getBean
	 * @see #isSingleton
	 */
	boolean isPrototype(String name) throws NoSuchBeanDefinitionException;

	/**
	 * Check whether the bean with the given name matches the specified type.
	 * More specifically, check whether a {@link #getBean} call for the given name
	 * would return an object that is assignable to the specified target type.
	 * <p>Translates aliases back to the corresponding canonical bean name.
	 * <p>Will ask the parent factory if the bean cannot be found in this factory instance.
	 * @param name the name of the bean to query
	 * @param typeToMatch the type to match against (as a {@code ResolvableType})
	 * @return {@code true} if the bean type matches,
	 * {@code false} if it doesn't match or cannot be determined yet
	 * @throws NoSuchBeanDefinitionException if there is no bean with the given name
	 * @since 4.2
	 * @see #getBean
	 * @see #getType
	 */
	boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;

	/**
	 * Check whether the bean with the given name matches the specified type.
	 * More specifically, check whether a {@link #getBean} call for the given name
	 * would return an object that is assignable to the specified target type.
	 * <p>Translates aliases back to the corresponding canonical bean name.
	 * <p>Will ask the parent factory if the bean cannot be found in this factory instance.
	 * @param name the name of the bean to query
	 * @param typeToMatch the type to match against (as a {@code Class})
	 * @return {@code true} if the bean type matches,
	 * {@code false} if it doesn't match or cannot be determined yet
	 * @throws NoSuchBeanDefinitionException if there is no bean with the given name
	 * @since 2.0.1
	 * @see #getBean
	 * @see #getType
	 */
	boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;

	/**
	 * Determine the type of the bean with the given name. More specifically,
	 * determine the type of object that {@link #getBean} would return for the given name.
	 * <p>For a {@link FactoryBean}, return the type of object that the FactoryBean creates,
	 * as exposed by {@link FactoryBean#getObjectType()}. This may lead to the initialization
	 * of a previously uninitialized {@code FactoryBean} (see {@link #getType(String, boolean)}).
	 * <p>Translates aliases back to the corresponding canonical bean name.
	 * <p>Will ask the parent factory if the bean cannot be found in this factory instance.
	 * @param name the name of the bean to query
	 * @return the type of the bean, or {@code null} if not determinable
	 * @throws NoSuchBeanDefinitionException if there is no bean with the given name
	 * @since 1.1.2
	 * @see #getBean
	 * @see #isTypeMatch
	 */
	@Nullable
	Class<?> getType(String name) throws NoSuchBeanDefinitionException;

	/**
	 * Determine the type of the bean with the given name. More specifically,
	 * determine the type of object that {@link #getBean} would return for the given name.
	 * <p>For a {@link FactoryBean}, return the type of object that the FactoryBean creates,
	 * as exposed by {@link FactoryBean#getObjectType()}. Depending on the
	 * {@code allowFactoryBeanInit} flag, this may lead to the initialization of a previously
	 * uninitialized {@code FactoryBean} if no early type information is available.
	 * <p>Translates aliases back to the corresponding canonical bean name.
	 * <p>Will ask the parent factory if the bean cannot be found in this factory instance.
	 * @param name the name of the bean to query
	 * @param allowFactoryBeanInit whether a {@code FactoryBean} may get initialized
	 * just for the purpose of determining its object type
	 * @return the type of the bean, or {@code null} if not determinable
	 * @throws NoSuchBeanDefinitionException if there is no bean with the given name
	 * @since 5.2
	 * @see #getBean
	 * @see #isTypeMatch
	 */
	@Nullable
	Class<?> getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException;

	/**
	 * Return the aliases for the given bean name, if any.
	 * <p>All of those aliases point to the same bean when used in a {@link #getBean} call.
	 * <p>If the given name is an alias, the corresponding original bean name
	 * and other aliases (if any) will be returned, with the original bean name
	 * being the first element in the array.
	 * <p>Will ask the parent factory if the bean cannot be found in this factory instance.
	 * @param name the bean name to check for aliases
	 * @return the aliases, or an empty array if none
	 * @see #getBean
	 */
	String[] getAliases(String name);

}​



ApplicationContext.java

구조도에 있는 인터페이스들 말고도 다른 많은 인터페이스들도 상속받고 있다.
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
		MessageSource, ApplicationEventPublisher, ResourcePatternResolver {

	/**
	 * Return the unique id of this application context.
	 * @return the unique id of the context, or {@code null} if none
	 */
	@Nullable
	String getId();

	/**
	 * Return a name for the deployed application that this context belongs to.
	 * @return a name for the deployed application, or the empty String by default
	 */
	String getApplicationName();

	/**
	 * Return a friendly name for this context.
	 * @return a display name for this context (never {@code null})
	 */
	String getDisplayName();

	/**
	 * Return the timestamp when this context was first loaded.
	 * @return the timestamp (ms) when this context was first loaded
	 */
	long getStartupDate();

	/**
	 * Return the parent context, or {@code null} if there is no parent
	 * and this is the root of the context hierarchy.
	 * @return the parent context, or {@code null} if there is no parent
	 */
	@Nullable
	ApplicationContext getParent();

	/**
	 * Expose AutowireCapableBeanFactory functionality for this context.
	 * <p>This is not typically used by application code, except for the purpose of
	 * initializing bean instances that live outside the application context,
	 * applying the Spring bean lifecycle (fully or partly) to them.
	 * <p>Alternatively, the internal BeanFactory exposed by the
	 * {@link ConfigurableApplicationContext} interface offers access to the
	 * {@link AutowireCapableBeanFactory} interface too. The present method mainly
	 * serves as a convenient, specific facility on the ApplicationContext interface.
	 * <p><b>NOTE: As of 4.2, this method will consistently throw IllegalStateException
	 * after the application context has been closed.</b> In current Spring Framework
	 * versions, only refreshable application contexts behave that way; as of 4.2,
	 * all application context implementations will be required to comply.
	 * @return the AutowireCapableBeanFactory for this context
	 * @throws IllegalStateException if the context does not support the
	 * {@link AutowireCapableBeanFactory} interface, or does not hold an
	 * autowire-capable bean factory yet (e.g. if {@code refresh()} has
	 * never been called), or if the context has been closed already
	 * @see ConfigurableApplicationContext#refresh()
	 * @see ConfigurableApplicationContext#getBeanFactory()
	 */
	AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;

}​



MessageCource.java


public interface MessageSource {

	/**
	 * Try to resolve the message. Return default message if no message was found.
	 * @param code the message code to look up, e.g. 'calculator.noRateSet'.
	 * MessageSource users are encouraged to base message names on qualified class
	 * or package names, avoiding potential conflicts and ensuring maximum clarity.
	 * @param args an array of arguments that will be filled in for params within
	 * the message (params look like "{0}", "{1,date}", "{2,time}" within a message),
	 * or {@code null} if none
	 * @param defaultMessage a default message to return if the lookup fails
	 * @param locale the locale in which to do the lookup
	 * @return the resolved message if the lookup was successful, otherwise
	 * the default message passed as a parameter (which may be {@code null})
	 * @see #getMessage(MessageSourceResolvable, Locale)
	 * @see java.text.MessageFormat
	 */
	@Nullable
	String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);

	/**
	 * Try to resolve the message. Treat as an error if the message can't be found.
	 * @param code the message code to look up, e.g. 'calculator.noRateSet'.
	 * MessageSource users are encouraged to base message names on qualified class
	 * or package names, avoiding potential conflicts and ensuring maximum clarity.
	 * @param args an array of arguments that will be filled in for params within
	 * the message (params look like "{0}", "{1,date}", "{2,time}" within a message),
	 * or {@code null} if none
	 * @param locale the locale in which to do the lookup
	 * @return the resolved message (never {@code null})
	 * @throws NoSuchMessageException if no corresponding message was found
	 * @see #getMessage(MessageSourceResolvable, Locale)
	 * @see java.text.MessageFormat
	 */
	String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;

	/**
	 * Try to resolve the message using all the attributes contained within the
	 * {@code MessageSourceResolvable} argument that was passed in.
	 * <p>NOTE: We must throw a {@code NoSuchMessageException} on this method
	 * since at the time of calling this method we aren't able to determine if the
	 * {@code defaultMessage} property of the resolvable is {@code null} or not.
	 * @param resolvable the value object storing attributes required to resolve a message
	 * (may include a default message)
	 * @param locale the locale in which to do the lookup
	 * @return the resolved message (never {@code null} since even a
	 * {@code MessageSourceResolvable}-provided default message needs to be non-null)
	 * @throws NoSuchMessageException if no corresponding message was found
	 * (and no default message was provided by the {@code MessageSourceResolvable})
	 * @see MessageSourceResolvable#getCodes()
	 * @see MessageSourceResolvable#getArguments()
	 * @see MessageSourceResolvable#getDefaultMessage()
	 * @see java.text.MessageFormat
	 */
	String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;

}​



EnvironmentCapable.java


public interface EnvironmentCapable {

	/**
	 * Return the {@link Environment} associated with this component.
	 */
	Environment getEnvironment();

}​



ApplicationEventPublisher.java

@FunctionalInterface
public interface ApplicationEventPublisher {

	/**
	 * Notify all <strong>matching</strong> listeners registered with this
	 * application of an application event. Events may be framework events
	 * (such as ContextRefreshedEvent) or application-specific events.
	 * <p>Such an event publication step is effectively a hand-off to the
	 * multicaster and does not imply synchronous/asynchronous execution
	 * or even immediate execution at all. Event listeners are encouraged
	 * to be as efficient as possible, individually using asynchronous
	 * execution for longer-running and potentially blocking operations.
	 * @param event the event to publish
	 * @see #publishEvent(Object)
	 * @see org.springframework.context.event.ContextRefreshedEvent
	 * @see org.springframework.context.event.ContextClosedEvent
	 */
	default void publishEvent(ApplicationEvent event) {
		publishEvent((Object) event);
	}

	/**
	 * Notify all <strong>matching</strong> listeners registered with this
	 * application of an event.
	 * <p>If the specified {@code event} is not an {@link ApplicationEvent},
	 * it is wrapped in a {@link PayloadApplicationEvent}.
	 * <p>Such an event publication step is effectively a hand-off to the
	 * multicaster and does not imply synchronous/asynchronous execution
	 * or even immediate execution at all. Event listeners are encouraged
	 * to be as efficient as possible, individually using asynchronous
	 * execution for longer-running and potentially blocking operations.
	 * @param event the event to publish
	 * @since 4.2
	 * @see #publishEvent(ApplicationEvent)
	 * @see PayloadApplicationEvent
	 */
	void publishEvent(Object event);

}​



ResourcePatternResolver.java

하나 특이한점은 ResourceLoader를 직접 상속받는 것이 아닌 ResourceLoader를 상속받은 ResourcePatternResolver를 상속받고 있다. 코드를 확인해보면 알겠지만 ResourceLoader에서 정의한 getResource를 Resource[] 타입으로 오버로딩해서 사용하고 있다.
public interface ResourcePatternResolver extends ResourceLoader {

	/**
	 * Pseudo URL prefix for all matching resources from the class path: {@code "classpath*:"}.
	 * <p>This differs from ResourceLoader's {@code "classpath:"} URL prefix in
	 * that it retrieves all matching resources for a given path &mdash; for
	 * example, to locate all "beans.xml" files in the root of all deployed JAR
	 * files you can use the location pattern {@code "classpath*:/beans.xml"}.
	 * <p>As of Spring Framework 6.0, the semantics for the {@code "classpath*:"}
	 * prefix have been expanded to include the module path as well as the class path.
	 * @see org.springframework.core.io.ResourceLoader#CLASSPATH_URL_PREFIX
	 */
	String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

	/**
	 * Resolve the given location pattern into {@code Resource} objects.
	 * <p>Overlapping resource entries that point to the same physical
	 * resource should be avoided, as far as possible. The result should
	 * have set semantics.
	 * @param locationPattern the location pattern to resolve
	 * @return the corresponding {@code Resource} objects
	 * @throws IOException in case of I/O errors
	 */
	Resource[] getResources(String locationPattern) throws IOException;

}​

Spring Github에서 확인해볼 수 있는 코드들이다. 접은 글에서 확인하고 넘어가는 것도 좋지만 직접 깃허브에서 클론해와서 확인해보는 것을 추천한다. 필요없다고 생각이 들 수도 있지만 어떤 클래스가 어느 부분에 위치해있는 것인지 아는 것만으로도 도움이 된다.

 

BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스이다.
  • 스프링 빈을 관리하고 조회하는 역할을 담당한다. 유일한 객체인지(싱글톤인지)를 확인하고 타입 동일 유무를 확인하는 등의 메소드를 통해 스프링 빈을 관리한다.

ApplicationContext

  • BeanFactory 기능을 모두 상속받아서 제공한다.
  • 여러 다른 인터페이스들을 상속받아 다음과 같은 부가기능들을 제공한다.
    • 메시지 소스를 활용한 국제화 기능 (MessageSource)
    • 환경변수 - 로컬, 개발, 운영 등을 구분해서 처리 (EnvironmentCapable)
    • 애플리케이션 이벤트 관리 - (ApplicationEventPublisher)
    • 편리한 리소스 조회 - (ResourcePatternResolve extends ResourceLoader)

역할에 따라 클래스 네이밍이 정말 직관적인걸 확인할 수 있다. 보통 스프링 컨테이너라고 하면 ApplicationContext를 말한다. BeanFactory가 최상위이긴 하지만 BeanFactory의 기능에 다른 부가기능을 더한 ApplicationContext를 사용하게 되기 때문이다.


DI(Dependency Injection)

DI는 Spring의 가장 중요한 특성중 하나다. 오죽하면 스프링 컨테이너를 DI 컨테이너라고도 할까. (위에서도 말했지만 IoC 컨테이너라고도 한다.) 직역하면 의존성 주입이라고도 한다. DI를 정확히 알기 전에 의존관계라는 것을 알아야할 필요가 있다. A와 B가 존재하고 B의 상태나 특정 값이 변할때, 그것이 A에 영향을 미친다면 A는 B와 의존관계라고 한다. 코드로 예를 들어보자.

 

public class PizzaChef {
    private PizzaRecipe pizzaRecipe;
    
    public PizzaChef() {
        this.pizzaRecipe = new PizzaRecipe();
    }
}

 

PizzaChef 객체는 PizzaRecipe 객체에 의존관계가 있다. 이런 구조는 다음 같은 문제점들을 가진다.

 

1. 두 클래스의 결합성이 높다.

PizzaChef 클래스는 PizzaRecipe 클래스와 강한 결합도를 가지고 있는 것이 문제이다. 만약에 PizzaChef가 새로운 레시피를 이용해야 한다면 즉, 새로운 피자레시피 클래스를 사용해야 한다면 PizzaChef의 생성자를 변경해야만 한다. 그래서, 매번 코드 수정이 불가피하기 때문에 유연성이 떨어진다.

 

2. 객체들 간의 관계가 아닌 클래스 간의 관계가 맺어진다.

객체 지향 5원칙(SOLID) 중 "추상화(인터페이스)에 의존해야지 구체화(구현체)에 의존하면 안된다"DIP (Dependency Inversion Principle) 원칙이 존재한다. 위의 코드에서 PizaaChef는 클래스에 의존하고 있다. 이는 객체 지향 5원칙을 위반하는 것으로 PizzaChef 클래스의 변경이 어렵다.

 

이러한 문제들을 DI로 해결할 수 있다.

 

DI는 의존관계를 외부에서 결정 및 주입 해주는 것을 말한다. Spring에서는 이러한 DI를 바로 스프링 컨테이너가 처리해준다. 위에서 설명했듯이 ApplicationContext에서 관리를 하게 된다. 스프링에서 보통 설정이나 구성 정보를 @Configuration 어노테이션을 사용한 AppConfig 클래스를 통해 사용한다. 이곳의 @Bean 어노테이션을 사용한 메소드를 모두 호출하여 반환된 객체를 스프링 컨테이너에 등록하게 된다. 이러한 것들을 스프링 빈으로써 관리하게 되는데 ApplicationContext.getBean() 메소드를 통해서 찾아낼 수 있다. 어노테이션에 대한 설명은 추후에 더 자세히 다뤄볼 것이다.

 

DI를 하는 방법에는 필드 주입 방식, 생성자 주입 방식, 수정자 주입 방식 등 세가지 정도가 있다. 스프링에서는 생성자 주입 방식을 권장하는데 요즘엔 아예 생성자 주입 방식으로 하라고 더 강력하게 권장하는 듯 하다. DI 방식에 대한 글들은 구글에 널려있다. 조금만 찾아보면 쉽게 이해할 수 있는 내용이므로 이전에 프로젝트 리팩토링을 하면서 만났던 [Spring] 순환 참조에 대한 의문점 이라는 포스트 링크를 남기겠다. 실제로 몸소 생성자 주입 방식을 사용해야 하는 이유를 깨닫게 된 경험이고 클래스 설계를 꼼꼼히 해야하는 이유들이 기록 되어있으니 한번씩 읽어보는 것을 추천한다.

 

 

IoC (Inversion of Control)

직역하자면 제어의 역전이라는 말이다. 보통은 우리가 객체를 생성할 때 new 키워드를 사용해서 객체를 생성한다. 프로그램을 개발하면서 아무리 설계를 잘했다고 하더라도 수많은 객체를 사람이 직접 생성을 한다면 과연 유연한 코드가 나올 수 있을까? 그렇지 않기 때문에 스프링과 같은 프레임워크가 만들어졌을 것이다.

 

IoC가 필요한 이유는 객체지향 원칙을 잘 지켜서 변경에 유연한 코드를 작성하기 위함이다. 객체를 관리해주는 프레임워크와 내가 구현하고자 하는 부분으로 역할과 책임을 분리하여 응집도는 높이고 결합도는 낮추는 것이다. 마찬가지로 객체를 관리하는 것은 스프링에선 스프링 컨테이너가 담당한다. 

 

그렇다면 정확히 어떤 제어를 프레임워크로 위임하는 것일까? 위에서 의존관계는 하나의 클래스의 변경이 다른 클래스에 영향을 끼치는 관계가 의존관계라고 했다. 한 클래스 내에서 다른 클래스를 new 연산자를 사용하여 직접 생성하게 되는 것은 개발자가 직접 의존성을 만드는 것이 되는 것이다. 계속 언급되었는데 객체를 생성하고 관리하는 것을 프레임워크로 위임하는 것이다. 스프링 컨테이너가 객체를 생성하고 이를 필요한 곳에 주입을 하게 된다. 이 말을 다시 살펴보면 DI는 IoC의 일종이라고 볼 수 있다.

 

덧붙여 스프링에서의 의존성 주입은 반드시 Bean으로 등록된 객체들끼리만 가능하다. 스프링 컨테이너는 Bean으로 등록되지 않은 객체에는 의존성 주입을 하지 않는다.

 

스프링 컨테이너가 그래서 어떻게 객체들을 관리할까? Component Scanning이라는 것으로 @Component 어노테이션을 활용하여 관리한다. 여러 인터페이스들을 라이프사이클 콜백이라고 부르는데, @Component와 혹은 같은 역할을 하는 어노테이션들을 모두 찾아 그 클래스들의 인스턴스를 만들고 Bean으로 등록하는 복잡한 작업을 하는 어노테이션 처리기가 스프링에 존재한다. 지금 이 과정들은 스프링 빈에 대해서 알면 이해하기가 쉬워진다.

 

Spring Bean과 Spring Bean 생성 방법

스프링 빈은 스프링 컨테이너가 관리하는 객체이다. 어떤 작업을 수행할 수 있는 객체들이고 스프링이 관리하면서 언제든지 DI 될 수 있다. 스프링 빈은 별다른 설정이 없다면 기본적으로 싱글톤으로 생성이 된다. 하지만 경우에 따라 싱글톤이 아닌 객체를 생성해야할 때가 있는데 이때 Spring Bean Scope를 다르게 설정하여 객체를 생성하게 된다.

 

Spring Bean Scope

  • Singleton (Default) : 기본적으로 사용되는 scope. 예를 들어 A란 객체가 스프링 컨테이너에 생성됐다면 A객체는 단 하나만 존재한다. DI 시에도 항상 같은 객체가 사용된다.
  • Prototype : 다수의 객체가 컨테이너에 존재할 수 있다. DI 시 매번 새로운 객체가 생성된다.
  • Request : 하나의 HTTP request 동안 하나의 객체만 존재한다. 즉 request가 들어올 때마다 생성된다.
  • Session : 하나의 HTTP session 동안 하나의 객체만 존재한다.

Spring Bean 생성방법

  • @Bean : 외부 라이브러리에서 정의한 클래스를 스프링 빈으로 등록하고자 하는 경우 사용한다.
  • @Component : 개발자가 직접 작성한 클래스를 스프링 빈으로 등록하고자 하는 경우 사용한다. @Component 어노테이션은 각 컴포턴트의 역할을 구분할 때 사용하는데 아래와 같이 구분이 될 수 있다.
    • @Controller : 기본적으로 @Component와 같다. presentation layer의 component 라는 것을 표기
    • @Service : 기본적으로 @Component와 같다. business layer의 component 라는 것을 표기
    • @Repository : 기본적으로 @Component와 같다. persistence layer의 component 라는 것을 표기 
      • Presentation Layer는 주로 클라이언트에서 보낸 요청 간의 json 형태의 요청을 수행한다.
      • Business Layer는 Presentation Layer에서 보내온 데이터를 비즈니스 로직에 따른 계산, 처리를 수행한다.
      • Persistence Layer는 주로 데이터베이스와의 연결을 유지하고 데이터를 송수신한다.

스프링 빈과 빈으로 등록하는 방법들을 간략하게 알아봤다. 그럼 마저 컴포넌트 스캐닝에 대해서 살펴보자. 먼저 main 메소드가 있는 Application 부분을 확인해보자.

 

 

@SpringBootApplication 어노테이션을 확인해보면

 

@ComponentScan 어노테이션이 존재한다. 컴포넌트 스캔은 어떠한 지점에서부터 컴포넌트를 탐색하라고 알려주는 어노테이션이다. 그럼 @SpringBootApplication이 붙은 클래스에서부터 모든 하위 클래스들을 흝어보면서 모든 @Component 어노테이션을 찾게 된다. 그리고 스프링 컨테이너에 Bean으로 등록하게 되는 것이다.

 

 


정리

스프링 3대 요소에는 IoC/DI, AOP, PSA가 있다. 두번의 포스트에 걸쳐서 알아보겠다고 했는데 이번 포스트는 스프링 컨테이너가 객체를 어떻게 관리하는지를 알아보면서 IoC/DI 를 주로 다뤘다. 다음 포스트에서 남은 AOP와 PSA를 알아보도록 하자.

 

 

 

 

 


참고 링크

 

 

[Spring] IoC,DI, 스프링 컨테이너(Container), 스프링 빈(Bean)이란?

IoC(Inversion of Control)란? IoC는 제어의 역전이라는 뜻으로 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 말한다. 이전에는 개발자가 객체를 생성하고 관리하며 프로

code-lab1.tistory.com

 

 

[Spring] Spring의 IoC(Inversion of Control)과 Bean

컴퓨터/IT/알고리즘 정리 블로그

chanhuiseok.github.io

 

 

[Spring] Spring 기초

이번 포스팅에서는 인턴을 하게 된 회사에서 진행해준 신입사원 교육 중, Spring의 기초에 대해 정리해보려 합니다. Spring Boot로 프로젝트를 진행했던 경험이 있어서 Spring의 특징에 대해 어느 정도

programforlife.tistory.com