IoC, Di, 그리고 컨테이너
스프링에 대해 조금이라도 공부하면 마주치게되는 용어들입니다. 처음보면 잘 이해되지 않아 이번기회에 정확하게 이해하고 넘어가기위해 정리하고자 합니다.
제어의 역전 IoC (Inversion of Control)
- 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성, 연결, 실행 했습니다. 한마디로 구현 객체가 프로그램의 제어 흐름을 스스로 조종했습니다. 개발자 입장에서는 본인이 작성한 코드이기때문에 자연스러운 흐름입니다.
- 반면에 AppConfig가 등장한 이후에는 구현 객체는 자신의 로직을 실행하는 역할만 담당합니다. 프로그램의 제어 흐름은 이제 AppConfig가 가져가는 것 입니다. 예를들어 OrderServiceImpl은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지 모릅니다.
- 즉, 프로그램에 대한 제어 흐름의 권한은 모두 AppConfig가 가지고 있습니다.
- 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라고 합니다.
프레임워크 vs 라이브러리
- 프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크 입니다. (JUnit)
- 반면에, 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아닌 라이브러리입니다.
의존관계 주입 DI (Dependency Injection)
- OrderServiceImpl은 DiscountPolicy 인터페이스에 의존합니다. OrderServiceImpl은 실제 어떤 구현 객체가 사용될지는 모릅니다.
- 의존관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 합니다.
- 정적인 클래스 의존관계
클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있습니다. 정적인 의존관계는 애플리케이션을 실행하지 않아도 분석할 수 있습니다. OrderServiceImpl은 MemberRepository, DiscountPolicy에 의존한다는 것을 알 수 있습니다. 그런데 이러한 클래스 의존관계 만으로는 실제 어떤 객체가 OrderSrviceImpl에 주입되는 지 알 수 없습니다.
- 동적인 객체 인스턴스 의존 관계
애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계입니다.
- 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라고 합니다.
- 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있습니다.
- 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있습니다.
IoC 컨테이너, DI 컨테이너
- AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라고 합니다.
- 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라고 합니다.
스프링으로 전환하기
지금까지 순수한 자바 코드만으로 DI를 적용했습니다. 이제 스프링을 사용하여 프로젝트를 변경해보겠습니다.
- AppConfig
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FIxDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration // 설정 정보에 하는 어노테이션
public class AppConfig {
@Bean // 각 메서드에 Bean 어노테이션을 추가하면 스프링 컨테이너에 추가됨.
public MemberService memberService() {
return new MemberServiceImpl(getMemberRepository());
}
@Bean
public MemberRepository getMemberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(
getMemberRepository(),
getDigcountPolicy());
}
@Bean
public DiscountPolicy getDigcountPolicy() {
//return new FIxDiscountPolicy();
return new RateDiscountPolicy();
}
}
- MemberApp
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MemberApp {
public static void main(String[] args) {
//AppConfig appConfig = new AppConfig();
//MemberService memberService = appConfig.memberService();
// AppConfig 클래스에 있는 생선한 객체를 컨테이너에 넣어주고 관리해줌
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("member = " + member.getName());
System.out.println("findmember = " + findMember.getName());
}
}
AppConfig인 클래스에 Configuration 어노테이션을 등록하고 각 메서드마다 Bean어노테이션을 해주면 각 메서드는 스프링 컨테이너에 등록된다고 합니다. 실제로 MemberApp에서 AnnotationConfigApplicationContext 파라미터에 AppConfig.class를 넣어준 뒤 실행한 결과를 보면 아래 사진과 같이 문제없이 돌아가는 것을 볼 수 있고 로그를 보면 각 메서드명으로 스프링 컨테이너에 등록된 것을 볼 수 있습니다.
스프링 컨테이너
- 위에서 사용한 ApplicationContext를 스프링 컨테이너라고 합니다.
- 기존에는 개발자가 AppConfig를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용합니다.
- 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 사용합니다. 여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록합니다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 합니다.
- 이전에는 개발자가 필요한 객체를 AppConfig를 사용해서 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 합니다. 스프링 빈은 ApplicationContext.getBean() 메서드를 사용해서 찾을 수 있습니다.
- 코드가 약간 더 복잡해진 것 같은데, 스프링 컨테이너를 사용하면 어떤 장점이 있는지 궁금해집니다. 결과부터 말해주셨는데 어마어마한 장점이 있다고 합니다.(궁금합니다..)
스프링 컨테이너 생성
스프링 컨테이너가 생성되는 과정을 알아봅시다
// 스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
- ApplicationContext를 스프링 컨테이너라고 합니다.
- ApplicationContext는 인터페이스입니다. (다형성)
- 스프링 컨테이너는 XML을 기반으로 만들 수 있고, 어노테이션 기반의 자바 설정 클래스로 만들 수 있습니다.
- 직전에 AppConfig를 사용했던 방식이 어노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든 것 입니다.
- new AnnotationConfigApplicationContext(AppConfig.class)
- 스프링 컨테이너를 생성할 때는 구성 정보를 지정해주어야 합니다.
- 여기서는 AppConfig.class를 구성 정보로 지정했습니다.
- 스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록합니다.
빈 이름
- 빈 이름은 메서드 이름을 사용합니다. (default)
- 빈 이름을 직접 부여할 수도 있습니다.
- @Bean(name="원하는 이름")
- 주의) 빈 이름은 항상 다른 이름을 부여해야 합니다. 같은 이름을 부여할 경우 빈이 무시되거나, 기존 빈을 덮어버리는 오류가 생길 수 있습니다.
- 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입(DI)합니다.
- 단순히 자바 코드를 호출하는 것 같지만 차이가 있습니다. 이 차이는 후에 싱글톤 컨테이너에서 이해할 수 있다고 합니다.
참고
스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나뉘어져 있습니다. 그런데 이렇게 자바 코드로 스프링 빈을 등록하면 생성자를 호출하면서 의존관계 주입도 한번에 처리됩니다.
정리
스프링 컨테이너를 생성하고, 설정(구성) 정보를 참고해서 스프링 빈도 등록하고, 의존 관계도 설정했습니다. 이제 스프링 컨테이너에서 데이터를 조회해보고자 합니다.
컨테이너에 등록된 모든 빈 조회
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 bean = ac.getBeanDefinition(beanDefinitionName);
//Role ROLE_APPLICATION: 직접 등록한 애플리케이션 빈
//Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
if(bean.getRole() == BeanDefinition.ROLE_APPLICATION) {
System.out.println("name = " + beanDefinitionName + "object = " + bean);
}
}
}
- 모든 빈 출력하기
- ac.getBeanDefinitionNames() : 스프리에 등록된 모든 빈 이름을 조회합니다.
- ac.getBean() : 빈 이름으로 빈 객체(인스턴스)를 조회합니다.
- 애플리케이션 빈 출력하기
- getRole()로 등록한 빈, 스프링 내부 빈인 것을 확인할 수 있습니다.
- BeanDefinition.ROLE_APPLICATION : 일반적으로 상요자가 정의한 빈 입니다.
- BeanDefinition.ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈 입니다.
스프링 빈 조회 - 기본
스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회방법
- ac.getBean(빈이름, 타입)
- ac.getBean(타입)
- 조회 대상 스프링 빈이 없으면 예외 발생
- NoSuchBeanDefinitionException: No bean named 'xxxxx' available
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 findBeanByName2() {
MemberService memberService = ac.getBean("memberService",MemberServiceImpl.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("빈 이름으로 조회")
void findBeanByEmptyName() {
//MemberService memberService = ac.getBean("xxxxx",MemberService.class);
//assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
assertThrows(NoSuchBeanDefinitionException.class,
() -> ac.getBean("xxxxx",MemberService.class));
}
참고로 구체 타입으로 조회하면 변경시 유연성이 떨어집니다.
스프링 빈 조회 - 동일한 타입이 둘 이상
- 타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생합니다. 이때는 빈 이름을 지정하면 됩니다.
- ac.getBeansOfType()을 사용하면 해당 타입의 모든 빈을 조회할 수 있습니다.
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
@Test
@DisplayName("타입으로 조시 같은 타입이 둘 이상 있으면, 중복 오류가 발생합니다.")
void findBeanByTypeDulicate() {
// 이렇게 하면 에러가 터집니다. 어떤 MemberRepository를 빈에서 가져와야 하는지 모르기 때문입니다.
//MemberRepository memberRepository = ac.getBean(MemberRepository.class);
assertThrows(NoUniqueBeanDefinitionException.class,
() ->ac.getBean(MemberRepository.class));
}
@Test
@DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다") void findBeanByName() {
MemberRepository memberRepository = ac.getBean("memberRepository1",
MemberRepository.class);
assertThat(memberRepository).isInstanceOf(MemberRepository.class);
}
@Test
@DisplayName("특정 타입을 모두 조회하기")
void findAllBeanByType() {
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);
}
@Configuration
static class SameBeanConfig {
@Bean
public MemberRepository memberRepository1() {
return new MemoryMemberRepository();
}
@Bean
public MemberRepository memberRepository2() {
return new MemoryMemberRepository();
}
}
스프링 빈 조회 - 상속 관계
- 부모 타입으로 조회하면, 자식 타입도 함께 조회합니다.
- 그래서 모든 자바 객체의 최고 부모인 Object 타입으로 조회하면, 모든 스프링 빈을 조회합니다.
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
@Test
@DisplayName("부모 타입으로 조회시, 자식이 둘 이상있으면, 중복 오류가 발생합니다.")
void findBeanByParentTypeDuplicate() {
//DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
Assertions.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));
}
}
@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));
}
}
@Configuration
static class TestConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FIxDiscountPolicy();
}
}
BeanFactory와 ApplicationContext
- BeanFactory
- 스프링 컨테이너의 최상위 인터페이스입닌다.
- getBean()을 제공합니다.
- ApplicationContext
- BeanFactory 기능을 모두 상속받아서 제공합니다.
- ApplicationContext는 BeanFactory가 제공하는 기능을 포함한 수많은 부가기능을 제공합니다.
- 메시지소스를 활용한 국제화 기능
- 예를 들어서 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력합니다.
- 환경변수
- 로컬, 개발, 운영등을 구분해서 처리합니다.
- 애플리케이션 이벤트
- 이벤트를 발행하고 구독하는 모델을 편리하게 지원합니다.
- 편리한 리소스 조회
- 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회합니다.
다양한 설정 형식 지원 - 자바 코드, XML
스프링 컨테이너는 다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되어 있습니다.
어노테이션 기반 자바 코드 설정 사용
- 현재까지 했던 것 입니다.
XML 설정 사용
- 최근에는 스프링 부트를 많이 사용하면서 XML 기반의 설정은 잘 사용하지 않는다고 합니다. 아직 많은 레거시 프로젝트들이 XML로 되어있고, 또 XML을 사용하면 컴파일 없이 빈 설정 정보를 변경할 수 있는 장점도 있으므로 한번쯤 배워두는 것도 좋다고 합니다.
- GenericXmlApplicationContext를 사용하면서 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>
위는 xml 코드입니다. application.properties가 있는 resources폴더 안에 xml 파일로 생성한 뒤 복붙하시면 좀 더 편하게 테스트 해보실 수 있을 것 같습니다. 보시면 자바 코드로된 AppConfig.java 설정 정보와 비슷하다는 것을 알 수 있습니다.
테스트 하는 코드는 기존의 코드에서 구현체와 파라미터를 수정해주시면 됩니다.
ApplicationContext ac = new GenericXmlApplicationContext("AppConfig.xml");
필요하면 스프링 공식 레퍼런스 문서를 확인해보시면 좋을 것 같습니다.
https://spring.io/projects/spring-framework
스프링 빈 설정 메타 정보 - BeanDefinition
- 스프링은 어떻게 이런 다양한 설정 형식을 지원하는 걸까요?? 그 중심에는 BeanDefinition 이라는 추상화가 있습니다.
- 쉽게 이야기해서 역할과 구현을 개념적으로 나눈 것 입니다.
- XML 또는 자바 코드를 읽어서 BeanDefinition을 만들면 됩니다.
- 스프링 컨테이너는 자바 코드인지, XML인지 몰라도 됩니다. 오직 BeanDefinition을 알고있을 뿐 입니다.
- BeanDefinition을 빈 설정 메타정보라 합니다.
- @Bean, <bean> 당 각각 하나씩 메타 정보가 생성됩니다.
- 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성합니다.
코드 레벨로 조금 더 깊게 보면
- AnnotationConfigApplicationContext는 AnnotatedBeanDefinitionReader를 사용해서 AppConfig.class를 읽고 BeanDefinition을 생성합니다.
- GenericXmlApplicationContext는 XmlBeanDefinitionReader를 사용해서 appConfig.xml 설정 정보를 읽고 BeanDefinition을 생성합니다.
- 새로운 형식의 설정 정보가 추가되면, XxxBeanDefinitionReader를 만들어서 BeanDefinition을 생성하면 됩니다.
BeanDefinition 살펴보기
BeanDefinition 정보
- 실제로 Bean에 등록된 객체를 출력할 때 나오는 출력 값 입니다.
beanDefinitionName = memberService // 생성할 빈의 클래스 명
beanDefinition = Root
bean: class [null];
scope=; // 싱글톤(기본값)
abstract=false;
lazyInit=null; // 스프링 컨테이너를 생서할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연처리 하는지 여부
autowireMode=3;
dependencyCheck=0;
autowireCandidate=true;
primary=false;
factoryBeanName=appConfig; // 팩토리 역할의 빈을 사용할 경우, 예) appConfig
factoryMethodName=memberService; // 빈을 생성할 팩토리 메서드 지정, 예) memberService
initMethodName=null; // 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
destroyMethodName=(inferred); // 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
defined in hello.core.AppConfig
BeanDefinition 정리
- BeanDefinition에 대해서는 너무 깊이있게 이해하기보다는, 스프링이 다양한 형태의 설정 정보를 BeanDefinition으로 추상화해서 사용하는 것 정도만 이해하면 좋을 것 같다고 합니다.
출처
'spring > 인프런 강의 정리' 카테고리의 다른 글
[Spring/기본편] 6. 의존관계 자동 주입 (0) | 2021.12.25 |
---|---|
[Spring/기본편] 5. 컴포넌트 스캔 (0) | 2021.12.24 |
[Spring/기본편] 4. 싱글톤 컨테이너 (0) | 2021.12.23 |
[Spring/기본편] 2. 순수한 자바로 SOLID특징 지키며 설계하기 (0) | 2021.12.22 |
[Spring/기본편] 1. 객체 지향 프로그래밍 및 스프링 개념 정리 (0) | 2021.12.19 |