DI, IoC 에 대한 설명은 개발자 면접에서 자주 나오는 질문 중의 하나!
"A가 B를 의존한다."
= 의존 대상 B가 변하면, 그것이 A에 영향을 미친다
예를 들어
피자 가게 요리사(PizzaChef)는 피자 레시피(PizzaRecipe)에 의존한다.
피자 레시피(PizzaRecipe)가 변화하게 되었을 때, 변화된 레시피에 따라서 요리사(PizzaChef)는 피자 만드는 방법을 수정해야 한다.
--> 요리사(PizzaChef)는 레시피(PizzaRecipe)에 의존한다
방법 1
class PizzaChef {
private PizzaChefRecipe pizzaChefRecipe;
public PizzaChef() {
pizzaChefRecipe = new PizzaChefRecipe();
}
}
방법 2 : 인터페이스로 추상화
다양한 PizzaRecipe를 의존받을 수 있게 구현하기위해
class PizzaChef {
private PizzaRecipe pizzaRecipe;
public PizzaChef() {
pizzaRecipe = new PizzaRecipe();
//pizzaRecipe = new CheesePizzaRecipe();
//pizzaRecipe = new BulgogiPizzaRecipe();
}
}
interface PizzaRecipe {
newPizza();
// 이외의 다양한 메소드
}
class PizzaRecipe implements PizzaRecipe {
public Pizza newPizza() {
return new Pizza();
}
// ...
}
정의
예시
A객체(요리사)가 B객체의 메서드or상태(레시피)에 접근하고 있다면, A객체(요리사)는 B객체(레시피)에게 의존하고 있는 것이다.
A객체(요리사)를 생성하기 위해서는 B객체(레시피)를 주입해줘야 한다.
A객체가 B객체를 사용하고 있다면, A객체는 B객체에 의존성이 있다고 말한다.
의도
객체의 생성과 사용의 관심을 분리하는 것
지금까지의 구현에서는 PizzaRecipe가 어떤 값을 가질지 PizzaChef가 직접 정하고 있다.
그러나 만약,
어떤 PizzaRecipe를 만들지를 피자 가게 사장님(PizzaRestaurantOwner)이 정하는 상황이라면?
즉, PizzaChef가 의존하고 있는 PizzaRecipe를 외부(사장님, PizzaRestaurantOwner)에서 결정하고 주입하는 것이다.
PizzaChef가 PizzaRecipe를 사용한다면, PizzaChef는 PizzaRecipe에 의존성이 있다고 말한다.
결합도
객체 내부에 다른 객체를 생성하는 것은 결합도를 높이게 만든다.예시
A 클래스 내부에 B 객체를 직접 생성한 경우,
B 객체를 C 객체로 변경하고 싶을 때는 B 객체와 A 클래스까지 수정해야한다.그러나, DI 는 외부에서 객체를 주입받아 결합도를 낮춰준다.
클래스 변수를 결정하는 방법들이 곧 DI를 구현하는 방법이다.
런타임 시점의 의존관계를 외부에서 주입하여 DI 구현이 완성된다.
생성자를 통해, 의존 관계를 주입
예시 1
class PizzaChef {
private PizzaRecipe pizzaRecipe;
public PizzaChef(PizzaRecipe pizzaRecipe) {
this.pizzaRecipe = pizzaRecipe;
}
}
//PizzaRestaurantOwner 가
class PizzaRestaurantOwner {
//객체 PizzaChef 에게 객체 PizzaRecipe 를 "주입"
private PizzaChef pizzaChef = new PizzaChef(new PizzaRecipe());
public void changeMenu() {
PizzaChef = new PizzaChef(new CheesePizzaRecipe());
}
}
예시 2
@Component
public class Test{
private final Test test;
// 생성자가 한개만 존재할 경우 @Autowired 생략해도 주입 가능
@Autowired
public Test(Test test) {
this.test = test;
}
}
예시 3
@Service
public class UserService {
private UserRepository userRepository;
private MemberService memberService;
// 생성자가 한 개만 존재할 경우 @Autowired 생략해도 주입 가능
@Autowired
public UserService(UserRepository userRepository, MemberService memberService) {
this.userRepository = userRepository;
this.memberService = memberService;
}
}
생성자의 호출 시점에 "1회" 호출 되는 것이 보장된다.
그렇기 때문에 주입받는 객체의 변화가 없거나 반드시 객체의 주입이 필요한 경우에 강제하기 위해 사용이 가능하다.
@RequiredArgsConstructor 을 통한, 생성자 주입
문제: 생성자 주입의 단점은 '생성자를 만들기 번거로움'
해결책: @RequiredArgsConstructor
--> final 또는 @NotNull 을 필드 앞에 붙이면, 생성자를 자동 생성해준다.
--> 의존성이 多 경우, 간결한 생성자 주입이 가능@RequiredArgsConstructor class PizzaChef { private final PizzaRecipe pizzaRecipe; ... }
@Autowired 를 사용한 방식은 결합도를 높이므로,
최근에는 @RequiredArgsConstructor 을 多 사용
필드 값을 변경하는 Setter를 통해, 의존 관계를 주입
(생성자 주입과 다르게) 주입받는 객체가 변할 수 있는 경우에 사용
→ 실제로 변경이 필요한 경우는 小
예시 1
class PizzaChef {
private PizzaRecipe pizzaRecipe = new PizzaRecipe();
public void setPizzaRecipe(PizzaRecipe pizzaRecipe) {
this.pizzaRecipe = pizzaRecipe;
}
}
//PizzaRestaurantOwner 가
class PizzaRestaurantOwner {
private PizzaChef pizzaChef = new PizzaChef();
// 객체 PizzaChef 에게 객체 PizzaRecipe 를 "주입"
public void changeMenu() {
pizzaChef.setPizzaRecipe(new CheesePizzaRecipe());
}
}
예시 2
@Component
public class Test{
private Test test;
@Autowired
public void setTest(Test test) {
this.test = test;
}
}
예시 3
@Service
public class UserService {
private UserRepository userRepository;
private MemberService memberService;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Autowired
public void setMemberService(MemberService memberService) {
this.memberService = memberService;
}
}
주의!
문제: @Autowired 로 주입할 대상(= XXX 빈)이 없는 경우에는 오류가 발생
해결법: 주입할 대상이 없어도 동작하도록 하려면, @Autowired(required = false) 를 통해 설정 가능
필드에 바로 의존 관계를 주입
예시 1
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private MemberService memberService;
}
예시 2
@Component
public class Test{
@Autowired
private Test test;
}
장점
1. 코드가 간결해짐 → 과거에 多 이용
단점
1. 외부에서 접근(변경)이 불가능 → 테스트 코드의 중요성이 부각됨에 따라, 필드의 객체를 수정할 수 없는 필드 주입은 거의 사용 X
2. DI 프레임워크(스프링같은)가 존재해야지만 사용이 가능
=> 애플리케이션의 실제 코드와 무관한 테스트 코드나 설정을 위해 불가피한 경우에만 이용하도록 하자.
수정자 주입 or 일반 메소드 주입을 이용할 경우,
불필요하게 수정의 가능성을 열어두어 유지보수성 감소시킴
따라서, 생성자 주입을 통해 필드에 final 을 사용하여 불변성을 유지하므로 안정성은 ↑
특정 프레임워크에 의존한 테스트(= 생성자 주입이 아닌 다른 주입으로 작성된 코드)는 침투적이므로 X
따라서, 순수한 자바 코드로 단위 테스트를 작성할 수 있는 생성자 주입을 이용하는 것이 좋다.
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private MemberService memberService;
public void register(String name) {
userRepository.add(name);
}
}
순수 자바 테스트 코드
public class UserServiceTest {
@Test
public void addTest() {
UserService userService = new UserService();
userService.register("MangKyu");
}
}
이렇게 생성자 주입을 사용할 경우,
- 컴파일 시점에 객체를 주입받아 테스트 코드를 작성 가능
- 주입하는 객체가 누락된 경우 컴파일 시점에 오류를 발견 가능
- 테스트를 위해 만든 Test객체를 생성자로 넣어 편리함
생성자 주입 : 필드 객체에 final 키워드 사용 가능하며, 컴파일 시점에 누락된 의존성을 확인 가능
(객체의 생성과 "동시에" 의존성 주입)
→ final 키워드를 붙이면, Lombok과 결합되어 코드를 간결하게 작성 가능
(생성자가 1개인 경우 @Autowired를 생략 가능하며, 해당 생성자를 Lombok으로 구현했기 때문)
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final MemberService memberService;
public void register(String name) {
userRepository.add(name);
}
}
다른 주입 방법들 : 객체의 생성(생성자 호출) 이후에 호출되므로, final 키워드 사용 X
필드 주입을 사용할 경우,
@Autowired 를 이용하게 되는데, 이것은 스프링이 제공하는 어노테이션이다.
이 어노테이션을 사용한다면, UserService 에 스프링 의존성이 침투하게 된다.
import org.springframework.beans.factory.annotation.Autowired;
// 스프링 의존성이 UserService 에 import 되어 코드로 박혀버림
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private MemberService memberService;
}
따라서, 더 유연한 코드를 확보하고자 스프링에 의존하지 않는 생성자 주입이 좋다.
애플리케이션 구동 시점(클라이언트 구동 시, 객체의 생성 시점)에
Bean에 등록하기 위해 객체를 생성하는데, 이 때 순환참조를 찾으면 바로 에러를 내뱉는다.
참고: 순환 참조 (Circular Reference)
UserService
@Service
public class UserService {
// 필드을 사용해 서로 호출하는 코드
// UserSerivce가 MemberService에 의존
@Autowired
private MemberService memberService;
@Override
public void register(String name) {
memberService.add(name);
}
}
MemberService
@Service
public class MemberService {
// 필드을 사용해 서로 호출하는 코드
// UserSerivce가 이미 MemberService에 의존하고 있는데, MemberService 역시 UserService에 의존
@Autowired
private UserService userService;
public void add(String name){
userService.register(name);
}
}
결과적으로
두 메소드는 서로를 계속 호출하게 되고, 메모리에 함수의 CallStack이 계속 쌓여 StackOverflow 에러가 발생
만약 이러한 문제를 발견하지 못하고 서버가 운영된다면,
해당 메소드의 호출 시 StackOverflow 에러에 의해 서버가 죽게 될 것임
애플리케이션 구동 시점(객체의 생성 시점, Bean에 등록하기 위해 객체를 생성하는 과정)에서 순환 참조가 발생하고, 이 순환 참조가 에러를 일으킨다.
// 순환 참조 예시
new UserService(new MemberService(new UserService(new MemberService()...)))
따라서, 생성자 주입은 순환 참조를 찾는 즉시 에러를 뱉으므로 순환 참조 에러를 예방할 수 있다.
@Autowired
private Test test; // Test 타입인 bean 으로 연결
@Inject
private Test test; // Test 타입인 bean 으로 연결
@Resource
private Test test; // test 라는 이름의 bean 으로 연결
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Target({ METHOD, CONSTRUCTOR, FIELD })
@Target({TYPE, FIELD, METHOD})
@Primary
- Spring Boot에서 어노테이션을 통해 자동으로 빈을 컨테이너에 설정하는 경우, 같은 인터페이스의 구현체 클래스 두 개 이상이 빈으로 등록되면, 2개의 빈이 존재한다며 오류가 발생 -> 해결 방법 中 하나는 @Primary 를 사용하는 것
- 여러 빈이 있을 때 기본적으로 선택될 빈에 @Primary 어노테이션을 붙여주면 자동적으로 해당 빈이 선택됨
- Qualifier 방법보다 간단(주입 받을 때마다 모든 코드에 @Qualifier 어노테이션을 붙여줘야 함)
@Component @Primary public class Dog implements Animal { public String sound() { return "왈왈"; } } @Component public class Cat implements Animal { public String sound() { return "야옹"; } } @Service public class AnimalService { private final Animal animal; @Autowired public AnimalService(Animal animal) { this.animal = animal; } }
'의존한다'는 것은 그 의존 대상의 '변화에 취약하다'는 것이다.
DI로 구현하게 되었을 때, 주입받는 대상이 변하더라도 그 구현 자체를 수정할 일이 없거나 줄어들게 된다.
기존에 PizzaChef 내부에서만 사용되었던 PizzaRecipe를 별도로 구분하여 구현하면, 다른 클래스에서 재사용할 수가 있다.
하나의 객체에 의존하지 않기 때문이다.
PizzaRecipe의 테스트를 PizzaChef 테스트와 분리하여 진행할 수 있다.
PizzaRecipe의 기능들을 별도로 분리하게 되어 가독성이 높아진다.
Spring 이 없었을 때, 의존성이 필요한 객체를 개발자가 직접 인스턴스화 시켜서 객체의 생명주기를 직접 관리했다.
그러나, 현재는 이를 Spring (= 외부)에게 위임했고(제어권을 Spring 에게 위임), 개발자는 비즈니스 로직 구성에만 집중하도록 하는 것을 말한다.
용도에 맞게 필요한 객체를 그냥 가져다 사용
객체간의 결합도 ↓
유연한 코드를 작성할 수 있게 하여 가독성 ↑
코드의 중복을 줄여 유지보수를 편하게 함
이렇게 Spring에 의하여 관리당하는 자바 객체를 사용하는데, 이 객체를 '빈(bean)'이라 한다.
(= Spring IoC Container 가 제어권을 가지고, 직접 생성하고 관계를 부여하는 대상이 되는 자바 객체)
개발자들이 직접 Object 를 관리하지 않고, IoC Container 에 제어권을 맡기는 이유?
효율적인 코딩을 위해!Application 을 제작하려고 할 때 우리는 설계에 많은 시간을 소모하는데,
그 이유는 제작 전에 아키텍처, 스키마, 코드 컨벤션 등을 상황에 맞게 효율적으로 맞추고 시작하지 않으면, 제작이 완료된 후 성능이 매우 떨어지거나 유지보수에서 많은 비용이 들 수 있기 때문그래서 개발자가 IoC Container에게 제어를 맡김으로써 메모리를 효율적으로 관리하고 코드의 중복을 줄여서 가독성 및 유지보수에 많은 도움이 됨
(IoC Container 는 위 코드와 같이 아주 복잡하고 많은 내부 로직을 통해, Singletone의 한계점을 해결하여 효율적으로 Object 를 관리)
Spring 이 실행되는 순간, Bean 이 생성된다.
그리고 이 Bean 은 Container (ApplicationContext의 구현체) 에 key-value 방식으로, Spring 이 관리하는 객체의 Heap 메모리 주소 정보를 가진 채로 생성되어 있다.
해당 객체가 필요한 곳에 이 Bean 을 자동 주입하게 된다.
이런 이유 때문에, Spring 을 Container 라고 지칭하기도 한다.
Spring 은 기본적으로 모든 Bean 을 Singleton 으로 생성하여 관리한다.
→ 기본적으로 모든 Bean 은 Scope 가 명시적으로 지정되지 않으면, Singleton 이다.
Singleton Bean 은 Spring Container 에서 한 번 생성 후, Container 가 사라질 때 Bean 도 같이 제거된다.
생성된 하나의 Instance 는 Single Beans Cache 에 저장되고, 해당 Bean 에 대한 요청과 참조가 있으면 캐시된 객체를 반환한다.
→ 이때, 객체는 하나만 생성되기 때문에 동일한 것을 참조함
→ Application 구동 시, JVM 안에서 Spring 이 Bean 마다 하나의 객체를 생성한다.
→ 따라서, Spring 을 통해서 Bean 을 주입 받으면, 언제나 주입받은 Bean 은 동일한 객체라는 가정하에서 개발
"빈(Bean)을 모아둔 통"
자바 객체(빈(Bean))의 생명 주기를 관리하며, 생성된 자바 객체들에게 추가적인 기능을 제공
IoC와 DI의 원리가 이 스프링 컨테이너에 적용됨
new 연산자, 인터페이스 호출, 팩토리 호출 방식으로 객체를 생성하고 소멸시킬 수 있는데,
스프링 컨테이너가 이 역할을 대신해 줌 (= 제어 흐름을 외부에서 관리하는 것)
객체들 간의 의존 관계를 스프링 컨테이너가 런타임(컴파일 과정을 마친 컴퓨터 프로그램이 실행되고 있는 환경 또는 동작되는 동안의 시간) 과정에서 알아서 만들어 줌
= 즉, 우리의 코드를 읽어서 객체에 대한 정보를 알아두고, 이용자가 호출 시 그 때 그에 맞는 객체를 할당해준다.
Container
- Spring 에서의 Container 는 인스턴스의 생명주기를 관리 및 제어하는 기능을 제공
(Spring에는 ServletContext 처럼 Servlet을 관리하는 Container 등이 존재)- Context 란?
- 여러 Thread 에서 공통의 자원을 공유하기 위해 사용하는 Container
- Security에서 학습한 인증객체를 담고 있는 SecurityContextHolder 도 마찬가지로 인증객체를 저장하고 관리 및 제어하는 Container
객체를 생성하고, DI를 처리해주는 기능만을 제공
Bean 등록, 생성, 조회, 반환을 관리
빈 자체가 필요하게 되기 전까지는 인스턴스화를 하지 않음
getBean() 메소드를 통해 빈을 인스턴스화할 수 있음
BeanFactory와 유사하지만 좀 더 많은 기능을 제공(BeanFactory 등을 상속하여 확장한 Container)
국제화가 지원되는 텍스트 메시지를 관리
BeanFactory 기능, 환경 변수 관련 처리, 리소스 조회 등의 기능
파일 자원(이미지같은)을 로드할 수 있는 포괄적인 방법을 알려줌
리스너로 등록된 빈에게 이벤트 발생을 알려줌
컨텍스트 초기화 시점에 모든 싱글톤 빈을 미리 로드한 후 애플리케이션 가동 후에는 Bean을 지연없이 얻을 수 있음
(= 미리 Bean을 생성해 놓아 빈이 필요할 때 즉시 사용할 수 있도록 보장)
BeanFactory vs ApplicationContext
- 기능: BeanFactory < ApplicationContext 가 더 多 기능 제공
- 지연
- BeanFactory: 처음으로 getBean() 메소드가 호출된 시점에서야 해당 빈을 생성
- ApplicationContext: Context 초기화 시점에 모든 싱글톤 빈을 미리 로드한 후 애플리케이션 가동 후에는 빈을 지연 없이 받을 수 있음
Spring IoC Container (= Spring IoC Container) 가 IoC Container 를 만들고,
IoC Container 안에 Bean 을 등록할 때 사용하는 Interface들을 Life Cycle Callback
이라고 부른다.
Life Cycle Callback 中 Annotation Processor가 등록돼있다.
→ 여기서의 Annotation Processor 는 @ComonentScan, @Component 어노테이션이 붙어있는 Class 말한다.
→ 해당 class 는 @ComonentScan, @Component 이 붙어있는 모든 Class 의 Instance 를 생성해 Bean 으로 등록하는 작업을 수행
→ 즉, @ComponentScan, @Component 을 사용해서 Bean 을 등록
@ComponentScan
어느 지점부터 Component 를 찾으라고 알려주는 역할
@Component
실제로 찾아서 Bean 으로 등록할 Class
package 에서부터 해당 패키지의 하위 모든 package 또는 Class (@ComponentScan 이 붙은)를 찾아 다니면서,
@Component 가 부여된 또는 @Component 를 사용하는 다른 어노테이션을 사용하는 Class 를 자동으로 Bean 으로 등록한다.
→ @Component 하위 애노테이션 : @Configuration, @Controller, @Service, @Repository 등...이 있다.
Bean 설정 파일은 일반적으로는 XML에 설정하지만('(3) XML 파일에 설정' 참고), 최근 추세는 Java 설정 파일을 많이 사용
@Bean
직접 Bean 을 정의해서, 자동으로 Bean 으로 등록되게 한다.
@Configuration
내부적으로 @Component 를 사용하므로, @ComponentScan 의 검색 대상이 되고
그에 따라 Bean 을 정의한 @Configuration 이 읽힐 때, 그 안에 정의한 Bean들이 IoC Container 에 등록된다.
사용되는 상황?
@Configuration
public class ExampleConfiguration {
@Bean
public ExampleController exampleController() {
return new ExampleController;
}
}
exampleController()에서 리턴되는 객체(ExampleController)가 IoC Container 안에 Bean으로 등록된다.
주의!
1개 이상의 @Bean 을 제공하는 클래스의 경우, 반드시 @Configuration 을 명시해 주어야 Singletone 이 보장된다.
XML 방식으로 Bean 을 정의하는데 필요한 속성
class(필수) : 정규화된 자바 class 이름
id : bean의 고유 식별자
scope : 객체의 범위 (sigleton, prototype 등)
constructor-arg : 생성 시 생성자에 전달할 인수
property : 생성 시 bean setter에 전달할 인수
init-method, destroy-method
기본적인 양식
<!-- A simple bean definition -->
<bean id="..." class="..."></bean>
<!-- A bean definition with scope-->
<bean id="..." class="..." scope="singleton"></bean>
<!-- A bean definition with property -->
<bean id="..." class="...">
<property name="message" value="Hello World!"/>
</bean>
<!-- A bean definition with initialization method -->
<bean id="..." class="..." init-method="..."></bean>
예시
<?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 http://www.springframework.org/schema/context/spring-context-3.2.xsd">
<bean id="dog" class="com.spring.Dog">
<property name="myName" value="poodle"></property>
</bean>
<bean id="cat" class="com.spring.Cat">
<property name="myName" value="bella"></property>
</bean>
<bean id="petOwner" class="com.spring.PetOwner" scope="singleton">
<constructor-arg name="animal" ref="dog"></constructor-arg>
</bean>
</beans>
참고: [Spring] 다양한 의존성 주입 방법과 생성자 주입을 사용해야 하는 이유 - (2/2)
참고: [Spring] Bean 정리