GrowMe

[Spring]DI를 코드 + a로 쉽게 이해해보자 본문

About Spring

[Spring]DI를 코드 + a로 쉽게 이해해보자

오늘도 타는중 2022. 6. 27. 00:57
DI(Dependency Injection)
# DI
# 스프링 컨테이너
# 빈 컨테이너
# 의존성 주입
# Component
# Bean
# 빈 등록법
# 빈 조회법

*DI란?

  • 의존성 주입(Dependency Injection) : 사용할 객체를 외부에서 주입받는 것
  • 기존처럼 new 연산자로 객체를 새로 생성하는 것이 아닌, 미리 만들어둔 객체를 불러오는 것!

*외부 어디에서 불러오는데??  : Spring Container

📦Spring Container의 종류(1) : BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스
  • BeanFactory는 빈을 등록하고 생성하고 조회하고 돌려주는 등 빈을 관리하는 역할을 합니다.
  • getBean() 메소드를 통해 빈을 인스턴스화할 수 있습니다.

📦Spring Container의 종류(2) : ApplicationContext

  • BeanFactory를 상속받아 기능을 확장하여 제공합니다.
  • 부가 기능(참고)
    • MessageSource: 메세지 다국화를 위한 인터페이스
    • EnvironmentCapable: 개발, 운영 등 환경변수 등으로 나눠 처리하고, 애플리케이션 구동 시 필요한 정보들을 관리하기 위한 인터페이스
    • ApplicationEventPublisher: 이벤트 관련 기능을 제공하는 인터페이스
    • ResourceLoader: 파일, 클래스 패스, 외부 등 리소스를 편리하게 조회

📦컨테이너를 인스턴스화하여 사용하기

1. AnnotationConfigApplicationContext (Annotation)

public static void main(String[] args) {
// 빈이 등록된 컨테이너를 인스턴스화하여 사용할 준비시키기!
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
// 등록된 빈을 가져와 담기
MyService myService = ctx.getBean(MyService.class);
// 빈을 사용하기(내부 메서드)
myService.doStuff();
}

2. ClassPathXmlApplicationContext (XML)

// 빈이 등록된 컨테이너를 인스턴스화하여 사용할 준비시키기!
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

// 등록된 빈을 가져오기
PetStoreService service = context.getBean("cmarket", cmarketService.class);

// 객체의 이름만 따로 저장하기
List<String> userList = service.getUsernameList();

*Bean이 뭔데? : 사용할 객체(미리 등록되있는)

  • Spring 컨테이너가 관리하는 자바 객체(인스턴스화된 객체를 의미)
  • 스프링 컨테이너에 등록된 객체 = Spring Bean
  • 빈은 클래스의 등록정보, getter/setter 메서드 등의 정보를 포함한다.

*Bean은 그럼 어떻게 등록하면 될까?

implementation('org.springframework.boot:spring-boot-starter-web')
  • 먼저 build.gradle 파일에 위 의존성을 추가해준다. -> 관련 라이브러리들이 자동으로 받아진다.

1. 원초적 방법(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="bookService"
          class="me.wordbe.springgoahead.BookService">
        <property name="bookRepository" ref="bookRepository" />
    </bean>
 
    <bean id="bookRepository"
          class="me.wordbe.springgoahead.BookRepository" />
 
 
</beans>
  • xml 설정 파일에 등록할 객체의 클래스를 각각 빈으로 등록한다.
  • 사용할 장소(bookService)의 특성(property)의 참조로 사용할 객체(boorRepository)를 등록한다.
  • bookService 클래스에서 boorRepository를 선언할 경우, 여기 등록된 boorRepository 객체가 불러와져 주입이 된다.

 

2. Spring 2.5 버전 (Component-scan 기능 추가)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
 
    <context:component-scan base-package="me.wordbe.springgoahead" />
 
 
</beans>

패키지의 각각 클래스에 어노테이션을 붙여서 빈을 등록한다.

import org.springframework.stereotype.Repository;
 
@Component
public class BookRepository {
}
@Component
public class BookService {
 
    @Autowired
    BookRepository bookRepository;
 
    public void setBookRepository(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
}
  • xml에 등록된 <context:component-scan base-package= 패키지경로>의 하위 클래스들 중, @Component가 붙은 클래스들을 모두 스캔하여 빈에 자동등록 시켜준다.

 

3. Java 기반 컨테이너 설정

사용할 클래스들과 같은 패키지에 설정파일(java)을 만든다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class ApplicationConfig {
 
    @Bean
    public BookRepository bookRepository() {
        return new BookRepository();
    }
 
    @Bean
    public BookService bookService(BookRepository bookRepository) {
        BookService bookService = new BookService();
 
        // 의존성 주입 setter 이용
        bookService.setBookRepository(bookRepository);
        return bookService;
    }
}
  • @Configuration : 빈 설정 클래스라는 것을 명시한다. 이 클래스는 컨테이너에서 불러와 사용될 수 있다.
  • @Bean : 이 어노테이션이 붙은 메서드의 메서드명을 이름으로 하여 객체를 빈으로 등록한다.
public class DemoApplication {
 
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
                
        BookService bookService = context.getBean(BookService.class);
    	bookService.doStuff();
}
    }
 
}

 

  • ApplicationContext를 구현한 객체인 AnnotationConfigApplicationContext를 통해 설정해주었던, 빈 설정 클래스(ApplicationConfig)를 등록해주어 컨테이너를 생성해 사용할 수 있다.
  • doStuff()는 BookService 클래스의 내부 메서드

 

4. 자바 Config, ComponentScan 활용

  • 3번에서는 Config파일에 사용할 클래스들을 모두 @Bean을 통해 일일이 등록해야 했다.
  • 이를 해결하기 위한 방법 : ComponentScan 활용하기
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
 
@Configuration
@ComponentScan(basePackageClasses = SpringApplication.class) // 최상단 메인 클래스
public class ApplicationConfig {
}
  • @ComponentScan : 2번에서 설명한 ComponentScan을 xml 설정파일에 추가한 것과 동일한 기능을 제공한다.
    -> basePackageClasses로 메인 클래스를 지정해주면, 동일 패키지 및 그 하위 패키지에 @Component가 붙은 클래스 Or @Bean이 붙은 메서드들을 자동으로 스캔해주고, 등록해준다.
  • @Bean은 메서드 단위로, @Component는 클래스 단위로 스캔되며 통상 두 방식 중 하나만 사용한다.

 

5. @SpringBootApplication

@SpringBootApplication
public class Application {
 
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • @SpringBootApplication 내부에는 @ComponentScan 과 @Configuration이 포함되어 있다. 즉, @SpringBootApplication와 같은 경로의 패키지 or 그 하위 패키지의 빈들을 찾아서 스캔하고 등록하여준다.

*Bean을 조회하는 방법

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

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class ApplicationContextBasicFindFirst {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

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


    @Test
    @DisplayName("빈을 타입으로만 조회")
    void findBeanByType() {
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }
    
    @Test
    @DisplayName("동일한 타입이 둘 이상인 스프링 빈을 타입으로 조회할 빈 이름을 지정한다.")
    void findBeanByName() {
        DiscountPolicy discountPolicy = ac.getBean("fixDiscountPolicy", DiscountPolicy.class);
        assertThat(discountPolicy).isInstanceOf(FixDiscountPolicy.class);
    }

    @Test
    @DisplayName("특정 타입을 모두 조회한다.")
    void findAllBeanType() {
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + "value = "+ beansOfType.get(key));
        }
        System.out.println("beansOfType = " + beansOfType);
        assertThat(beansOfType).hasSize(2);
    }
}
  • 이름으로 조회
  • 타입으로 조회
  • 동일한 타입이 둘 이상의 빈은?? : 이름으로 부르거나, 타입들 모두 부르거나
Comments