1. DI 개념 소개 및 장점
DI ( Dependency Injection )
의존성 주입
- 한 객체가 다른 객체를 사용할 때 의존성이 있다고 한다
- 런타임 시 의존 관계를 맺는 대상을 외부에서 결정하고 주입해 주는 것
- 스프링 프레임워크는 DI 기능을 지원해주는 프레임워크
DI 장점
1. 의존성 주입을 인터페이스 기반으로 설계하면 코드가 유연해진다
- 느슨한 결합도 ( loose coupling )
2. 변경에 유연해짐
- 결합도가 낮은 객체끼리는 부품을 쉽게 갈아끼울 수 있음
3. 테스트하기 좋은 코드가 됨
DI 프레임워크 구조
package org.example.di;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
public class BeanFactory {
private Set<Class<?>> preInstantiatedBeans;
private Map<Class<?>, Object> beans = new HashMap<>();
public BeanFactory(Set<Class<?>> preInstantiatedBeans) {
this.preInstantiatedBeans = preInstantiatedBeans;
initialize();
}
@SuppressWarnings("unchecked")
public void initialize() {
for (Class<?> clazz : preInstantiatedBeans) {
Object instance = createInstance(clazz);
beans.put(clazz, instance);
}
}
private Object createInstance(Class<?> concreteClass) {
Constructor<?> constructor = findConstructor(concreteClass);
List<Object> parameters = new ArrayList<>();
for (Class<?> typeClass : Objects.requireNonNull(constructor).getParameterTypes()) {
parameters.add(getBean(typeClass));
}
try {
return constructor.newInstance(parameters.toArray());
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
private Constructor<?> findConstructor(Class<?> concreteClass) {
Constructor<?> constructor = BeanFactoryUtils.getInjectedConstructor(concreteClass);
if (Objects.nonNull(constructor)) {
return constructor;
}
return concreteClass.getConstructors()[0];
}
public <T> T getBean(Class<T> requiredType) {
return (T) beans.get(requiredType);
}
}
이 코드는 Java로 작성된 간단한 의존성 주입 ( Dependency Injection ) 을 처리하는 BeanFactory 클래스를 나타낸다.
의존성 주입은 객체 간의 의존성을 관리하고 객체를 생성하고 조립하는 디자인 패턴이다.
1. 필드 및 생성자
private Set<Class<?>> preInstantiatedBeans;
: 미리 생성된 빈들을 저장하는 집합(Set)
private Map<Class<?>, Object> beans = new HashMap<>();
: 클래스와 인스턴스를 매핑하여 저장하는 맵
public BeanFactory(Set<Class<?>> preInstantiatedBeans) {
this.preInstantiatedBeans = preInstantiatedBeans;
initialize();
: 생성자는 미리 생성된 빈들을 받아들여 필드에 할당한 후 initialize() 메스드를 호출하여 초기화 한다.
- 이 작업을 하는 이유 -
BeanFactory 의 생성자가 미리 생성된 빈들을 받아들여 필드에 할당하고 초기화하는 이유는 BeanFactory를 사용하는 과정에서 기본적인 설정이나 빈들의 초기화 작업을 수행하기 위함이다.
1. 준비 작업 수행
- 생성자에서 초기화를 수행함으로써 BeanFactory는 처음부터 필요한 데이터를 갖추고 시작할 수 있다.
- 필요한 빈들의 목록을 미리 받아들여 필드에 할당하여 이후 해당 빈들을 생성하고 관리하는 데 사용된다.
2. 의존성 관리
- 미리 생성된 빈들의 목록을 받아들여야 해당 빈들 간의 의존성을 파악하고 관리할 수 있다.
- 의존성 주입을 위해 빈들을 초기화할 때 다른 빈들을 필요로 할 수 있으며 이를 위해 미리 필요한 빈들을 받아들여 초기화 하는 것이 중요하다.
3. 효율성과 일관성
- 초기화 작업을 생성자에서 수행하면 BeanFactory 인스턴스가 생성될 때 바로 필요한 적업을 수행할 수 있다.
이는 효율성과 일관성을 유지하는 데 도움이 된다
- 사용자가 BeanFactory 인스턴스를 얻고 난 후에 별도의 초기화 메서드를 호출하지 않아도 된다.
따라서 생성자에서 초기화를 수행함으로써 BeanFactory는 사용자가 빈들을 요청하거나 사용할 때 즉시 사용 가능한 상태로 있게 된다.
2. initialize() 메서드
public void initialize() {
for (Class<?> clazz : preInstantiatedBeans) {
Object instance = createInstance(clazz);
beans.put(clazz, instance);
initialize() 메서드는 미리 생성된 빈들을 반복하며 각 클래스에 대한 인스턴스를 생성하고 beans 맵에 저장한다.
- 이 작업을 하는 이유 -
1. 인스턴스 생성 및 저장
- preInstantiatedBeans 에 포함된 클래스들은 미리 생성되어야 하는 빈들의 목록이다
- createInstance() 메서드를 통해 각 클래스에 대한 인스턴스를 생성하고 이를 beans 맵에 해당 클래스와 함께 저장한다.
2. 의존성 주입
- 이 과정에서 각 빈이 필요로 하는 의존성을 만족시키기 위해 생성된 인스턴스들을 beans 맵에 저장한다
- 생성된 빈들의 의존성이 다른 빈들에 의존할 때 해당 의존성을 만족시켜주기 위해 미리 생성된 빈들을 저장하는 것이 중요하다.
3. 성능과 효율성
- 미리 생성된 빈들을 초기화하여 저장해 두면 실제 사용 시에는 이미 생성된 인스턴스를 가져다 사용할 수 있다 매번 요청할 때마다 새로운 인스턴스를 생성하는 것보다 효율적이다.
4.인스턴스 재사용
- 이미 생성된 인스턴스를 재사용하여 메모리와 자원을 절약할 수 있다 같은 종류의 빈이 필요한 경우 매번 새로운 인스턴스를 생성하는 것이 아니라 미리 생성된 인스턴스를 활용할 수 있다.
이 작업은 의존성 주입 컨테이너의 핵심인 빈들의 생성,관리,의존성 샣결에 필요한 초기화 작업을 수행하는 과정이다.
코드에서 initialize() 메서드는 BeanFactory가 사용될 때 필요한 작업을 준비하고 초기화하는 역활을 한다.
3. createInstance() 메서드
private Object createInstance(Class<?> concreteClass) {
Constructor<?> constructor = findConstructor(concreteClass);
List<Object> parameters = new ArrayList<>();
for (Class<?> typeClass : Objects.requireNonNull(constructor).getParameterTypes()) {
parameters.add(getBean(typeClass));
}
try {
return constructor.newInstance(parameters.toArray());
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
- createInstance() : 메서드는 주어진 클래스의 인스턴스를 생성한다.
- 해당 클래스의 생성자를 찾고 생성자에 필요한 파라미터들을 주입하기 위해 getBean() 메서드를 사용한다.
- 생성자를 통해 인스터스를 생성하고 반환한다.
- 이 작업을 하는 이유 -
1. 의존성 주입
- 클래스의 생성자를 통해 인스턴스를 만들 때 해당 클래스가 필요로 하는 다른 빈들의 인스턴스를 주입하기 위함이다.
- getParameterTypes() 를 사용하여 생성자의 매개변수 타입들을 얻어 해당 타입들에 맞는 빈을 getBean() 을 통해 가져와 인자로 전달한다.
2. 유연성과 확장성
- 리플렉션(reflection)을 활용하여 클래스의 생성자를 동적으로 결정하고 해당 생성자에 필요한 파라미터들을 동적으로 결정된 빈들로 주입할 수 있다.
- 이를 통해 새로운 빈이 추가되거나 변경될
3. 동적 객체 생성
- 프로그램 실행 중에 클래스의 인스턴스를 생성할 수 있어야 할 떄 리플렉션을 통해 동적으로 인스턴스를 생성할 수 있습니다.
- 이는 프로그램의 유연성을 높이고 런타임 시 동적으로 객체를 생성할 필요성이 있을 때 유용하다.
위와 같은 이유로 createInstance() 메서드는 주어진 클래스의 인스턴스를 생성하고 해당 클래스가 필요로 하는 의존성을 주입하여 유연하고 동적인 방식으로 객체를 생성하는 데 사용된다.
4. findConstructor() 메서드
private Constructor<?> findConstructor(Class<?> concreteClass) {
Constructor<?> constructor = BeanFactoryUtils.getInjectedConstructor(concreteClass);
if (Objects.nonNull(constructor)) {
return constructor;
}
return concreteClass.getConstructors()[0];
}
- findConstructor() 메서드는 주어진 클래스에 대한 생성자를 찾아 반환한다
- BeanFactoryUtils.getInjectedConstructor() 를 사용하여 의존성 주입이 필요한 생성자가 있는지 확인하고 없다면 기본 생성자를 반환한다.
- 이 작업을 하는 이유 -
1. 의존성 주입 확인
- BeanFactoryUtils.getInjectConstructor(concreteClass) 를 통해 해당 클래스에 의존성 주입을 위한 특별한 생성자가 있는지 확인한다.
- 프레임워크나 라이브러리에서 사용되는 주입용 어노테이션 또는 규칙을 통해 해당 클래스에 사용되는 주입용 생성자를 찾는다.
2. 기본 생성자 반환
- 주입용 생성자가 없을 경우 클래스에 명시된 생성자 중 첫 번째 생성자
(concreteClass.getConstructors()[0]) 을 반환한다.
- 만약 클래스에 명시된 생성자가 없다면 기본 생성자가 반환된다.
3. 유연성과 확장성
- 프로그램이 클래스를 동적으로 다루고 의존성 주입을 고려하는 경우가 많다
- 위 코드는 이러한 환경에서 클래스에 특정 생성자가 있는지 확인하고 없다면 기본 생성자를 반환하여 해당 클래스의 객체를 생성한다.
이러한 작업을 주어진 클래스에 대한 적절한 생성자를 찾아내고 의존성 주입이 필요한 경우 해당 생성자를 반환하여 객체 생성에 사용된다. 이는 유연성과 코드의 재사용성을 높이는 데 도움이 된다.
5. getBean()
public <T> T getBean(Class<T> requiredType) {
return (T) beans.get(requiredType);
}
- getBean() 메서드는 주어진 클래스의 빈을 반환한다.
- requiredType 에 해당하는 클래스의 인스턴스를 beans 맵에서 가져와 캐스팅하여 반환한다.
- 이 작업을 하는 이유 -
getBean() 메서드는 주어진 타입에 해당하는 빈(인스턴스)을 가져오는 역활을 한다.
1. 빈(Bean) 검색
- 사용자가 요청한 타입에 해당하는 빈(인스턴스)을 찾아서 반환한다.
- 주로 의존성 주입 컨테이너나 BeanFactory와 같은 클래스에서 사용된다. 사용자가 원하는 타입의 빈을 요청할 때 해당 빈을 반환하는 데 활용된다.
2. 의존성 관리
- 객체지향 프로그래밍에서는 객체 간의 의존성을 관리하고 이를 주입하는 것이 중요하다. getBean() 메서드는 이러한 의존성을 관리하기 위해 사용될 수 있다.
3. 싱글톤 관리
- 일반적으로 getBean() 은 싱글톤 패턴을 적용한 빈들을 반환하는 데 사용된다. 여러 번 호출되더라도 동일한 인스턴스를 반환함으로써 메모리와 자원을 절약한다.
4. 다양한 타입 지원
- 제네릭 메서드로 구현되어 있어 사용자가 원하는 타입을 지정하여 해당 타입의 빈을 반환할 수 있다. 이는 유연성을 제공하며 다양한 타입의 빈을 요청할 수 있다.
getBean() 메서드는 주로 의존성 주입 컨테이너나 비슷한 컨테이너에서 사용되며, 사용자가 요청한 타입에 맞는 빈을 반환 하여 의존성을 관리하고 객체를 제공하는 데 활용된다.
이 코드는 미리 지정된 클래스들을 기반으로 의존성을 주입하고 BeanFactory 를 통해 필요한 빈을 얻을 수 있도록 하는 간단한 구현체이다.
package org.example.di;
import org.example.annotation.Inject;
import org.reflections.util.ReflectionUtilsPredicates;
import java.lang.reflect.Constructor;
import java.util.Set;
import static org.reflections.ReflectionUtils.getAllConstructors;
public class BeanFactoryUtils {
@SuppressWarnings({"rawtypes", "unchecked"})
public static Constructor<?> getInjectedConstructor(Class<?> clazz) {
Set<Constructor> injectedConstructors = getAllConstructors(clazz, ReflectionUtilsPredicates.withAnnotation(Inject.class));
if (injectedConstructors.isEmpty()) {
return null;
}
return injectedConstructors.iterator().next();
}
}
1. 클래스와 메서드
- BeanFactoryUtils : 주로 BeanFactory나 의존성 주입과 관련된 유틸리티 메서드를 담은 클래스이다
- getInjectedConstructor() : 주어진 클래스에서 주입용으로 표시된 생성자를 찾아 반환하는 메서드이다.
2. 메서드 동작
- getInjectedConstructor(Class<?> clazz) : 주어진 클래스 ('clazz) 에서 @Inject 어노테이션이 붙은 생성자를 찾아 반환한다.
- getAllConstructors() : Reflections 라이브러리를 사용하여 클래스의 모든 생성자를 가져온다.
- ReflectionUtilsPredicates.withAnnotation(Inject.class): @Inject 어노테이션이 붙은 생성자를 찾기 위한 Reflections의 Predicate(조건)를 정의한다.
- getAllConstructors(clazz, ...)는 clazz 클래스에서 지정된 조건(여기서는 @Inject 어노테이션이 붙은 생성자)에 맞는 모든 생성자를 반환한다.
3. 리턴 값
- Set<Constructor> : 주어진 클래스에서 @Inject 어노테이션이 붙은 생성자들의 집합(Set)을 반환한다.
- 이 메서드는 클래스에서 @Inject 어노테이션이 붙은 생성자를 찾을 수 없을 경우 null 을 반환한다.
4. 목적과 활용성
- 주로 의존성 주입을 구현하는 프레임워크나 라이브러리에서 사용된다.
- 이 유틸리티 메서드는 주입용 생성자를 찾아 의존성 주입을 위해 사용 될수 있다.
이 코드는 Reflections 라이브러리를 활용하여 클래스의 생성자를 검색하고 @Inject 어노테이션이 붙은 생성자를 찾아 반환하는 유틸리티 메서드를 제공한다 이를 통해 의존성 주입을 위한 적절한 생성자를 찾을 수 있다.
내가 정리한 DI 프레임워크란 ?
1. DI의 핵심 개념
DI는 의존성 주입이라는 개념으로, 객체 간의 의존성을 외부에서 주입하는 디자인 패턴입니다. 이는 객체를 직접 생성하고 의존성을 관리하는 것이 아니라, 외부에서 의존성을 주입받아 유연하고 확장 가능한 코드를 작성할 수 있게 해줍니다.
2. DI 프레임워크의 역할
의존성 관리 및 제어권의 이동: DI 프레임워크는 객체의 의존성을 관리하고, 필요한 의존성을 객체에 주입합니다. 이를 통해 객체 간의 결합도를 낮추고 유연성을 높입니다.
구성과 설정의 분리: DI는 객체 생성 및 설정을 분리하여 애플리케이션의 구성을 단순화하고 재사용성을 높입니다. 객체 생성 및 의존성 주입을 외부 설정 파일 또는 어노테이션 등을 통해 관리할 수 있습니다.
3. 깨달은 점
테스트 용이성: DI를 통해 의존성을 주입받는 방식으로 테스트하기 쉽습니다. Mock 객체를 주입하여 유닛 테스트를 보다 쉽게 수행할 수 있습니다.
확장성과 유연성: DI를 사용하면 새로운 기능을 추가하거나 변경할 때 코드 수정 없이 의존성을 변경하거나 추가할 수 있습니다. 이는 애플리케이션의 유지보수와 확장에 큰 장점을 제공합니다.
4. DI 프레임워크 선택 시 고려해야 할 사항
가독성과 학습 곡선: DI 프레임워크의 가독성과 사용하기 쉬운 API가 중요합니다. 학습 곡선이 낮고 직관적인 프레임워크를 선택하는 것이 좋습니다.
성능과 Overhead: DI 컨테이너의 오버헤드와 성능에 대한 고려가 필요합니다. 애플리케이션의 규모와 요구 사항에 맞게 적절한 성능을 제공하는지 확인해야 합니다.
'JAVA > MVC 프레임워크' 카테고리의 다른 글
프런트 컨트롤러 패턴 실습 (0) | 2023.11.10 |
---|---|
프런트 컨트롤러 패턴 개념 (0) | 2023.11.09 |
리플렉션 API 개념 (0) | 2023.11.08 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!