이전에 공부한 내용을 다시 상기시켜볼겸 스프링의 핵심 기술중 하나인 IoC,DI에 대해 다시 알아보려고한다.
IoC 와 DI란?
아래 코드를 보면 @Service라는 어노테이션으로 MemoService라는 클래스에 등록하였고 내부에는 두 개의 필드가 존재한다.
그렇다면 MemoService는 Controller또는 Repository와 연결하여 레이어 아키텍처로 설계를 할 수 있고 각 클래스간 연결이 필요하다, 또한 이 연결을 위해 명시하는 필드 memoRepository, jdbcTemplate는 MemoService클래스에서 생성자의 매개변수 또는 필드로 연결되어야 한다.
이러한 과정을 Spring에서는 우리가 직접하는 것이 아닌 애플리케이션 시작시에 자동으로 Bean이라는 객체로 생성하고 이를 연결시켜주는 작업을 대신해준다. 이때 IoC(Inversion Of Control)이라고하는 제어(Controle)의(of) 역전(Inversion)이라는 기술과 DI(Dependency Injection)이라고 하는 의존성(Dependency) 주입(Injection)의 기술의 사용이다.
그렇다면 어떻게 빈이라는 객체로 생성하고 주입을 해주는 걸까?
@Service
@RequiredArgsConstructor
public class MemoService {
private final MemoRepository memoRepository;
private final JdbcTemplate jdbcTemplate;
public MemoResponseDto createMemo(MemoRequestDto requestDto) {
// RequestDto -> Entity
Memo memo = new Memo(requestDto);
// DB 저장
Memo saveMemo = memoRepository.save(memo);
// Entity -> ResponseDto
MemoResponseDto memoResponseDto = new MemoResponseDto(saveMemo);
return memoResponseDto;
}
IoC
아래 코드를 보면 어노테이션으로 @SpringBootApplication 이라는 어노테이션이 붙어있는것을 볼 수 있다.
@SpringBootApplication
public class MemoApplication {
public static void main(String[] args) {
SpringApplication.run(MemoApplication.class, args);
}
}
이 어노테이션 내부를 들여다보자. @ComponentScan이라는 어노테이션을 확인할 수 있다.
그리고 그 아래에는 @AliasFor이라는 별칭을 설정할 수 있는 어노테이션으로 컴포넌트스캔이 수행되는 별칭들도 있는 것을 볼 수 있다.
@ComponentScan이라는 어노테이션을 통해 MemoApplicatoin 하위에 있는 객체들을 돌아다니며 @Component , @Controller, @Service, @Repository 등의 어노테이션이 붙은 클래스를 스캔하고 가져와서 Bean객체로 생성하게 된다.
그렇다면 왜 @ComponentScan이 이러한 어노테이션들을 인식할 수 있는지 알아보자.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
@AliasFor(
annotation = EnableAutoConfiguration.class
)
Class<?>[] exclude() default {};
@AliasFor(
annotation = EnableAutoConfiguration.class
)
String[] excludeName() default {};
@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackages"
)
String[] scanBasePackages() default {};
@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackageClasses"
)
Class<?>[] scanBasePackageClasses() default {};
@AliasFor(
annotation = ComponentScan.class,
attribute = "nameGenerator"
)
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
이 코드는 @Service 어노테이션의 내부 내용이다. 보시다시피 @Component라는 어노테이션이 들어있는 것을 확인할 수 있다.
다시말해 @ComponentScan은 @Component어노테이션이 포함되어 있는 객체들을 인식할 수 있도록 설정되어 있다는 것이다.
사진처럼 Bean으로 생성된 객체는 왼쪽에 콩모양을 확인할 수 있다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
의존성 주입(DI)
그렇다면 필드에 있는 이 두개의 클래스또한 빈으로 생성되었을때 어떻게 연결되는 걸까?
기본적으로는 @Autowired 어노테이션이 적용되어 있는 필드들 간에 의존성을 주입하게 된다. 단 @Autowired 어노테이션을 적용할 수 있는 필드들은 당연하지만 Bean객체들이어야 한다. 스프링 컨테이너에 들어있어야 연결을 할 수 있기 때문이다.
방법은 총 3가지가 있고 가장 많이 사용하는 방법은 생성자에 매개변수로 넣는 방법이다.
나머지는 필드 주입, 메서드 주입 방식이 있는데 필드 주입은 단점이 커서 잘 사용하지 않으며, 메서드 주입은 인터페이스를 통한 하위 객체 변경등의 요구사항이 있을때 주로 사용한다.
@Service
@RequiredArgsConstructor
public class MemoService {
private final MemoRepository memoRepository;
private final JdbcTemplate jdbcTemplate;
DI를 하는 원리
객체를 주입하기 위해서는 각 객체의 Bean을 찾아야한다고 했는데 어떻게 찾는지 알아보자 방법은 크게 두 가지로 나뉜다. 코드의 주석과 함께 어떻게 객체를 찾아오는지 확인해보자. ApplicationContext는 스프링 컨테이너 내부에 등록된 빈이 담겨있다. 이 공간에 있는 빈들을 찾는 것이다.
@Service
public class MemoService {
private final MemoRepository memoRepository;
public MemoService(ApplicationContext context) {
//1. Bean에서 Class 형식으로 가져오기
// memoRepository = context.getBean(MemoRepository.class);
//2. Bean의 이름으로 가져오기
memoRepository= (MemoRepository) context.getBean("memoRepository");
}
1. 생성자 주입
생성자 주입은 대부분 Lombok 라이브러리를 많이 사용하게 된다. Lombok에는 @Getter, @Setter, @Data, @RequiredArgusConstructor, @AllArgusConstructor 등 객체 생성에 필요한 다양한 어노테이션을 제공하며 생성자 주입에 필요한 어노테이션은 @RequiredArgusConstructor 이녀석이다.
위 코드처럼 필드에 final이 붙어있고 해당 어노테이션이 명시되어 있으면 객체 생성시에 필드간의 의존성이 주입되어 해당 클래스의 비즈니스 로직에 두 필드 변수를 사용할 수 있다.
생성자 주입은 객체의 생성시에 주입되는 방식이기 때문에 불변의 속성을 띄고, 컴파일 시 오류가 발생하기 때문에 사전에 발생할 수 있는 오류를 예방할 수 있다.
Spring 4.3 버전 이후부터는 생성자에 넣을 필드가 하나일때에는 생성자에 @Autowired를 적용하지 않아도 자동으로 빈으로 연결해주는 편리한 기능도 있으니 참고하면 좋을 것 같다.
@Service
public class MemoService {
private final MemoRepository memoRepository;
//두 개 이상의 매개변수가 들어가는 경우 @Autowired를 적용해야 함
public MemoService(MemoRepository memoRepository) {
this.memoRepository = memoRepository;
}
2. 메서드 주입
말 그대로 메서드를 만들어서 주입하는 방식이다. 위 방법과 차이점으로는 final키워드가 빠져있는 것을 볼 수 있다.
final은 불변 키워드이기때문에 메서드 주입시에는 명시하게 되면 해당 객체에 주입을 할 수 없기 때문에 예외가 발생한다.
@Service
public class MemoService {
private MemoRepository memoRepository;
@Autowired
public void changeDI(MemoRepository memoRepository) {
this.memoRepository = memoRepository;
}
3. 필드 주입
필드 주입은 사용을 권하지 않는 이유가 강한 의존성으로 연결되기 때문이다. 이전 메서드 또는 생성자 방식의 주입 방법은 합성의 방법으로 두 객체의 의존성을 주입했다면 필드는 합성이 아닌 직접적인 연결의 방법을 수행하기 때문에 각 객체간의 영향력이 매우 커진다.
'Spring' 카테고리의 다른 글
[Spring / Swagger] Cors , Fail to Fetch 해결방법 (0) | 2025.01.24 |
---|---|
[Elastic Search] Elastic Search 개념 (1) | 2024.12.24 |
테스트 코드의 기본 이론 (1) | 2024.11.16 |
[Spring/ 환경변수 설정] 로컬에서 IntelliJ 환경변수 설정하기 (0) | 2024.08.12 |
[Spring- LogBack] LogBack과 Log4j2 중 어떤걸 사용할까? (0) | 2024.08.03 |