AtraFelis's Develop Diary

[Spring] 스프링 컨테이너와 스프링 빈 (스프링 핵심원리 - 기본편 section5) 본문

Programming/Spring

[Spring] 스프링 컨테이너와 스프링 빈 (스프링 핵심원리 - 기본편 section5)

AtraFelis 2025. 1. 12. 18:05

스프링 핵심 원리 - 기본편 강의를 수강하며 작성한 글입니다.
Section 5

 

이제 순수한 자바에서 벗어나, 스프링을 사용해보자.

먼저 기존의 AppConfig를 직접 사용하여 DI하던 것을 스프링을 사용하는 형태로 수정한다.

@Configuration  
public class AppConfig {  

    @Bean  
    public MemberService memberService() {  
        return new MemberServiceImpl(memberRepository());  
    }  

    @Bean  
    public MemberRepository memberRepository() {  
        return new MemoryMemberRepository();  
    }  

    @Bean  
    public OrderService orderService() {  
        return new OrderServiceImpl(memberRepository(), discountPolicy());  
    }  

    @Bean  
    private static DiscountPolicy discountPolicy() {  
        return new FixDiscountPolicy();  
    }  
}

@Configuration 애노테이션으로 AppConfig를 설정 파일로 등록해준 후, 각 메소드에 @Bean를 붙여준다. @Bean 애노테이션이 붙은 매소드에서 반환되는 구현체들은 스프링 컨테이너라는 곳에 저장되어 사용된다. 이렇게 스프링 컨테이너에 등록된 개체들을 스프링 빈이라고 한다.

또한 기존에 AppConfig를 사용하던 OrderApp과 같은 클래스도 스프링을 활용하도록 수정을 해준다.

public class OrderApp {  
    public static void main(String[] args) {

//        AppConfig appConfig = new AppConfig();  
//        MemberService memberService = appConfig.memberService();  
//        OrderService orderService = appConfig.orderService();  

        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);  

    MemberService memberService = context.getBean("memberService", MemberService.class);  
    OrderService orderService = context.getBean("orderService", OrderService.class);

        ...
    }
}

이렇게 하면, 스프링을 이용하여 DI를 할 수 있게 되었다.

여기서 ApplicationContext가 스프링 컨테이너이다. 이 스프링 컨테이너에서 getBean() 메소드를 이용해 등록해둔 스프링 빈을 꺼내어 사용하면 된다.

💡 이렇게 수정하면 뭐가 좋은 건가요?

이것에 대한 것은 Section 6. 싱글톤 컨테이너에서 다룬다. 그래도 간략하게 설명하자면, 이렇게 함으로써, 단 하나의 객체만을 생성하여 공유할 수 있게 된다. 이를 통해 메모리 소모를 줄일 수 있다.

 

스프링 컨테이너와 스프링 빈

스프링 컨테이너를 만드는 방식에는 두 가지가 있다.

XML 방식과 애노테이션 기반의 자바 설정 클래스 방식이다. 위에서 AppConfig클래스를 이용한 방식이 후자의 방식이다. 이 방식이 매우 간편하고 잘 되어있기 때문에, XML 방식은 요즘 잘 쓰지 않는다.

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

1. 스프링 컨테이너 생성

new AnnotationConfigApplicationContext(AppConfig.class)와 같은 방식으로 스프링 컨테이너를 생성할 수 있다.

한글로 풀어 적어보자면, 애노테이션 기반의 스프링 컨테이너를 생성하되 구성 정보는 AppConfig.class를 참고하라, 라고 할 수 있겠다.

XML 기반이라면 new GenericXmlConfigApplicationContext(AppConfig.class)이 될 것이다.

 

2. 스프링 빈 등록

스프링 내부적으로 AppConfig.class를 보며 스프링 컨테이너에 스프링 빈을 채워넣기 시작한다.

위에서 상술했듯 AppConfig.class 내부의 메소드들이 반환하는 객체들을 스프링 컨테이너에 등록하는 것이다. 빈 이름은 따로 지정하지 않을 시 메소드의 이름과 동일하게 설정된다.

💡 빈 이름 지정하는 방법

이렇게 따로 빈 이름을 지정할 수는 있지만, 추후에 헷갈릴 수 있기 때문에 권장하지 않는다. 보통 이렇게 이름을 따로 지정하는 경우는 같은 타입의 객체를 스프링 빈에 등록해주어야 할 때인데, 애초부터 중복이 일어나지 않게끔 만들자.

java
 @Configuration  
 public class AppConfig {  
     @Bean(name = "ms")
     public MemberService memberService() {  
         return new MemberServiceImpl(memberRepository());
     }
     ...
}

 

3. 스프링 빈 의존관계 설정

설정 정보를 참고하여 의존관계를 주입한다.

이와 관련된 내용은 바로 다음 포스팅 6. 싱글톤 컨테이너에서 다룬다.

 

컨테이너에서 빈 조회하기

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
이 예시에서 모든 스프링 컨테이너는 ac라는 이름으로 이와 같이 선언되어 있다.

 

  • ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회하여 String 배열의 형태로 반환한다.
    `
  • ac.getBean(타입) : 타입만으로 검색한다. - ac.getBean(MemberService.class)
    • 같은 타입의 빈이 두 개 존재할 경우, NoUniqueBeanDefinitionException 예외 발생.
  • ac.getBean(빈이름, 타입) : 빈이름과 타입으로 검색한다. - `MemberService bean = ac.getBean("memberService", MemberService.class)
    • 조회 대상 빈이 없다면, NoSuchBeanDefinitionException 예외 발생.
  • ac.getBeansOfType(타입) : 해당 타입을 가지는 모든 빈을 Map<빈이름, 타입>의 형태로 반환한다. - Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
  • 부모 타입으로 조회할 경우 하위 타입의 모든 스프링 빈을 조회할 수 있다.
    • ac.getBeansOfType(Object.class)와 같이 조회하면, 존재하는 모든 스프링 빈을 조회할 수 있다.

 

BeanFactory

ApplicationContext를 스프링 컨테이너라고 했었는데, 사실 스프링 컨테이너라고 하면 BeanFactory까지 포함하여 스프링 컨테이너라고 할 수 있다. (용어가 좀 모호한 감이 없지 않아 있다. 보통 BeanFactory를 직접 사용할 일은 없으므로 스프링 컨테이너라고 하면 ApplicationContext를 지칭한다고 보면 될 것 같다.)

위 그림과 같이, ApplicationContextBeanFactory를 상속받는 구조이다. 즉, BeanFactory에서 조금 더 기능을 추가해 놓은 것이 ApplicationContext이다.

ApplicationContext의 코드를 직접 확인해보자.

public interface ApplicationContext extends
			EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
            MessageSource, ApplicationEventPublisher, ResourcePatternResolver
{  
    @Nullable  
    String getId();
    String getApplicationName();  
    String getDisplayName();  
    long getStartupDate();  
  
    @Nullable  
    ApplicationContext getParent();
    AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;  
}

BeanFactory와 함께 다른 여러가지 인터페이스도 상속받고 있는 것을 볼 수 있다.

💡 그냥 BeanFactory가 아니라 다른 걸 상속받고 있는데요?

눈썰미가 좋은 분들은 눈치챘겠지만, 확인해보면 바로 BeanFactory를 상속받는 것이 아니라, BeanFactory를 상속하는 ListableBeanFactory, HierarchicalBeanFactory를 상속 받는 것을 확인할 수 있다.

ListableBeanFactory는 여러 Bean 정보를 목록 형태로 가져올 수 있는 추가 메서드들을 정의하는 BeanFactory의 확장 인터페이스이다. 이 인터페이스에서 getBeansOfType()과 같은 메소드를 정의하고 있다.

HierarchicalBeanFactory는 부모(Parent) BeanFactory를 참조하는 기능을 제공함으로써, 계층적인(계승되는) 컨테이너 구조를 지원한다. (by ChatGPT o1)

위 두 인터페이스는 BeanFactory에서 약간의 확장 기능만을 추가하고 있으므로, 그냥 BeanFactory를 상속받는다고 생각해도 될 듯하다. 그래서 김영한 님도 강의에서 별 다른 언급이 없으셨던게 아닐까? 아니면 심화 강의에서 언급하실 수도 있고.

 

각 인터페이스들은 아래와 같은 역할을 하는 메소드들을 정의하고 있다.

  • 메시지소스를 활용한 국제화 기능 - MessageSource
    • 예를 들어서 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력
  • 환경변수 - EnvironmentCapable
    • 로컬, 개발, 운영등을 구분해서 처리
  • 애플리케이션 이벤트 - ApplicationEventPublisher
    • 이벤트를 발행하고 구독하는 모델을 편리하게 지원
  • 편리한 리소스 조회 - ResourcePatternResolver
    • 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회

 

BeanDefinition

스프링 컨테이너를 설정하는 방법은 여러가지가 있다고 언급했었다.

지금까지 해왔던 것처럼 자바 코드 기반의 애노테이션을 이용할 수도 있고 XML 기반의 구성 정보를 이용할 수도 있다. 혹은 사용자가 원하는 형식으로(예를 들어 JSON으로 한다거나) 만들어 사용할 수도 있다.

이렇게 보았을 때 지금까지 해왔던 것들이 슥 스쳐지나가면서, 무언가 떠오르지 않는가?

바로 역할과 구현의 분리다.

클라이언트 코드인 OrderApp이 어떤 할인 정책(DiscountPolicy)를 사용하든, 어떤 저장소(MemberRepository)를 사용하든 상관 없이 그 기능을 실행하는데만 집중했던 것처럼, 스프링 컨테이너도 마찬가지다.

스프링 컨테이너는 사용자가 구성 파일을 자바 코드로 작성했든, XML로 작성했든, JSON으로 작성했든 상관하지 않는다. 그냥 읽고 BeanDefinition이라는 빈 설정 메타정보를 만들면 된다.

김영한 님의 말에 따르면, AnnotationConfigApplicationContext를 이용하여 BeanDefinition을 생성한다고 한다.

직접 코드를 뜯어 확인해보자.

public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry {  
    private final AnnotatedBeanDefinitionReader reader;  
    private final ClassPathBeanDefinitionScanner scanner;  

    public AnnotationConfigApplicationContext() {...}  

    public AnnotationConfigApplicationContext(DefaultListableBeanFactory beanFactory) {...}

    public AnnotationConfigApplicationContext(Class<?>... componentClasses) {...}

    public AnnotationConfigApplicationContext(String... basePackages) {...}

    public void setEnvironment(ConfigurableEnvironment environment) {...}

    public void setBeanNameGenerator(BeanNameGenerator beanNameGenerator) {...}

    public void setScopeMetadataResolver(ScopeMetadataResolver scopeMetadataResolver) {...}

    public void register(Class<?>... componentClasses) {...}

    public void scan(String... basePackages) {...}

    public <T> void registerBean(@Nullable String beanName, Class<T> beanClass, @Nullable Supplier<T> supplier, BeanDefinitionCustomizer... customizers) {...}
}

AnnotationConfigApplicationContext 클래스 파일을 열어보면, 이렇게 정의되어 있다.

강의에 언급된 것처럼 AnnotatedBeanDefinitionReader도 선언되어 있는 것을 확인할 수 있다. 이것을 활용하여 구성 정보 파일을 읽고, 빈 메타정보를 생성한다.

강의에 있는 BeanDefinition을 확인하는 테스트 코드를 실행하면 이렇게 메타 정보를 조회할 수도 있다. (BeanDefinitionTest.class에서 선언한 findApplicationBean() 메소드)

BeanDefinition이 무슨 역할을 담당하고 있는지는 굳이 외울 필요 없다. 필요할 때 공식 문서에서 찾아보면 된다. (실제로 별로 사용할 일 없다고 한다.)

여기서 중요한 것은 스프링 컨테이너가 어떻게 구성 파일을 읽고 BeanDefinition을 생성하는지에 대한 일련의 과정이다. 이것만 기억하고 있으면 될 것 같다.

 


References

스프링 핵심 원리 - 기본편