IoC
public class MemberServiceImpl implements MemberService {
// 이 소스 코드에서는 MemberRepository라는 인터페이스(역할)와
// MemoryMemberRepository라는 객체(구현)에 동시 의존하고 있다. (의존 역전 원칙 위배)
// MemoryMemberRepository를 DbMemoryMemberRepository로 바꾸려면 클라이언트인
// MemberServiceImpl의 소스 코드를 변경해야 한다. (개방-폐쇄 원칙 위배)
private MemberRepository memberRepository = new MemoryMemberRepository();
@Override
public void join(Member member) {
memberRepository.save(member);
}
}
public class MemberApp {
public static void main(String[] args) {
MemberService memberService = new MemberServiceImpl();
Member member = new Member(1L, "memberA", Grade.BASIC);
memberService.join(member);
}
기존 프로그램(위 소스 코드, MemberServiceImpl.java)은 클라이언트 구현 객체(MemberServiceImpl)가 스스로 필요한 서버 구현 객체(MemoryMemberRepository)를 생성·연결·실행하여 클라이언트 구현 객체가 프로그램의 제어 흐름을 스스로 조종했습니다.
이러한 서버 구현 객체들의 생성·연결·실행을 다른 객체(AppConfig)에 위임하여 클라이언트 구현 객체는 자신의 로직을 실행만을 담당하게 되어 프로그램의 제어 흐름을 다른 객체가 맡게 됩니다. 이렇게 프로그램의 제어 흐름을 직접 제어하는 것이 아닌 외부에서 관리하는 것을 제어의 역전(Inversion of Control, IoC)이라고 합니다.
// 서버 구현 객체들의 생성·연결·실행을 다른 객체(AppConfig)에 위임한다.
@Configuration
public class AppConfig {
public MemberService memberService() {
// return new MemberServiceImpl(new MemoryMemberRepository());
return new MemberServiceImpl(memberRepository());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
public class MemberServiceImpl implements MemberService {
// 이 소스 코드에서는 MemberRepository라는 인터페이스(역할)에만 의존한다. (의존 역전 원칙 만족)
// AppConfig 덕에 이제 클라이언트는 의존 관계에 신경 쓸 필요 없이 실행에만 집중할 수 있다.
private MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
}
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
Member member = new Member(1L, "memberA", Grade.BASIC);
memberService.join(member);
}
}
의존 관계 주입
정적인 클래스 의존 관계
클래스가 사용하는 import 문만 보고 의존 관계를 쉽게 판단할 수 있으며 애플리케이션을 실행하지 않아도 의존 관계가 어떻게 설정되어 있는지 알 수 있습니다. MemberServiceImpl에서 MemberRepository 인터페이스에 의존함을 위 두 번째 소스 코드에서 쉽게 확인할 수 있습니다.
동적인 객체 인스턴스 의존 관계
애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 이를 클라이언트 구현 객체에 전달하여 클라이언트 구현 객체와 서버 구현 객체 간 실제 의존 관계가 연결되는 것을 의존 관계 주입(Dependency Injection)이라 합니다. 이는 서버 구현 객체 인스턴스(MemoryMemberRepository)를 생성한 후 그 레퍼런스 값을 클라이언트 구현 객체(MemberServiceImpl)에 전달해 의존 관계를 연결합니다.
의존 관계 주입을 사용하면 클라이언트 소스 코드 변경 없이 호출하는 대상의 타입 인스턴스를 변경할 수 있습니다. 즉 정적인 클래스 의존 관계를 변경하지 않고 동적인 객체 인스턴스 의존 관계를 변경할 수 있습니다.
IoC 컨테이너, DI 컨테이너
AppConfig처럼 객체를 생성하고 관리하며 의존 관계를 연결해 주는 컨테이너를 IoC 컨테이너, DI 컨테이너, 어샘블러, 오브젝트 팩토리 등으로 불립니다.
Bean
Java Bean
Java Bean은 JSP(Java Server Pages: HTML에 Java 코드를 넣어 동적으로 웹 페이지를 구성할 수 있음; 서버 사이드 렌더링)에서 클래스를 설계할 때 따라야 하는 기법입니다.
Spring Bean
Spring Bean은 스프링 컨테이너에 의해 생성되고 의존 관계 주입이 일어나는 객체를 말합니다. Spring Bean은 Java Bean 특성을 만족할 수도, 만족하지 않을 수도 있습니다.
스프링 컨테이너 생성과 스프링 빈 등록
자바의 인터페이스인 BeanFactory, ApplicationContext를 스프링 컨테이너라고 부릅니다. 이들은 기존에 개발자가 AppConfig 객체를 통해 직접 구현 객체를 생성하고 의존 관계를 주입하는 것을 대신합니다.
BeanFactory
BeanFactory는 스프링 컨테이너의 최상위 인터페이스로 스프링 빈을 관리하고 조회하는 역할을 담당합니다.
ApplicationContext
ApplicationContext는 BeanFactory의 기능을 상속 받아 BeanFactory의 기능과 더불어 ApplicationContext만의 추가 기능을 제공합니다.
GenericXmlApplicationContext
GenericXmlApplicationContext는 XML 파일을 구성 정보로 사용하여 객체를 생성하고 초기화하여 스프링 컨테이너에 등록해 관리합니다.
AnnotationConfigApplicationContext
AnnotationConfigApplicationContext는 @Configuration 애노테이션이 있는 클래스를 구성 정보로 사용하여 @Bean 애노테이션이 있는 메소드를 모두 호출해 반환된 객체를 스프링 컨테이너에 등록하여 관리합니다.
스프링 컨테이너에 등록된 빈 조회
ApplicationContext의 getBean 메소드를 통해 빈을 인스턴스화할 수 있습니다. getBean 메소드의 입력 값으로 빈 타입 또는 빈 이름과 빈 타입을 전달할 수 있습니다.
BeanDefinition
BeanDefinition은 스프링 빈과 관련한 정보를 스프링 컨테이너에 전달하여 스프링 빈을 생성하게 됩니다.
모든 빈 조회
자바의 모든 클래스는 Object 클래스를 상속 받는다는 것을 이용하여 ApplicationContext의 getBeansOfType 메소드를 사용하여 모든 스프링 빈의 이름을 키로 한 Map<String, Object>를 얻어 조회할 수 있습니다. 또는 ApplicationContext의 getBeanDefinitionNames 메소드를 사용하여 BeanDefinition이 있는 스프링 빈 이름을 원소로 한 String 배열을 얻고 이를 토대로 스프링 빈을 조회할 수 있습니다. AppConfig와 같은 팩토리 빈처럼 BeanDefinition이 정의되지 않은 스프링 빈은 이 방식에서 조회되지 않습니다.
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 부모 타입으로 스프링 빈 출력하기
public void findAllBeanByObjectType() {
// 스프링 빈 이름을 key, 스프링 빈을 value로 얻는다.
Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + " bean = " + beansOfType.get(key));
}
}
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 모든 스프링 빈 출력하기
public void findAllBean() {
// 빈 이름을 모두 불러온다
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
// 빈 이름으로 빈을 찾는다
Object bean = ac.getBean(beanDefinitionName);
// 찾은 빈의 정보를 출력
System.out.println("name: " + beanDefinitionName + " object: " + bean);
}
}
애플리케이션 빈 조회
위에서 서술한 BeanDefinition의 getRole 메소드를 이용하여 스프링 자체 빈과 사용자가 만든 빈을 구분하여 조회할 수 있습니다.
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 애플리케이션 빈 출력하기
public void findApplicationBean() {
// 빈 이름을 모두 불러온다
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
// 빈의 정보를 찾는다
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
// 빈 역할로 애플리케이션 빈만 출력
// BeanDefinition.ROLE_APPLICATION: 애플리케이션 빈
// BeanDefinition.ROLE_INFRASTRUCTURE: 스프링 생성 빈
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
// 빈 이름으로 빈을 찾는다
Object bean = ac.getBean(beanDefinitionName);
// 찾은 빈의 정보를 출력
System.out.println("name: " + beanDefinitionName + " object: " + bean);
}
}
}
빈 조회 시 주의사항
ApplicationContext의 getBean 메소드는 하나의 스프링 빈만 반환합니다. 만약 타입으로 조회할 때 해당하는 스프링 빈이 여러 개면 예외가 발생합니다. 이런 경우에는 스프링 이름을 파라미터의 인자로 전달하거나 getBeansOfType 메소드 등을 사용해야 합니다.
getBeansOfType 메소드는 파라미터의 인자로 전달된 타입뿐만 아니라 그 클래스(타입)를 상속 받은 모든 클래스(타입)의 스프링 빈을 조회합니다.