반응형

# 싱글톤 컨테이너

## 싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리한다.
  • 스프링 컨테이너가 싱글톤 역할을 한다.
  • 싱글톤 레지스트리 : 싱글톤 객체를 생성, 관리하는 기능.

SingletonTest

public class SingletonTest {
    @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer() {

        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        // 1. 조회 : 호출할 때마다 객체 생성하는지 확인.
        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);

        // 참조값이 다른 것 확인.
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        Assertions.assertThat(memberService1).isSameAs(memberService2);
    }
}
  • 클라이언트의 요청(memberService 요청) 시 동일한 memberService를 반환(요청시마다 새로 생성하는게 아닌 이미 만들어진 객체(동일한 객체)를 반환).

## 싱글톤 방식의 주의점 (공유필드 조심! 항상 무상태 (stateless) 로 설계하기!)

  • 객체 인스턴스를 하나만 생성해서 공유하는 경우, 여러 클라이언트가 하나의 객체를 공유해서 사용하기 때문에 싱글톤 객체는 상태를 유지 (stateful) 하게 설계하면 안된다. -> 무상태 (stateless) 로 설계해야 한다.
  1. 특정 클라이언트에 의존적인 필드가 없어야 한다.
  2. 특정 클라이언트가 값을 변경할수 있는 필드가 없어야 한다.
  3. 읽기만 가능해야 하고 값을 수정하면 안된다.
  4. 필드 대신 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.

상태 유지로 설계된 경우 예시.

StatefulService (상태 유지)

package singleton;

public class StatefulService {

    private int price;  // 상태 유지하는 필드

    public void order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);

        // 문제점.
        this.price = price;
    }

    public int getPrice() {
        return price;
    }
}

statefulServiceTest

package singleton;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

public class StatefulServiceTest {

    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        // ThreadA : A 사용자 20,000 주문.
        statefulService1.order("userA", 20000);
        // ThreadB : B 사용자 150,000 주문.
        statefulService2.order("userB", 150000);

        // ThreadA : 사용자A 가 주문 금액을 조회.
        int price = statefulService1.getPrice();
        System.out.println("price = " + price);

        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(150000);
    }

    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}
  • 위 테스트 코드 실행 시  A사용자가 주문한 금액을 조회하면 아래와 같은 결과가 나온다.

  • 특정 클라이언트가 공유되는 필드의 값을 변경하여 문제 발생. (공유 필드는 조심해야 함)

무상태로 설계된 경우 예시. (위 상태 유지 코드를 아래와 같이 변경 가능)

package singleton;

public class StatefulService {
    public int order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);
        return price;
    }
}
package singleton;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

public class StatefulServiceTest {

    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        // ThreadA : A 사용자 20,000 주문.
        int userAPrice = statefulService1.order("userA", 20000);
        // ThreadB : B 사용자 150,000 주문.
        int userBPrice = statefulService2.order("userB", 150000);

        // ThreadA : 사용자A 가 주문 금액을 조회.
        System.out.println("price = " + userAPrice);

    }

    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}

## @Configuration 과 싱글톤

AppConfig

@Configuration
public class AppConfig {

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

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

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

    // discountPolicy 역할
    @Bean
    public DiscountPolicy discountPolicy() {
//        return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}
  • AppConfig 에서 로직을 살펴보면 아래와 같은 문제점이 존재한다는걸 확인할 수 있다.
@Bean memberService() 시 new MemoryMemberRepository() 호출

@Bean orderService() 시 new MemoryMemberRepository() 중복 호출

 

위 내용 관련하여 테스트해서 확인 진행

MemberServiceImpl 에 테스트용 메서드 생성.

package hello.core.member;

public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }

    // 테스트용
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

OrderServiceImpl 에 테스트용 메서드 생성.

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    // 테스트용
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

ConfigurationSingletonTest

package singleton;

import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ConfigurationSingletonTest {

    @Test
    void configurationTest() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        MemberRepository memberRepository1 = memberService.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();

        System.out.println("memberService > memberRepository = " + memberRepository1);
        System.out.println("orderService > memberRepository = " + memberRepository2);
        System.out.println("memberRepository = " + memberRepository);

        Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }
}
  • 테스트 코드를 실행해 보면 아래와 같은 결과를 확인할 수 있다. (동일함)

  • 동일한 인스턴스를 공유해서 사용중임을 확인할 수 있다. 
  • AppConfig 동작 시 아래와 같이 특정 메서드가 중복 호출될 것 같은데 실제로는 중복 없이 호출된다. (@Configuration)
AppConfig.memberService
AppConfig.memberRepository
AppConfig.memberRepository
AppConfig.orderService
AppConfig.memberRepository

## @Configuration 과 바이트코드 조작의 마법 

  • 스프링 컨테이너는 싱글톤 레지스트리임. (스프링 빈이 싱글톤이 되도록 보장)
  • AppConfig 에서 특정 메서드가 중복으로 호출될 것 같은데 실제로는 중복 없이 호출되는 비밀은 @Configuration 에 있다.
  • 아래와 같이 AnnotationConfigApplicationContext 에 파라미터로 넘긴 값(AppConfig) 은 스프링 빈으로 등록된다. 그렇기 때문에 AppConfig 도 스프링 빈으로 등록됨.
new AnnotationConfigApplicationContext(AppConfig.class);

ConfigurationSingletonTest 에서 등록된 AppConfig 조회 관련 테스트 코드 추가. (빈을 조회하여 클래스 정보 출력)

@Test
void configurationDeep() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    AppConfig bean = ac.getBean(AppConfig.class);

    System.out.println("bean = " + bean.getClass());
}

 

  • 위 테스트 코드 실행 시 아래와 같은 결과를 확인 할 수 있다. (순수한 클래스라면 정상적인 path가 출력)

  • 위와 같은 결과가 나온 이유는 스프링 빈이 CGLIB라는 바이트코드 조작 라이브러리를 사용, AppConfig 클래스를 상속받은 임의의 다른 클래스를 생성하고 그걸 스프링 빈으로 등록한 것.
  • 그래서 이름은 등록한 AppConfig 이지만, 바이트코드를 조작하여 작성된 다른 클래스 정보가 표시되는 것.
  • AppConfig@CGLIB 내부 로직을 예상해 보면 아마도, 등록된 빈이 없으면 신규로 빈을 등록하고, 존재하면 등록된 빈을 반환하도록 되어있을 것임 -> 그래서 AppConfig에서 중복없이 메서드 호출.

@Configuration 부착여부에 따라

  • @Configuration 을 붙이면 바이트코드를 조작하는 CGLIB 를 사용해서 싱글톤을 보장해준다.
  • @Configuration  붙이지 않고 @Bean 만 사용해도 스프링 빈으로 등록되지만, 싱글톤은 보장하지 않게됨. > AppConfig 실행 시, 중복된 메서드를 호출하게 된다. (중복 호출된 빈은 모두 동일하지 않음)
반응형
반응형

# 싱글톤 컨테이너

## 웹 애플리케이션과 싱글톤

  • 스프링은 기업용 온라인 서비스 기술을 지원하기 위해 탄생함.
  • 대부분의 스프링 애플리케이션은 웹 애플리케이션이고, 웹 애플리케이션은 여러 고객이 '동시'에 요청을 한다.
  • 현재 작업한 DI 컨테이너 (AppConfig)의 경우 고객이 요청을 하면 new로 반환. 그렇기 때문에 고객이 여러 번 요청하면 동일한 요청에 대해 객체가 여러개 생성되게 된다. 

SingletonTest : 요청 시 마다 새로운 객체 생성되는지 확인 위한 테스트

public class SingletonTest {

    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer() {
        AppConfig appConfig = new AppConfig();

        // 1. 조회 : 호출할 때마다 객체 생성하는지 확인.
        MemberService memberService1 = appConfig.memberService();
        MemberService memberService2 = appConfig.memberService();

        // 참조값이 다른 것 확인.
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        Assertions.assertThat(memberService1).isNotSameAs(memberService2);
    }
}
  • 위 테스트 코드 실행 시 아래와 같이 새로운 객체를 생성해서 호출하는걸 확인할 수 있음.

  • 기존에 작업한 스프링 없는 순수한 DI 컨테이너(AppConfig)의 경우 요청 시 마다 객체를 새로 생성한다. (고객 요청에 따라 객체 생성, 소멸이 일어나기 때문에 메모리 낭비가 발생한다.)
  • 해결방법 : 해당 객체가 1개만 생성, 공유할 수 있도록 설계. (싱글톤 패턴)

## 싱글톤 패턴

  • 디자인 패턴으로, 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 패턴.
  • private 생성자를 사용해서 외부에서 임의로 new 하지 못하도록 (객체 인스턴스를 2개 이상 생성하지 못하도록) 막아야 한다.

SingletonService : 싱글톤 패턴 적용.

public class SingletonService {

    // 1. static 영역에 객체를 1개만 생성해둔다.
    private static final SingletonService instance = new SingletonService();

    // 2. 객체 인스턴스 필요 시 해당 메서드를 통해서만 조회하도록 허용.
    public static SingletonService getInstance() {
        return instance;
    }

    // 3. 생성자를 private 로 선언 -> 외부에서 new 키워드 사용하지 못하도록 막음.
    private SingletonService() {
    }

    public void login() {
        System.out.println("싱글톤 객체 로직 호출.");
    }
}

SingletonTest : 싱글톤 패턴 적용여부 테스트, 새로운 객체 생성이 아닌 동일한 객체인지 확인.

@Test
@DisplayName("싱글톤 패턴 적용한 객체 사용 테스트.")
void singletonServiceTest() {
    // new SingletonService(); -> 컴파일 오류 발생.

    SingletonService singletonService1 = SingletonService.getInstance();
    SingletonService singletonService2 = SingletonService.getInstance();

    System.out.println("singletonService1 = " + singletonService1);
    System.out.println("singletonService2 = " + singletonService2);

    Assertions.assertThat(singletonService1).isSameAs(singletonService2);
}

싱글톤 패턴의 문제점

  • 싱글톤 패턴 구현하는 코드 자체가 많이 들어감.
  • 의존관계상 문제 존재. (클라이언트가 구체 클래스에 의존, DIP 위반 및 OCP 위반 가능성 높아짐)
  • 테스트에 어려움 존재.
  • 내부 속성 변경, 초기화 어려움.
  • 유연성이 떨어짐.
  • 위 문제들로 인해 안티패턴으로 불리기도 함.
반응형
반응형

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

## BeanFactory 와 ApplicationContext

  • 최상위 컨테이너인 BeanFactory (인터페이스) 존재하고 그걸 상속받는 ApplicationText (인터ApplicationContextㅔ이스) 존재.

BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스.
  • 역할 : 스프링 빈 관리, 조회
  • getBean() 제공.
  • BeanFactory 를 직접 사용할 일은 거의 없고, 주로 ApplicationContext 를 사용.

ApplicationContext

  • BeanFactory 의 기능을 모두 상속받아 제공.
  • BeanFactory 와 차이점 : AppicationContext의 경우 BeanFactory  뿐만 아니라 MessageSource (인터페이스), EnvironmentCapable (인터페이스), ApplicationEventPublisher (인터페이스), ResourceLoader (인터페이스) 등의 기능을 상속받음.
  • 메시지 소스를 활용한 국제화 기능 : 접속 국가에 따른 언어로 출력.
  • 환경변수 : 로컬, 개발, 운영 등을 구분하여 처리.
  • 애플리케이션 이벤트 : 이벤트를 발행, 구독하는 모델을 편리하게 지원함.
  • 편리한 리소스 조회 : 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회.

## 다양한 설정 형식 지원 (자바 코드, XML)

  • 스프링 컨테이너의 경우 다양한 형식의 설정 정보를 받아드릴 수 있도록 유연하게 설계되어 있음. (자바 코드, XML, Groovy 등)
  • ApplicationText를 구현한 것에는 아래와 같은 것들이 존재.

1. 애노테이션 기반 자바코드 설정 사용 (AppConfig.class)

  • AnnotationConfig (ApplicationContext)
  • new AnnotationConfigApplicationContext(AppConfig.class)
  • AnnotationConfigApplicationContext 클래스를 사용하면서 자바 코드로된 설정 정보 넘김.

2. XML 설정 사용 (appConfig.xml)

  • GenericXml ApplicationContext
  • 최근에는 거의 사용하지 않음.
  • 레거시 프로젝트에서 사용.
  • 장점 : XML 사용 시 컴파일 없이 빈 설정 정보 변경 가능.
  • GenericXmlApplicationContext 를 사용하면서 xml 설정 정보 넘김.

3. appConfig.xxx (임의 설정)

  • Xxx ApplicationContext

AppConfig.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.member.MemberServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository"/>
    </bean>

    <bean id="memberRepository" class="hello.core.member.MemoryMemberRepository"/>

    <bean id="orderService" class="hello.core.order.OrderServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository"/>
        <constructor-arg name="discountPolicy" ref="discountPolicy"/>
    </bean>

    <bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy"/>
</beans>

XmlAppContext : xml 관련 Test 코드.

package hello.core.xml;

import hello.core.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);
    }
}
  • xml 기반 appConfig.xml 과 자바 코드로 된 AppConfig.java 가 거의 비슷함. (xml 기반의 설정은 거의 사용하지 않음)  

## 스프링 빈 설정 메타 정보 (BeanDefinition)

  • 스프링 컨테이너는 자바 코드, XML 인지 몰라도 BeanDefinition (빈 설정 메타정보) 만 알면 된다. (스프링 컨테이너는 BeanDefinition (인터페이스) 에만 의존)
  • @Bean, <bean> 을 사용하게되면 각각 하나씩 메타 정보가 생성된다. (스프링 컨테이너의 경우 해당 메타 정보를 바탕으로 스프링 빈을 생성)
  • AnnotationConfigApplicationContext 의 경우 내부에 AnnotatedBeanDefinitionReader 를 사용, AppConfig.class를 읽고, BeanDefinition 생성.
  • GenericXmlApplicationContext 의 경우 내부에 XmlBeanDefinitionReader 를 사용, appConfig.xml 를 읽고, BeanDefinition 생성.

BeanDefinitionTest 

package hello.core.beandefinition;

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

public class BeanDefinitionTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 설정 메타정보 확인 Test")
    void findApplicationBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                System.out.println("beanDefinitionName = " + beanDefinitionName
                        + "beanDefinition = " + beanDefinition);
            }
        }
    }
}
  • 실무에서 BeanDefinition 을 직접 정의하거나 사용할 일은 거의 없음.
반응형
반응형

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

## 스프링 컨테이너 생성

@Configuration
public class AppConfig {
    ...
}
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
  • 스프링 컨테이너 : 위 코드에서 ApplicationContext 를 스프링 컨테이너라고 함. (ApplicationContext 는 인터페이스로 다형성이 적용되어 있다.)
  • 스프링 컨테이너 생성 : XML을 기반(요즘엔 거의 사용하지 않음)으로 만들 수 있고, 애노테이션 기반의 자바 설정 클래스로 생성 가능하다.
  • 위 코드에서 사용한 방식이 애노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든 것.
  • new AnnotationConfigApplicationContext(AppConfig.class) 는 ApplicationContext(인터페이스) 의 구현체.

1. 스프링 컨테이너 생성(AppConfig.class)

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
  • 스프링 컨테이너에는 내부에 스프링 빈 저장소가 존재 (스프링 빈 저장소는 빈 이름(key), 빈 객체(value)를 갖고 있음)
  • 구성 정보 지정 : 스프링 컨테이너 생성 시 파라미터로 AppConfig.class 로 구성 정보를 지정. 

2. 스프링 빈 등록

  • 스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용, 스프링 빈을 등록한다. (AppConfig.class 에서 @Bean 붙은 것에서 메서드명(빈 이름) 과 메서드의 리턴 값(빈 객체)에 맞게 등록)
@Configuration
public class AppConfig {

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

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

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

    // discountPolicy 역할
    @Bean
    public DiscountPolicy discountPolicy() {
//        return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}
  • 일반적으로 빈의 이름은 메서드 명을 사용하지만, @Bean(name="설정할 빈 이름") 으로 별도로 지정 가능하다. 
  • 주의할 점!!! '빈의 이름은 항상 다른 이름' 으로 부여. (빈 이름이 동일할 경우 다른 빈이 무시되거나, 기존 빈을 덮어버릴 가능성 존재)

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

  • 스프링 컨테이너는 설정 정보를 참고하여 의존관계 주입(DI) 진행.

## 스프링 컨테이너에 등록된 모든 빈 조회

  • TEST 코드 생성하여 스프링 컨테이너에 등록된 빈 조회.
public class ApplicationContextInfoTest {

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

}

  • 아래의 경우 직접 등록한 빈만 출력.
@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);
        }
    }
}

  • ROLE_APPLICATION : 직접 등록한 애플리케이션 빈.
  • ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈

## 스프링 빈 조회(기본)

  • 기본적인 조회 방법 : ac.getBean(빈이름, 타입) 또는 ac.getBean(타입)
  • 아래와 같이 빈 이름으로 조회 가능. (ac.getBean(빈이름, 타입))
public class ApplicationContextBasicFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 이름으로 조히")
    void finBeanByName() {
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        
        System.out.println("memberService = " + memberService);
        System.out.println("memberService.getClass() = " + memberService.getClass() );
    }
}

  • 아래와 같이 검증 로직 추가하여 검증 가능.
@Test
@DisplayName("빈 이름으로 조회")
void finBeanByName() {
    MemberService memberService = ac.getBean("memberService", MemberService.class);

    // 검증
    Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
  • 아래와 같이 빈 이름 없이, 타입으로만 조회 가능 (ac.getBean(타입))
@Test
@DisplayName("빈 타입으로만 조회")
void finBeanByType() {
    MemberService memberService = ac.getBean(MemberService.class);

    // 검증
    Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
  • 아래와 같이 구체 타입으로 조회 가능. (MemberServiceImpl)
@Test
@DisplayName("구체 타입으로 조회")
void finBeanByName2() {
    MemberServiceImpl memberService = ac.getBean(MemberServiceImpl.class);

    // 검증
    Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
  • 아래와 같이 빈 이름으로 조회되는게 없는 경우 예외 발생. (NoSuchBeanDefinitionException)
@Test
@DisplayName("빈 이름으로 조회 X")
void finBeanByNameX() {
    MemberService abc = ac.getBean("abc", MemberService.class);
}

  • 아래와 같이 검증 로직으로 변경 가능. (assertThrows 이용하여 ac.getBean() 실행 시 NoSuchBeanDefinitionException 가 발생한 경우 성공)
@Test
@DisplayName("빈 이름으로 조회 X")
void finBeanByNameX() {
    //MemberService abc = ac.getBean("abc", MemberService.class);

    // 검증
    assertThrows(NoSuchBeanDefinitionException.class,
            () -> ac.getBean("abc", MemberService.class));
}

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

  • 타입으로 빈 조회 시 동일한 타입이 둘 이상인 경우 오류 발생, 이를 해결하기 위해 빈 이름 지정.
  • 아래와 같이 TEST 코드 작성 후 실행 시 예외 발생. (NoUniqueBeanDefinitionException)
public class ApplicationContextSameBeanFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    @Test
    @DisplayName("타입 조회의 경우 동일한 타입 둘 이상이면 중복 오류 발생.")
    void finBeanByName() {
        ac.getBean(MemberRepository.class);
    }

    @Configuration
    static class SameBeanConfig {
        @Bean
        public MemberRepository memberRepository1() {
            return new MemoryMemberRepository();
        }

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

  • 아래와 같이 검증 로직으로 변경 가능. (assertThrows 이용하여 ac.getBean() 실행 시 NoUniqueBeanDefinitionException 가 발생한 경우 성공)
@Test
@DisplayName("타입 조회의 경우 동일한 타입 둘 이상이면 중복 오류 발생.")
void finBeanByTypeDuplicate() {
    // 검증
    Assertions.assertThrows(NoUniqueBeanDefinitionException.class,
            () -> ac.getBean(MemberRepository.class));
}
  • 아래와 같이 동일한 타입이 둘 이상인 경우 빈의 이름을 지정.
@Test
@DisplayName("타입 조회의 경우 동일한 타입 둘 이상이면 빈 이름 지정하면 된다.")
void finBeanByName() {
    MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class);

    // 검증
    assertThat(memberRepository).isInstanceOf(MemberRepository.class);
}
  • 아래와 같이 특정 타입을 모두 조회할 수 있음.
@Test
@DisplayName("특정 타입 모두 조회")
void finAllBeanByName() {
    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);
}

## 스프링 빈 조회 (상속 관계)

  • 스프링 빈 조회 시 '부모 타입'으로 조회 하면 자식 타입이 함께 조회된다.
  • 모든 자바 객체의 부모 : Object 타입
  • Object 타입으로 조회 시 모든 스프링 빈이 조회된다.
@Test
@DisplayName("부모 타입 조회, 자식 둘 이상 있으면 중복 오류 발생.")
void findBeanByParentTypeDuplicate() {
    // 검증
    assertThrows(NoUniqueBeanDefinitionException.class,
            () -> ac.getBean(DiscountPolicy.class));
}
  • 아래와 같이 부모 타입 조회 시 자식이 둘 이상인 경우 빈의 이름을 지정하면 된다.
@Test
@DisplayName("부모 타입 조회, 자식 둘 이상 있으면 빈 이름 지정하면 된다.")
void findBeanByParentTypeBeanName() {
    // 검증
    DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", 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("key = " + key + " value = " + beansOfType.get(key));
    }
}
  • 아래와 같이 Object 타입으로 모두 조회 가능. (이 경우 스프링 관련 빈까지 모두 조회 됨 - 자바 객체는 모두 Object 타입이기 때문)
@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));
    }
}

반응형

+ Recent posts