[Spring 핵심 원리 이해] 3. 스프링 컨테이너와 스프링 빈

2024. 12. 20. 12:55·Spring

저번 시간에 애플리케이션 전체의 구성하는 AppConfig가 어떻게 제어의 역전과 의존관계 주입을 할 수 있는지

그리고 스프링을 이용한 스프링 컨테이너와 스프링빈으로 AppConfig를 어떻게 활용할 수 있는지 알아보았다. 

이번 시간에는 스프링 컨테이너와 스프링 빈에 대해서 조금 더 알아보도록 하자!

 

1. 스프링 컨테이너 

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

 

이런 코드를 통해서 우리는 OrderApp과 MemberApp에 스프링 컨테이너를 활용했었다. 

이 중 ApplicationContext를 우리는 스프링 컨테이너라고 한다. 

그런데 ApplicationContext와 AnnotaionConfigApplicationContext는 유사해보이는데 뭐가 다른 것일까? 

 

ApplicationContext는 '인터페이스'이다. 

그리고 AnnotaionConfigApplicationContext가 구현체인 것이다. 

스프링 컨테이너는 XML 기반으로도 만들 수 있고, 에노테이션 기반의 자바 설정 클래스로도 만들 수 있다. 

위 코드는 자바 설정 클래스를 기반으로 스프링 컨테이너를 만든 방법이다. 

 

 

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

스프링 컨테이너를 생성할 때 구성정보를 지정해줘야 한다.

AppConfig.class가 그 역할을 수행한다. 

그럼 AppConfig는 @Configuration 이 붙었은 클래스이다. 해당 클래스에서 @Bean 이 붙은 메서드들을 스프링 머테이너에 등록하기 시작한다. 

 

스프링 빈 저장소에는 (빈 이름, 빈 객체) 이런 식으로 스프링 빈이 저정된다. 

 

package hello.core_review;

import hello.core_review.Discount.DiscountPolicy;
import hello.core_review.Discount.FixDiscountPolicy;
import hello.core_review.member.MemberRepository;
import hello.core_review.member.MemberService;
import hello.core_review.member.MemberServiceImpl;
import hello.core_review.member.MemoryMemberRepository;
import hello.core_review.order.OrderService;
import hello.core_review.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

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

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

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

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

 

AppConfig의 내용은 위와 같다. 

그렇다면 

스프링 컨테이너에는 

출처 - 김영한 스프링 강의

이와 같이 스프링 빈이 저장되어 있을 것이다. 

스프링 컨테이너는 파라미터러 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록한다. 

 

빈 이름은 메서드 이름을 사용하는게 기본적이지만, 이름을 직접 부여할 수도 있다. 

ex) @Bean(name = "memberService2")

 

그리고 스프링 컨테이너는 설정 정보를 참고해서 의존 관계를 주입한다. 

단순히 자바 코드를 호출하는 것과는 차이가 있다. 

출처 - 김영한 스프링 강의

 

정말 등록됐는지 확인하는 방법은 Junit을 통해 테스트 해보면 된다. 

 

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

@Test
@DisplayName("모든 빈 출력하기")
void findAllBean() {
    String[] beanDefinitionNames = ac.getBeanDefinitionNames();

    for (String beanDefinitionName : beanDefinitionNames) {
        Object bean = ac.getBean(beanDefinitionName);
        System.out.println("name = " + beanDefinitionName + " Object = " + bean);
    }
}

 

먼저 AppConfig에 등록된 모든 빈을 출력해보겠다. 

.getBeanDefinitionNames() 를 통해서 스프링에 등록된 모든 빈 이름을 조회할 수 있다. 

그리고 이 이름을 이용해서 ac.getBean()을 이용해 등록된 모든 빈을 출력한다. 

 

그런데 출력해보면 내가 등록한 빈만 존재하는게 아니라 스프링 내부에서 사용하는 빈까지 모두 출력되는 모습을 볼 수 있다. 

@Test
@DisplayName("애플리케이션 빈 출력하기")
void findApplicationBean() {
    String [] beanDefinitionNames = ac.getBeanDefinitionNames();
    for (String beanDefinitionName : beanDefinitionNames) {
        BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

        if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("name = " + beanDefinitionName + " Object = " + bean);
        }
    }
}

이럴 때는 .getBeanDefinition을 사용해서 BeanDefinition을 추출하고. 

이 추출한 BeanDefinition이 ROLE_APPLICATION인지 확인해주면 된다. 이는 사용자가 정의한 애플리케이션 빈인지 확인하는 것이다. 

참고로 ROLE_INFRASTRUCTURE는 스프링 내부에서 사용하는 빈을 확인하는 것이다. 

 

3. 스프링 빈 조회 

스프링 빈을 조회하는 기본적인 방법으로는 인자로 

 

- 빈 이름, 타입을 주는 방법 -> ac.getBean(빈이름, 타입)

- 타입을 주는 방법  -> ac.getBean(타입)

 

이렇게 두 가지가 있다. 만약 조회 대상 스프링 빈이 없다면 예외가 발생한다. 

 

타입으로 조회하는 방법은 추상 타입이 아니라 구체 타입으로 조회하는 방법도 존재하지만 유연성이 떨어지는 단점이 있다.  

package hello.core_review.beanfind;

import hello.core_review.AppConfig;
import hello.core_review.member.MemberService;
import hello.core_review.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ApplicationConfigApplicationContext {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName() {
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("빈 이름 없이 타입만으로 조회")
    void findBeanByType() {
        MemberService memberService = ac.getBean( MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("구체 타입으로 조회")
    void findBeanByName2() {
        MemberService memberService = ac.getBean("memberService", MemberServiceImpl.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("빈 이름으로 조회 X")
    void findBeanByNameX() {
//        ac.getBean("xxxxx", MemberService.class);
        org.junit.jupiter.api.Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean("xxxxx", MemberServiceImpl.class));
    }

}

 

 

그런데 타입으로만 빈을 조회할 때 같은 타입인 스프링 빈이 두개 이상 존재한다면 문제가 생긴다.

이럴때는 빈 이름을 함께 지정해주어야 한다. 

또한 특정 타입의 빈을 모두 조회하고 싶다면 .getBeanOfType()를 이용하고, 인자로 타입을 넣어주면 된다.

그럼 Map자료구조 형태로 반환된다. 

 

package hello.core_review.beanfind;

import hello.core_review.member.MemberRepository;
import hello.core_review.member.MemoryMemberRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

public class ApplicationContextSameBeanContext {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류가 발생")
    void findBeanByTypeDuplicate() {
        assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(MemberRepository.class));
    }

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있다면, 빈 이름을 지정하면 된다")
    void findBeanByName() {
        MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class);
        assertThat(memberRepository).isInstanceOf(MemberRepository.class);
    }

    @Test
    @DisplayName("특정 타입을 모두 조회하기")
    void findAllBeanByType() {
        Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);

        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
        System.out.println("beansOfType = " + beansOfType);
        assertThat(beansOfType.size()).isEqualTo(2);

    }


    @Configuration
    static class SameBeanConfig {

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

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

 

 

스프링 빈 조회시 상속 관계는 어떻게 될까? 

구현 객체들은 모두 interface를 상속하고 있다. 

스프링 빈 조회시 부모 타입으로 조회하면 자식 타입도 함께 조회된다.

 

그런데 부모타입으로 조회시 자식 타입이 둘 이상 있으면 중복 오류가 발생한다. 

그럴 때는 빈 이름을 함께 지정해주어야 한다.

Object 클래스는 모든 타입의 부모임으로 해당되는 모든 자식 속성이 출력된다. 

 

package hello.core_review.beanfind;

import hello.core_review.Discount.DiscountPolicy;
import hello.core_review.Discount.FixDiscountPolicy;
import hello.core_review.Discount.RateDiscountPolicy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

public class ApplicationContextExtentsFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면 중복 오류 발생")
    void findBeanByParentTypeDuplicate() {
        assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(DiscountPolicy.class));
    }

    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면 빈 이름 지정")
    void findBeanByParentTypeBeanName() {
        DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy1", DiscountPolicy.class);
        assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("특정 하위 타입으로 조회")
    void findBeanBySubType() {
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
        assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기")
    void findAllBeanByParentType() {
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);

        assertThat(beansOfType.size()).isEqualTo(2);

        for (String key : beansOfType.keySet()) {
            System.out.println("ket = " + key + " value = " + beansOfType.get(key));
        }
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기 - Object")
    void findAllBeanByObjectType() {
        Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
    }


    @Configuration
    static class TestConfig {
        @Bean
        public DiscountPolicy rateDiscountPolicy1() {
            return new RateDiscountPolicy();
        }

        @Bean
        public DiscountPolicy rateDiscountPolicy2() {
            return new FixDiscountPolicy();
        }
    }
}

 

 

4. BeanFactory와 ApplicationContext

출처 - 김영한 스프링 강의

 

BeanFactory는 스프링 컨테이너의 최상위 인터페이스이다. 스프링 빈을 관리하고 조히하는 역할을 담당한다. 

getBean() 메서드를 제공하는 인터페이스이다. 

 

ApplicationContext 는 BeanFactory를 상속받아 제공한다.

ApplicationContext는 BeanFactory 이외의 다른 인터페이스도 상속 받기 때문에 애플리케이션을 개발할 때 필요한 부가 기능들을 지원해줄 수 있다. 

출처 - 김영한 스프링 강의

이렇게 다양한 인터페이스들을 상속받는다. 

BeanFactory나 ApplicationContext를 스프링 컨테이너라고 부른다. 

 

이전에 우리는 스프링 컨테이너를 설계하는 방식이 애노테이션 기반 코드 설정 말고도 XML설정을 사용하는 방법이 있다고 말하였다. 

출처 - 김영한 스프링 강의

이렇게 ApplicatioinContext를 상속받는 클래스들이 존재한다. 우리는 그 중 애노테이셔 기반 코드를 이용한 AnnotationConfigApplicationContext를 이용해왔다. 

xml 을 작성해서 이를 스프링 빈에 등록해주면 된다 

 


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://
 www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="memberService" class="hello.core_review.member.MemberServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
    </bean>
    <bean id="memberRepository"
          class="hello.core_review.member.MemoryMemberRepository" />
    <bean id="orderService" class="hello.core_review.order.OrderServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
        <constructor-arg name="discountPolicy" ref="discountPolicy" />
    </bean>
    <bean id="discountPolicy" class="hello.core_review.Discount.RateDiscountPolicy" />
</beans>

 

package hello.core_review.xml;

import hello.core_review.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class XmlAppContext {

    @Test
    void xmlAppContext() {
        ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");

        MemberService memberService = ac.getBean("memberService", MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

 

 

5. 스프링 빈 설정 메타 정보 - BeanDefinition

스프링은 BeanDefinition이라는 추상화를 통해서 이렇게 다양한 설정 형식을 지원한다. 

 

이 또한 역할과 구현을 나눈 것으로 이해하면 된다. 

스프링 컨테이너는 BeanDefinition을 의존하기 때문에 이를 빈 설정 메타 정보라고 한다. 

스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다

출처 - 김영한 스프링 강의

더 크게 보면 

출처 - 김영한 스프링 강의

이렇게 생각할 수 있다. 결국은 BeanDefinition이라는 빈 메타정보가 생성되고 이를 스프링 컨테이너가 읽는다. 

 

BeanDefinition은 

  • BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
  • factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
  • factoryMethodName: 빈을 생성할 팩토리 메서드 지정, 예) memberService
  • Scope: 싱글톤(기본값)
  • lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연 처리 하는지 여부
  • InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
  • DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
  • Constructor arguments, Properties: 의존관계 주입에서 사용한다. (자바 설정 처럼 팩토리 역할의 빈을 사용 하면 없음)

이런 정보들을 포함하고 있다. 

 

'Spring' 카테고리의 다른 글

[Spring 핵심 원리 이해] 6. 의존관계 자동 주입  (6) 2025.01.03
[Spring 핵심 원리 이해] 5. 컴포넌트 스캔  (3) 2024.12.28
[Spring 핵심 원리 이해] 4. 싱글톤 컨테이너  (4) 2024.12.27
[Spring 핵심 원리 이해] 2. 객체 지향 원리 적용  (6) 2024.12.18
[Spring 핵심 원리 이해] 1. 예제 만들기  (6) 2024.12.07
'Spring' 카테고리의 다른 글
  • [Spring 핵심 원리 이해] 5. 컴포넌트 스캔
  • [Spring 핵심 원리 이해] 4. 싱글톤 컨테이너
  • [Spring 핵심 원리 이해] 2. 객체 지향 원리 적용
  • [Spring 핵심 원리 이해] 1. 예제 만들기
코뮝
코뮝
  • 코뮝
    코뮝의 기술 블로그
    코뮝
  • 전체
    오늘
    어제
    • 분류 전체보기 (25)
      • DB (1)
      • JAVA (0)
      • 운영체제 (11)
      • Spring (9)
      • 알고리즘 (2)
      • 카테캠 3기 (1)
      • DevOps (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Java
    스프링
    기아 스레드
    카태켐3기
    명품 운영체제
    스레드 매핑
    참조의 지역성
    역페이지테이블
    고정 크기 할당
    생산자 소비자 문제
    김영한
    CS
    타조 알고리즘
    Spring
    백엔드
    가변 크기 할당
    시스템 콜
    스프링 기본
    자바
    운영체제 공부
    OS
    코스만 조건
    프로그래머스 유연근무제
    분할 메모리 할당
    원자 명령
    카태켐
    커널 레벨 스레드
    기초 cs
    운영체제
    카테캠 백엔드
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
코뮝
[Spring 핵심 원리 이해] 3. 스프링 컨테이너와 스프링 빈
상단으로

티스토리툴바