스프링 컨테이너에 실제 스프링 빈들이 잘 등록 되었는지 확인해봅시다.

test.hello.core에 beanfind 패키지를 만듭니다.

Untitled

그리고 ApplicationContextInfoTest를 만듭니다.

Untitled

package hello.core.beanfind;

import hello.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

class ApplicationContextInfoTest {
		//...1
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈 출력하기")
    void findAllBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames(); //...2
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName); //...3
            System.out.println("name = " + beanDefinitionName + " object = " + bean);
        }
    }
}
  1. AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); 에서 AnnotationConfigApplicationContext 구현체 타입을 사용한 이유가 있을까요?

    실제로 AnnotationConfigApplicationContext 타입에서만 getBeanDefinition 메서드를 사용할 수 있습니다.

    AnnotationConfigApplicationContext이 ApplicationContext의 구현체인데 왜 getBeanDefinition을 못할까요?

    SOLID 중 하나인 인터페이스 분리 원칙 ISP(Interface Segregation Principal)을 지키기 때문입니다.

    쉽게 말하면 "인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 합니다".

    예를 들면

    Untitled

    프린터 기능만 이용하는 클라이언트가 팩스 기능의 변경으로 인해 발생하는 문제의 영향을 받지 않도록 해야합니다.

    즉 클라이언트에 특화된 인터페이스를 사용해야 합니다.

    Untitled

    프린터 인터페이스는 print 메서드만 구현하면 됩니다. 나머지는 전혀 신경쓰지 않습니다. 즉, fax 메서드가 변경이 있어도 프린터 클라이언트는 전혀 영향을 받지 않습니다.

    코드 참고


    여기서 또 다른 의문점이 생깁니다.

    MemberApp에서도 AnnotationConfigApplicationContext을 구현체의 데이터타입으로 사용해도 문제가 되지 않을 것같은데 인터페이스인 ApplicationContex를 사용한 이유는 무엇일까요?

    예를 들어 Map store = new HashMap()HashMap store = new HashMap()의 비교를 해봅시다.

    이렇게 둘이 비교해서 이점은 다음과 같습니다.

    1. Map 인터페이스의 제약을 따르겠다는 의도를 명확하게 드러냅니다.

      <aside> 💡 객체가 인터페이스를 사용하면, 인터페이스 메서드를 반드시 구현해야 하는 제약을 합니다.

      </aside>

    2. 사용하는 코드가 Map 인터페이스 제약을 따르기 때문에 향후 변경시에 사용코드를 변경하지 않아도 됩니다.

    3. HashMap을 다른 클래스로 변경이 필요하면 선언하는 코드만 변경하면 됩니다. 사용하는 코드를 고민하지 않아도 됩니다.

    4. 다른 개발자들이 이 코드를 나중에 더 성능이 좋거나 동시성 처리가 가능한 종류의 구체적인 Map으로 변경해야 할 때 HashMap store = new HashMap()이라고 되어 있다면, 변경 시점에 상당히 많은 고민을 해야 하지만 Map store = new HashMap()으로 선언이 되어 있다면 편안하게 선언부를 변경할 수 있습니다.

      Map이라는 공통된 인터페이스로 개발을 해두게 되면 나중에 HashMap이 아닌 TreeMap 등으로 바꿔야 하거나 특정한 Map을 구현하여 커스터마이징을 해야 된다고 하면, Map result = new HashMap()Map result = new TreeMap() 또는 Map result = new MyMap()으로만 바꿔주면 나머지는 코드에 손을 댈 필요가 없어집니다.

      좀 더 추가해서 말하면 구현 클래스에 추가로 메서드를 구현하고, 나중에 구현 객체를 사용할 때, 인터페이스 변수가 사용된다면, 추가로 구현된 메서드는 호출이 되지 않습니다.

      구현 클래스에서는 다른 구현된 메서드가 있을 수 있다는 뜻입니다. 그래서 인터페이스로 제약을 두게 되면 다른 구현 메서드에 대해서 고민을 할 필요가 없다는 의미가 됩니다.

      • 예시 코드
    5. 개발은 무의미한 자유도를 제공하는 것 보다, 제약을 부여하는 것이 혼란을 줄이고, 유지보수하기 쉽습니다.

    6. 만약 정말 HashMap의 구체적인 기능을 사용해야 한다면 HashMap store = new HashMap() 이라고 선언하는 것이 맞습니다.

  2. ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회합니다.

  3. ac.getBean() : 빈 이름으로 빈 객체(인스턴스)를 조회합니다.

🌟 [TIP]

JUnit5부터는 public 설정을 안해도 됩니다.

🌟 [TIP]

iter + TAP

리스트나 배열이 있으면 for문이 자동으로 완성이 됩니다.

실행을 해봅시다.

Untitled

위에 있는 것들은 스프링 내부적으로 스프링 자체를 확장하기 위해 사용하는 빈입니다.

참고로 appConfig도 빈으로 등록 됩니다.

그리고 밑의 4개가 우리가 등록한 빈입니다.

여기서 스프링 내부의 것들을 빼고 내가 등록한 것만 보고 싶다면?

앞의 코드에서 이어서 추가합니다.

...    

    @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);
            }

        }
    }

...
  1. getBeanDefinition() : 각각의 빈에 대한 메타 데이터 정보
  2. getRole() : ROLE이 총 3가지 있는데 하나는 안쓰고 가장 많이 쓰는게 ROLE_APPLICATION

실행을 해보면 등록한 5개가 나오게 됩니다.

Untitled

앞으로 필요할 때 마다 이 테스트를 돌려보면 어떤 빈들이 등록되는지 눈으로 확인할 수 있습니다.