Dependency Injection
Dependency Injection
Inversion Of Control
IoC(Inversion Of Control)는 제어의 역전
이라고 한다. 제어의 역전이란 프로그램의 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것 을 의미한다.
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.stereotype.Controller;
import com.techvu.team.TeamService;
@RequiredArgsConstructor
@RequestMapping("/team")
@Controller
public class TeamController {
/**
* @Resource(name = "teamService")
* private TeamService teamService;
*
* @Autowired
* private TeamService teamService;
*/
private final TeamService teamService;
public String join(User user) {
teamService.join(user);
}
}
TeamController 는 teamService 에 어떤 구현체가 들어와서 의존성이 주입되는지 신경쓰지 않고 자신이 할 일을 할 뿐이다. 즉, 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC) 라고한다.
스프링은 DI Container
를 통해 의존 관계 주입과 관련된 프로그램의 제어 흐름 관리한다.
DI(Dependency Injection)
의존 관계 주입(DI, Dependency Injection) 이란, 원어 그대로 해석하면 "의존 관계를 주입한다
"라고 볼 수 있다.
의존 관계는 두 가지로 나누어 생각해야한다.
- 정적인 의존 관계
- import 문을 통해 알 수 있는 의존 관계
- 컴파일 시점에 확인 가능한 의존 관계
- 동적인 의존 관계
- 런타임 시점에 확인 가능한 의존 관계
- 런타임에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다.
- 클라이언트 : TeamController
- 서버 : TeamService
- 런타임에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다.
- 의존 관계 주입을 사용하면, 클라이언트 코드를 변경하지 않고 클라이언트가 호출하는 대상(서버)의 타입 인스턴스를 변경할 수 있다.
- 의존 관계 주입을 사용하면, 정적인 의존 관계를 변경하지 않고 클라이언트가 호출하는 대상(서버)의 타입 인스턴스를 변경할 수 있다.
- 런타임 시점에 확인 가능한 의존 관계
위 코드를 다시 보자.
TeamController 에서 import 문을 통해 정적인 의존 관계
를 확인할 수 있다. 하지만 동적인 의존 관계는 런타임 시점에 확인할 수 있다. 지금까지 설명한 내용을 보면
관계(RelationShip)
라는 말이 많이 등장한다. 설명과 함께 코드를 다시 보면 객체들은 서로 관계를 맺고, 의사소통을 하는 것을 알 수있다.
이 말을 조금 더 OOP 스럽게 설명하면 객체들은 서로 관계를 맺고, 메시지를 주고 받는다. 라고 표현할 수 있다.
메시지(Message)는 객체들이 서로 의사소통 하는 것을 의미한다.
DI 는 의존성 주입이라고도 많이 부르지만, OOP 관점으로 바라보면 의존 관계
라는 말이 더 어울리는 것을 알 수 있다.
DI Container
DI Container(IoC Container) 는 객체를 생성하고 의존 관계를 관리해주는 컨테이너이다. ObjectFactory 라고 부르기도 한다.
스프링에서는 ApplicationContext
라는 컨테이너가 존재한다. 얘가 바로 DI Container 라고 생각하면 된다.
ApplicationContext 는 Interface 이다. 따라서 다양한 구현체를 만들 수 있도록 되어있는데 크게 다음과 같다.
- 애노테이션 기반으로 스프링 컨테이너 만들기
AnnotationConfigApplicationContext
- XML 기반으로 스프링 컨테이너 만들기
GenericXmlApplicationContext
빈으로 등록된 클래스들을 스프링 컨테이너가 관리한다. 애노테이션 기반 스프링 컨테이너를 사용하는 방법을 배워보자.
- AppConfig
/**
* AppConfig.java
* 애플리케이션에 대한 전반적인 동작 방식(환경 설정)
* 구현 객체 생성 담당 및 생성자를 통한 주입
*/
@Configuration
public class AppConfig {
/**
* @Bean 어노테이션은 name 속성을 따로 지정하지 않으면
* Ex) @Bean(name="memberService2")
* default 로 메서드명을 name 으로 지정한다.
*/
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
}
- 스프링 컨테이너를 통해서 빈으로 등록된 객체 꺼내서 사용하기
// AppConfig 에 Bean 으로 등록되어있는 애들을 스프링 컨테이너에 넣어서 관리해준다.
// 스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록한다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = ac.getBean("memberService", MemberService.class); // memberService 는 Bean 으로 등록된 메서드 이름
스프링 컨테이너 구현체 AnnotationConfigApplicationContext
에서 파라미터로 클래스 객체
를 받는 것을 볼 수 있다.
DI 도 리플렉션을 사용하며, 리플렉션의 시작은 클래스 객체로부터 시작된다. AnnotationConfigApplicationContext 내부에서는 Class<AppConfig>
클래스 객체를 사용하여 어노테이션 정보를 읽어서 빈들을 등록한다는 것을 알 수 있다.
세부적인 동작 과정을 살펴보자.
Reflection 을 활용한 빈 등록 및 의존 관계 설정
- AppConfig.class 와 Reflection 을 사용하여 스프링 컨테이너에 빈들을 등록
- 스프링 컨테이너 내부에 있는
스프링 빈 저장소
는Key, Value
형태로 이루어져 있음- Ex. Key : 빈 이름(memberService), Value : 빈 객체(MemberService@x01)
- 빈들을 등록하고 나면,
의존 관계
를 설정한다.- 의존 관계 설정도 내부적으로 Reflection 을 활용한다.
BeanFactory
우리가 일반적으로 스프링 컨테이너라고 부르는 ApplicationContext Interface 는 BeanFactory Interface
를 상속 받고 있다.
- BeanFactory
- 스프링 컨테이너의 최상위 인터페이스이다.
- 스프링 빈을 관리하고 조회하는 역할을 담당한다.
- getBean() 을 제공한다.
- ApplicationContext
- BeanFactory 기능을 모두 상속받아서 제공한다.
- BeanFactory 에서 제공하는 기능 이외에도, 애플리케이션을 개발할 때 필요한 여러가지 부가기능을 제공한다.
- 여러가지 부가기능
- MessageSource 를 활용한 국제화 기능
- 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력
- 환경 변수
- 로컬, 개발, 운영등을 구분해서 처리
- 애플리케이션 이벤트
- 이벤트를 발행하고 구독하는 모델을 편리하게 지원
- 편리한 리소스 조회
- 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회
- MessageSource 를 활용한 국제화 기능
BeanDefinition
실제로 리플렉션을 활용하여 빈을 등록할 때, 빈 설정 메타정보(BeanDefinition)
을 활용하여 이 메타정보를 기반으로 스프링 빈을 생성한다.
- ApplicationContext 는 여러 구현체가 있지만, BeanDefinition 이라는 추상회를 이용하기 때문에 어떤 코드로 작성된 것인지 몰라도 된다.
BeanClassName
: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)factoryBeanName
: 팩토리 역할의 빈을 사용할 경우 이름. (Ex. appConfig)factoryMethodName
: 빈을 생성할 팩토리 메서드 지정. (Ex. memberService)Scope
: 싱글톤(기본값)lazyInit
: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연처리 하는지 여부InitMethodName
: 빈을 생성하고, 의존 관계를 적용한 뒤에 호출되는 초기화 메서드 명DestroyMethodName
: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명Constructor arguments, Properties
: 의존관계 주입에서 사용한다. (자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
나만의 DI Container 만들기
DI 도 Reflection 을 통해 이루어진다.
public class ContainerService2 {
// 메서드로 넘겨주는 클래스 타입으로 리턴하기 위해서 제네릭 메서드 생성
public static <T> T getObject(Class<T> classType) {
T instance = createInstance(classType);
Arrays.stream(classType.getDeclaredFields()).forEach(field -> {
if(field.getAnnotation(Inject.class) != null) {
Object fieldInstance = createInstance(field.getType());
field.setAccessible(true); // 필드는 보통 private 으로 되어있어서(public 이면 안해줘도 된다.) accessible true 설정
try {
field.set(instance, fieldInstance);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
});
return instance;
}
private static <T> T createInstance(Class<T> classType) {
try {
return classType.getConstructor(null).newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
Links
References
- 토비의 스프링 3 / 이일민 저 / 에이콘 출판사