본문 바로가기

웹 개발/Backend

[Spring] 의존성 주입 ,스프링 컨테이너 (IoC 컨테이너) 개념 기초

1. 의존성 주입


Dependency Injection

  • IoC (Inversion of Control) 이라고도 함
  • 어떤 객체가 사용하는 의존 객체를 직접 만드는 것이 아니라 주입 받아 사용하는 방법

1.1 예시

1.1.1 의존성 주입 없이 직접 사용할 객체를 만드는 예시

public class BookService {
	private BookRepository bookRepository;
	
	//constructor
	public BookService() {
		bookRepository = new BookRepository();
	}
}

1.1.2 사용할 객체를 주입 받는 예시

public class BookService
	private BookRespository bookRepository;
	
	public void setBookRepository(BookRespository bookRepository) {
		this.bookRepository = bookRepository;
	}
}

1.1.3 설명

  • BookService 클래스는 관련 객체들을 저장해 둔 BookRepository 클래스가 필요하다
  • 즉, BookService 클래스는 BookRepository 클래스의 의존성을 가진다.
  • BookService와 BookRepository가 둘 다 Bean으로 등록되어 있을 때 BookService 생성자만 만들어주면 스프링 IoC 컨테이너가 BookRepository에 의존성을 자동으로 주입한다.
  • 스프링 4.3 이후부터 생성자가 하나인 경우 @Autowired 사용하지 않아도 된다.
결론은 의존성을 주입받아서 객체를 사용하라는 것

2. IoC 컨테이너


애플리케이션 컴포넌트의 중앙 저장소

  • Bean configuration 소스로부터 Bean의 정의를 읽고, 구성하고, 제공한다.
  • Bean간 의존 관계를 설정해준다.
  • 즉, 객체 생성을 담당하고, 의존성을 관리한다.
  • 중요한 인터페이스 BeanFactory, ApplicationContext

2.1 POJO

Plain Old Java Object

  • 오래된 방식의 자바 객체 , 즉 평범한 자바 클래스
  • 과거에 자바 웹 애플리케이션 개발을 위해서는 Servlet 클래스를 상속받아야 했는데 이는 POJO가 아니다.
  • 즉, 특정 기술, 프레임워크에 종속되지 않고 동작하는 순수한 자바 객체
  • 객체 지향적 원리에 충실하면서 환경과 기술에 종속되지 않고 필요에 따라 재활용할 수 있는 방식의 오브젝트

2.2 IoC 이외의 다른 호출 방식

2.2.1 클래스 호출 방식

  • 클래스 내에 선언과 구현이 함께 존재
  • 다양한 형태로 변화 불가능
  • 클래스 -사용, 생성, 호출 → 클래스

2.2.2 인터페이스 호출 방식

  • 인터페이스를 구현한 구현 클래스
  • 클래스는 인터페이스를 사용해서 해당 인터페이스를 구현한 클래스를 생성하고 호출해서 사용함
  • 구현 클래스를 교체해서 변화가 가능하지만 이때 이를 호출해서 사용하는 클래스도 코드 수정이 필요함

2.2.3 팩토리 호출 방식

  • 호출 클래스는 팩토리 클래스만 호출하면 됨
  • 팩토리가 인터페이스를 구현한 클래스를 생성함
  • 구현 클래스 변경시 호출 클래스에는 영향이 없으나 팩토리 클래스에 수정이 필요

2.3 IoC 호출 방식

  • IoC에서 인터페이스를 구현한 클래스를 생성해서 호출하던 클래스에 의존성을 주입
  • 호출 클래스는 의존성을 주입 받아 인터페이스를 사용
  • 서로의 의존성이 없음
  • 실행 시점에 클래스간 관계 형성

3. 스프링 컨테이너


프로그래머가 작성한 코드의 처리 과정으 위임 받아 독립적으로 처리하는 존재

  • 의존성 제어
  • 객체간 의존성을 낮추기 위해 IoC 컨테이너 사용

3.1 BeanFactory

  • 스프링 빈 컨테이너에 접근하기 위한 최상위 인터페이스
  • 빈 객체를 생성, 관리하는 기본적인 기능을 가진 인터페이스
  • 빈팩토리는 클라이언트의 요청이 있을 때 getBean()을 통해 빈 객체 (클래스 인스턴스) 를 생성 (lazy-loading)
  • 일반적으로 스프링 프로젝트에서 사용될 일 드물다.

3.1.1 Bean

  • 스프링 IoC 컨테이너가 관리하는 객체
  • IoC에 등록됨 → 등록된 빈에 대해 의존성 관리가 쉬워지고, 싱글톤 형태로 관리됨 (선언된 하나의 빈에 대해 컨테이너에 단 하나의 객체만이 존재함 → getBean 요청이 들어올 때마다 새 인스턴스 생성)

3.1.2 Bean 등록하기

  • 과거에는 직접 xml에 설정을 추가해줘야 했다. (무시무시.. )
  • ApplicationConfiguration 클래스 생성 후 어노테이션으로 빈 등록하는 방법
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() {
        BookService bookService = new BookService();
        bookService.setBookRepository(bookRepository());
        return bookService;
    }
}

 

  • 스프링 부트에서 지원하는 표준 방식 : 어노테이션으로 빈 설정하고 주입 받음
@Bean : 외부 라이브러리를 빈으로 등록할 경우 
@Component : 개발자가 직접 만든 클래스를 빈으로 등록하고 싶은 경우 
@Controller @Service @Repository @Congifuration @RestController: 위 @Component (빈)을 역할에 따라 명칭을 달리 지정하는 것 
	** @Configuration : IoC 컨테이너에게 해당 클래스가 빈 설정 파일임을 알림 -> 이 파일을 applicationContext가 읽어서 실행 

@Autowired : 컨테이너에 등록된 스프링 빈을 찾아 주입시켜줌 ** 3.2에서 추가 설명 

 

  • @ComponentScan
    • 프로젝트 생성시 등록한 최초 패키지 아래에 있는 파일 경로를 적어주면 해당 패키지 아래 있는 모든 빈 관련 어노테이션들을 찾아 빈으로 등록해줌
    • 이 어노테이션이 설정된 클래스가 포함된 패키지 하위의 클래스에서만 대상을 찾음
    • 그런데 스프링부트 프로젝트 생성시 메인 클래스가 있는 최상위 애플리케이션 클래스에 있는 @SpringBootApplication에 이 선언이 포함되어 있는 것 + @Configuration 포함
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackageClasses = DemoApplication.class)
public class ApplicationConfig {
    
}

3.2 @Autowired

  • 필요한 의존 객체의 타입에 해당하는 빈을 찾아 주입
  • 타입 - 생성자, setter, 필드
  • 디폴트가 true → 의존성 주입할 대상을 찾지 못하면 애플리케이션 구동 실패
  • @Autowired(required=false) 로 설정하면 빈으로 등록 X
  • 같은 타입 ex)Repository 클래스가 여러개 인 경우 둘 다 빈으로 등록되어 있다면 주입 불가
    • 빈으로 등록한 클래스하나에 @Primary를 등록 ex) @Repository @Primary
    • 또는 @Autowired @Qualifier("등록할 빈의 이름")
    • 또는 List<..>으로 해당 타입의 모든 빈을 주입받는 방식
    • primary가 더 안정적인 방법

3.2.1 Constructor DI

  • 생성자 주입
  • 권고되는 방법
  • 생성자에 의존성 주입을 받고자 하는 필드를 나열하는 방식
  • 필수적으로 사용해야 하는 레퍼런스 없이는 인스턴스를 만들 수 없도록 강제함

3.2.2 Field DI

  • 객체의 멤버 필드에 @Autowired 어노테이션을 선언하여 주입 받는 방법
  • 이 필드는 final** 선언 불가
    • ** final : 여러 컨텍스트에서 단 한 번만 할당될 수 있는 객체를 정의할 때 사용하는 키워드, 한번 초기화하면 변경할 수 없는 상수값, 객체 자체 변경 불가 (객체 속성 변경은 setter 함수로 가능), 메소드 내부 변수값 변경 불가

3.2.3 Setter DI

  • setter 메소드에 Autowired 어노테이션을 달아 주입 받는 방법
  • 의존성이 선택적으로 필요한 경우 사용

3.3 ApplicationContext

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
		MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
  • 빈 팩토리를 상속받은 인터페이스
  • 스프링의 핵심 인터페이스
  • 빈 객체를 생성하고 관리하는 기능을 가짐
    • 트랜잭션 관리, 메시지 기반 다국어 처리, AOP처리, DI 등 지원
  • pre-loading 방식 : 컨테이너가 구동되는 시점에 객체들을 생성

3.3.1 EnvironmentCapable 인터페이스

  • 프로파일과 프로퍼티를 다루는 인터페이스
  • 프로파일 profile
    • 빈들의 묶음
    • 어떤 환경에서는 어떤 빈들만 사용하겠다는 것을 의미
    • 등록한 프로파일 실행 → Edit Configurations → VM options에 -Dsping.profiles.active="test"
    • 프로파일 표현식에 !, | , & 사용 가능
@Configration // bean 설정 타입 중 하나
@Profile("test") // test라는 프로파일로 애플리케이션을 실행하기 전까지 아래 빈 설정이 적용되지 않음 
public class TestConfiguration{
	@Bean
	public BookRepository bookRepository() {
		return new TestBookRepository();
	}
}
@Component
public class AppRunner implements ApplicationRunner {

   @Autowired
   ApplicationContext ctx;

   @Autowired // test 프로파일로 실행하지 않은 경우 에러 발생 -> 빈으로 등록되지 않았기 때문! 
   BookRepository bookRepository;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        Environment environment = ctx.getEnvironment();
        System.out.println(Arrays.toString(environment.getActiveProfiles()));
        System.out.println(Arrays.toString(environment.getDefaultProfiles()));
    }
}
  • 프로퍼티 properties
    • 다양한 방법으로 정의할 수 있는 설정값
    • environment 역할 : 프로퍼티 소스 설정 및 값 가져오는 것
    • 같은 프로퍼티가 있을 경우 우선순위대로 높은 것을 설정값으로 가져옴

3.3.2 MessageSource 인터페이스

  • 메시지를 다국화하는 인터페이스
  • 메시지 설정 파일을 모아놓고 각 국가마다 로컬라이징을 하여 각 지역에 맞는 메시지를 제공할 수 있음
  • [file_name][language][country].properties 파일 생성
    • ex) messages_kr_KR.properties >> greeting=한국어, {0}
  • 메시지 소스 리로딩하는 방법 : ReloadableResourceBundleMessageSource 객체를 통해 메시지 프로퍼티를 갱신하면서 읽음 ⇒ 스프링부트에서는 메시지를 직접 빈으로 등록하지 않아도 messages.properties 사용 가능
import ...

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:/messages");
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setCacheSeconds(10);
        return messageSource;
    }
}

 

References