티스토리 뷰

인프런의 김영한님의 Spring 강의를 들으며 공부한 내용을 복습 차원에서 작성한 글이다.

이번 글에서는 스프링 컨테이너와 빈 등록(자동/수동) 방법에 대해 알아보겠다.

 


 

스프링 컨테이너란?

스프링 컨테이너자바 객체의 생명 주기를 관리하며, 생성된 자바 객체들에게 추가적인 기능을 제공하는 역할을 한다.

여기서 말하는 자바 객체를 스프링에서는 빈(Bean) 이라고 부릅니다.

여기 글에서는 다루지 않지만, IoC와 DI의 원리가 이 스프링 컨테이너에 적용이 된다.

(IoC와 DI, 제어와 역전 그리고 의존성 주입에 대해 잘 모르신다면 알아서 알아오기~)

 

개발자가 new 연산자, 인터페이스 호출 등등의 방식으로 객체를 생성하고 소멸시킬 수 있는데, 이 작업을 스프링 컨테이너가 대신해 줍니다. 즉, 제어 흐름을 외부에서 관리하는 것이다.

또한, 객체들 간의 의존 관계를 스프링 컨테이너가 런타임 과정에서 알아서 만들어 줍니다.

DI(의존성 주입)는 생성자, setter, @Autowired 를 통해 적용한다. 

(현재 글은 전부 알고 있다는 가정하에 내용을 정리한다.)

 


 

 

스프링 컨테이너의 종류

스프링 컨테이너는 크게 2가지로 BeanFactory와 ApplicationContext 가 있습니다.

 

1. BeanFactory

기본적인 의존성 주입을 지원하는 가장 간단한 형태의 컨테이너다. (스프링 컨테이너의 최상위 인터페이스)

BeanFactory는 빈을 등록하고 생성하고 조회하고 돌려주는 등 빈을 관리하는 역할을 한다.

getBean() 메서드를 통해 빈을 인스턴스화 할 수 있다.

 

2. ApplicationContext 

ApplicationContext 또한 BeanFactory와 동일하게 빈을 관리할 수 있다.

BeanFactory를 구현하고 있는 하위 인터페이스로 여러가지 빈 관리기능과 추가적인 편의기능이 추가 된 컨테이너다.

단순하게 Bean을 관리하는 것을 떠나서, 애플리케이션을 개발하기 위해 공통적으로 필요한 많은 부가 기능을 제공하기 위해 ApplicatioContext를 사용한다.

ApplicationContext가 제공하는 부가기능

 

부가 기능들은 하나씩 간단하게 살펴보겠다.

(보통 애플리케이션 개발을 위해 공통적으로 필요한 부가 기능들 이라고 한다.)

 

◆ 메시지소스를 활용한 국제화 기능

    - 간단한 예로 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력하는 부가 기능 제공

 

◆ 환경 변수

    - 실제 운영 단계에 들어서면 3가지 환경(로컬, 개발, 운영)이 있다.

    - 환경 별로 로컬, 개발, 운영들을 구분해서 처리한다.

 

◆ 애플리케이션 이벤트

    - 이벤트를 발행하고 구독하는 모델을 편리하게 지원한다.

 

◆ 편리한 리소스 조회

    - 파일, 클래스 패스, 외부 등에서 리소스를 편리하게 조회할 수 있도록 한다.

    - 추상화해서 편리하게 쓸 수 있도록 도와준다.

 

그리고 ApplicationContext 는 구현체로 애노테이션 기반 자바코드 설정 방식인  AnnotationConfigApplicationContext 을 주로 사용한다. (그 외 xml 설정 방식은 GenericXmlApplicationContext 등등 존재함)

 

암튼 여기까지 요약하자면 BeanFactory 자체를 사용하지 않고 부가 기능이 추가 된 ApplicationContext 를 사용한다고 생각하면 된다.

BeanFactory를 직접 사용하는 경우가 거의 없어, 일반적으로 ApplicationContext를 스프링 컨테이너라고 한다.


 

이제 실제로 스프링 컨테이너가 생성되는 개념적인 과정을 그림과 함께 이해해 보자.

 

1. 스프링 컨테이너 생성

첫 번째로는 스프링 컨테이너가 생성이 되면 위 그림과 같이 스프링 빈 저장소라는 것이 생성된다.

 

간단하게 코드로 예를 들어보면

// 구성,설정 정보 
@Configuration
public class AppConfig {
	
    @Bean 
    public Container containerBean() {
        return new Container();
    }
    
    @Bean
    public Test	testBean() {
    	return new Test();
    }
 }
public class MemberApp {

    public static void main(String[] args) {
    	// ApplicationContext (스프링 컨테이너) 생성
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    }
}

 

코드를 보고 아래 그림을 보자

1. 스프링 컨테이너의 생성

코드에서 보는 것 같이 new AnnotationConfigApplicationContext(AppConfig.class) 를 호출하여 컨테이너를 생성해주었다.(인터페이스는 ApplicationContext 사용)

 

스프링 컨테이너가 생성이 되면 위 그림과 같이 구성 정보를 활용하여 스프링 빈 저장소를 만든다.

여기서는 스프링 컨테이너를 생성할 때 같이 넘겨준 AppConfig.class 가 해당한다.

 

 

2. 스프링 빈 등록

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

@Bean 어노테이션이 붙은 메서드를 모두 실행해서 반환되는 객체를 스프링 컨테이너 내에 있는 빈 저장소에 등록한다.

 

빈 저장소에는 빈 이름(메서드 이름이 기본값)과 빈 객체의 key, value 쌍으로 저장된다.

 

2. 스프링 빈 등록

다시 한번 위의 코드를 보고 오자. 

우리가 간단하게 코드를 작성해본 것으로 얘기를 하자면, 위 그림에 빈 이름에는 우리가 @Bean 어노테이션을 붙여준 메서드인 containerBean 과 testBean 이 들어가게 된다.

 

빈 이름은 기본적으로 메서드 이름이 들어가지만 빈의 이름을 직접 부여할 수도 있다.

// 구성,설정 정보 
@Configuration
public class AppConfig {
	
    @Bean 
    public Container containerBean() {
        return new Container();
    }
    
    @Bean(name = "testBBB")
    public Test	testBean() {
    	return new Test();
    }
 }

 

위와 같은 코드에서는 스프링 빈 저장소의 빈 이름에 containerBean 과 testBBB 가 들어가게 된다.

 

추가로 빈 이름은 중복되지 않게 다른 이름을 부여해야 한다.

같은 이름을 부여하면, 다른 빈이 무시되거나 기존 빈을 덮어버리는 등 설정에 따라 오류가 발생할 수 있기 때문이다.

 

 

3. 스프링 빈 의존관계 설정 - 준비 및 완료

 

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

3번 4번 과정에서 볼수 있듯이 스프링 컨테이너는 설정 정보(여기서는 AppConfig)를 참고해 의존관계를 주입한다.

 

실제로 스프링 빈을 등록하면, 생성자를 호출하면서 의존관계 주입도 한 번에 처리된다고 한다.

 


 

스프링 빈 조회 방법

 

컨테이너에 등록된 모든 빈을 조회하는 방법

 

다음 코드와 같이 테스트 코드를 작성하여(JUnit 이용) 스프링 컨테이너에 올라간 모든 빈을 조회할 수 있다.

스프링 컨테이너를 생성 후에 getBeanDefinitionNames 메서드를 이용하여 모든 빈의 이름을 배열로 반환받는다.

그 후에 반복문을 통해 빈 이름 하나하나 꺼내어 getBean 메서드에 넣으면 빈 인스턴스(객체)가 반환된다.

class TestMain {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    void findAllBean() {
        // getBeanDefinitionNames 메서드를 이용해 모든 빈의 이름을 반환 받는다.
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("beanDefinitionName = " + beanDefinitionName + " object = " + bean);
        }
    }
}

 

테스트 결과를 보면 잘 모르겠지만 내가 만든 빈을 제외하고도 몇개 정도가 더 조회되는 걸 확인할 수 있다.

 

 

우리가 원하지 않은 정보를 빼고 개발자가 직접 등록해준 빈 만을 조회하기 위해서는 다음과 같이 작성한다.

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

            //Role ROLE_APPLICATION: 직접 등록한 애플리케이션 빈
            //Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + " object = " + bean);
            }
        }
    }

 

여기서는 BeanDifinition 을 이용해 빈의 역할에 따라 직접 등록한 애플리케이션 빈 만을 조회할 수 있도록 구현할 수 있다.

개발자가 직접 등록한 빈 만이 조회되는 것을 확인할 수 있다.

 

 

스프링 빈 조회 - 기본

가장 기본적인 스프링 빈을 조회하는 방법을 하나씩 알아보겠다.

 

1. 빈 이름과 object type으로 찾는 방법

@Test
void findBeanByName() {
    // bean 이름, bean Object 타입을 인자로 받는다.
    BeanTest beanTest = ac.getBean("beanTest", BeanTest.class);

    beanTest.test();

    // 반환받은 객체가 BeanTest class의 Instance인지 검증한다.
    Assertions.assertThat(beanTest).isInstanceOf(BeanTest.class);
}

 

아래 결과를 보면 beanTest 라는 빈 이름을 가진 인스턴스(객체)를 반환 받았고 그 객체의 test() 메서드도 실행해보았다.

그리고 해당 객체가 Instance인지도 확인 해본 결과 테스트가 정상적으로 실행되는 게 확인 가능하다.

(다음 부터는 생략)

 

현재는 테스트 하느라 추상, 구현 이런걸 신경 쓰지 않았지만, 단일 빈 조회 시에 추상이 아닌 구체의 타입으로도 조회가 가능하다.

(그리 권장되는 방법은 아니라고 한다.)

 

 

2. 이름 없이 타입으로만 조회하는 방법

@Test
void findBeanByType() {
    // type으로만 찾을수도 있다.
    BeanTest bean = ac.getBean(BeanTest.class);
}

 

위 코드와 같이 타입만으로 조회할 수 있으나, 같은 타입의 빈이 여러개 있을경우 문제가 발생할 수 있다.

 

 

스프링 빈 조회 - 동일한 타입이 둘 이상인 경우

이렇게 중복된 타입의 빈을 다음과 같이 테스트 환경에 만들어준다.

두 개의 스프링 빈(beanTest1, beanTest2) 이 모두 BeanTest 라는 같은 타입을 갖는 구조이다.

@Configuration
class SameBeanConfig {

    @Bean
    public BeanTest beanTest1() {
    	return new BeanTest();
    }
    
    @Bean
    public BeanTest beanTest2() {
        return new BeanTest();
    }

 

 

1. 빈 이름을 이용하여 스프링 빈 조회

@Test
void findBeanByNameDuplicate() {
	BeanTest bean1 = ac.getBean("beanTest1", BeanTest.class)
}

 

 

2. 같은 타입을 갖는 모든 빈을 조회

@Test	
void findAllBeanByType() {
	Map<String, BeanTest> beanOfType = ac.getBeansOfType(BeanTest.class);
    
    for (String key : beansOfType.ketSet()) {
    	System.out.println("key = " + key + " value = " + beansOfType.get(key));
    }
}

 

getBeansOfType 메서드를 이용하여 스프링 빈 객체들을 key, value 형태로 반환한다.

반복문을 통하여 ketSet 메서드를 이용해 key 값을 하나하나 반환받고 get(key) 를 통해 해당 key(스프링 빈 이름)의 객체를 반환받는 방식이다. 

 

 

스프링 빈 조회 - 상속 관계에 있는 빈

이 방법은 방법이라기보다는 원칙이라고 해야할거같다. 그러므로 대충 설명하고 넘어간다.

부모 타입으로 스프링 빈을 조회하는 경우 자식 타입이 모두 조회된다는 점이다.

 

예를 들어 A 타입, B 타입이 있다고 하자.

아래와 같은 방식으로 이름 없이 타입으로 조회하는 방식이다.

A a = ac.getBean(A.class); // A 객체 반환

B b = ac.getBean(B.class); // B 객체 반환

 

예를 들어 모든 자바 객체의 최고 부모인 Object 타입으로 스프링 빈을 조회하는 경우, 모든 스프링 빈을 조회한다.

(즉, A와 B가 모두 조회된다.)

 


 

 

자동 빈 등록하는 방법

사실 지금까지 AppConfig 설정 정보를 작성하여 스프링 컨테이너를 생성하기 위해AnnotationConfigApplicationContext(AppConfig.class) 를 호출하여 컨테이너를 생성하고 @Bean 어노테이션이 붙은 메서드를 빈 저장소에 저장하는 수동 빈 등록을 해준 것이다.

 

빈 등록을 자동으로 하려면 어떻게 해야할까..?

 

바로 @ComponentScan, @Component 와 @Autowired 어노테이션을 사용하면 된다.

 

@ComponentScan : @Component 애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다. 이때 스프링 빈의 기본 이름은 클래스명의 앞글자만 소문자로 바꾸어 사용한다. 추가로 @Configuration 설정 정보도 자동으로 등록된다.

@Component : 컴포넌트 대상을 지정할 때 사용한다. (이 어노테이션이 붙어있어야 스프링 빈으로 등록이 된다.)

@Autowired : 컴포넌트 스캔 사용 의존 관계 주입을 위해 생성자에 @Autowired 사용한다.  

 

코드를 통해 간단하게 알아보자.

우선 설정 정보를 작성한다.

@Configuration
@ComponentScan
public class AutoConfig {
}

 

설정 정보(@Configuration 붙어야 함) 클래스 레벨 위에 @ComponentScan을 붙여주면 된다.

 

스프링 빈에 등록할 클래스는 아래와 같이 @Component 어노테이션을 붙여준다.

@Component
public class BeanTest implements MainTest {

    @Override
    public void test() {
        System.out.println("스프링 빈 테스트입니다.");
    }
}

 

@ComponentScan 이 자동으로 클래스 경로들을 찾아보며 @Component 어노테이션이 붙은 클래스들을 스프링 빈에 자동으로 등록해 준다. 굳이 설정 파일에 클래스들을 작성할 필요가 없어 진 것이다.

 

그럼 의존 관계 주입은 어떻게 할까?

의존 관계 주입을 생성자, 메서드, 필드 전부 받을 수 있는 데, 주로 사용하는 생성자 주입으로 알아보자.

@Autowired 어노테이션을 생성자 위에 붙어준다. (생성자가 한 개밖에 없다면 생략가능)

// @Component("serviceHello") 빈 이름을 지정해 줄 수도 있음
@Component
public class ServiceT {

    MainTest test;

    @Autowired  // 생성자가 하나 밖에 없다면 @Autowired 생략 가능
    public ServiceT(MainTest beanTest) {
        this.test = beanTest;
    }

    public void write() {
        test.test();
    }
}

 

 

 

이렇게만 작성해주면, 실행 시점에 스프링 컨테이너가 생성되고 스프링 빈에 BeanTest가 등록이 되고 @Autowired에 BeanTest 가 주입이 된다. 

 

여기서 의문이 들 수 있는데 "어라? 파라미터로 MainTest 가 들어가있는데 왜 BeanTest 가 주입이 되는 거죠?"

BeanTest 가 MainTest의 구현체이기 때문에 알아서 스프링에서 등록해 준다. 

(MainTest에 @Component 를 붙이거나 혹은 MainTest의 다른 구현체에 @Component 가 붙어 있으면UnsatisfiedDependencyException 예외가 발생한다.)

 

BeanTest 를 작성해서 주입 받아도 될 텐데 굳이 인터페이스인 MainTest 를 주입받는 이유는 OCP 원칙을 준수하기 위해서이다.

이렇게 되면 구현체를 바꾸고 싶을 때, 코드를 수정할 필요 없이 구현체에 @Component 를 붙여주기만 하면 쉽게 구현체를 변경할 수 있기 때문이다.

 

테스트 코드를 작성해보자.

public class TestAuto {
    @Test
    @DisplayName("자동 빈 등록 테스트")
    void AutoBeanTest() {

        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);

        ServiceT service = ac.getBean(ServiceT.class);

        service.write();
    }
}

 

 

@DisplayName 어노테이션은 테스트 이름을 지정할 수 있게 해주는 어노테이션이다.

코드와 테스트 결과를 확인해 보면 쉽게 이해 할 수 있을 것이다.