<Spring> Spring Bean, IoC/DI 정리 1
📌 IoC 🟢 IoC 란? IoC는 Inversion of Control의 약자로 ‘제어의 역전’이라는 의미를 가진다. 이때, ‘제어의 역전’은 무슨 의미를 가지는 걸까? 제어 객체 생명주기나 메서드의 호출을 직접 제어한
wtg1026.tistory.com
위 글에서 이어지는 부분입니다!
📌 Component Scan
🟢 Component Scan이란?
스프링이 애플리케이션의 클래스를 검색하고, 자동으로 스프링 빈을 등록하는 방법이다.
이러한 방식으로 빈을 자동으로 구성함으로써 애플리케이션의 구성 및 설정을 간소화할 수 있다.
@ComponentScan , @Component 어노테이션을 이용하면 된다.
🟢 @Component
스프링 빈으로 등록하려는 클래스에 붙일 수 있는 어노테이션이다.
스프링은 이 어노테이션이 붙은 클래스를 자동으로 검색하고 빈으로 등록한다.
이전 예시에 사용했던 class에 @Component를 붙인 후, 빈 조회를 할 경우 등록된 것을 볼 수 있다.
@Component
public class BeanA {
...
}
@Component
public class BeanB {
...
}
@Component
public class BeanC {
...
}
public class AutoAppconfig {}
AnnotationConfigApplicationContext applicationContext
= new AnnotationConfigApplicationContext(AutoAppconfig.class);
@Test
void findAllBean(){
String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = applicationContext.getBean(beanDefinitionName);
log.info("name=" + beanDefinitionName + " object=" + bean);
}
}
하지만 생각과는 다르게 빈이 등록되지 않았다.
@Component를 붙인 클래스를 스프링이 찾지 못했기 때문이다.
🟢 Component Scan
그렇다면 어떻게 스프링이 해당 클래스를 찾도록 지정해줄 수 있을까?
@ComponentScan 어노테이션을 사용하여 스프링이 어느 패키지에서 클래스 검색을 시작할지 및 검색할 패키지의 범위를 설정할 수 있다.
@ComponentScan
public class AutoAppconfig {}
이번엔 @ComponentScan을 붙이고 다시 테스트해보겠다.
클래스 3개 모두 등록된 것을 볼 수 있다.
위 @ComponentScan 설명에서 검색할 패키지의 범위를 설정할 수 있다고 했다.
기본적으로 @ComponentScan 이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
현재 이런 파일 구조를 가지므로 BeanA, BeanB, BeanC 클래스는 컴포넌트 스캔의 대상이 되고, 빈으로 등록이 되는 것을 볼 수 있었다.
등록된 빈은 싱글톤일까?
@Test
void singleton2(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppconfig.class);
BeanB beanB = ac.getBean("beanB", BeanB.class);
BeanC beanC= ac.getBean("beanC", BeanC.class);
BeanA beanA1 = beanB.getBeanA();
BeanA beanA2= beanC.getBeanA();
log.info("beanA1 = " + beanA1);
log.info("beanA2 = " + beanA2);
assertThat(beanA1).isSameAs(beanA2);
}
그렇다 싱글톤이다.
앞서 말한 것처럼 스프링 빈은 기본적으로 싱글톤으로 관리된다.
이처럼 Component Scan을 이용하면 더 쉽게 빈을 등록할 수 있다.
🟢 @SpringBootApplication
스프링 프로젝트를 생성할 경우 루트가 되는 클래스에 @SpringBootApplication 이 붙은 것을 볼 수 있다.
해당 어노테이션의 소스코드를 살펴보자.
@ComponentScan 이 붙어있는 것을 확인할 수 있다.
🟢 @Configuration, @Service, @Controller, @Repository
위 어노테이션의 공통점이 뭘까?
바로 컴포넌트 스캔 대상이라는 점이다.
위에서 @Component 어노테이션이 붙어있으면 컴포넌트 스캔이 된다고 했다.
위 사진에서 볼 수 있듯이 모두 @Component 가 붙어있어서 자동으로 빈 등록이 됩니다.
public class A {
}
@Configuration
public class Appconfig {
@Bean
public A a() {
return new A();
}
}
@Slf4j
@SpringBootApplication
public class GdscDemoApplication {
public static void main(String[] args) {
ApplicationContext applicationContext = SpringApplication.run(GdscDemoApplication.class, args);
log.info("---------- 모든 Bean 출력 ----------");
String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = applicationContext.getBean(beanDefinitionName);
log.info("name=" + beanDefinitionName + " object=" + bean);
}
}
}
그러므로 @Configuration 이 붙어있는 클래스도 빈 등록이 됩니다.
또한 해당 클래스에 포함된 수동으로 등록한 빈 또한 빈으로 등록이 되는 것을 볼 수 있습니다.
📌 DI
🟢 의존성 주입(DI, Dependency Injection)
@Component
public class ClassB {
private ClassA classA;
public ClassB(ClassA classA) {
this.classA = classA;
}
}
모든 코드를 살펴봐도 ClassB 에 ClassA 를 주입해주는 코드가 없는데,
ClassB 가 빈으로 등록되어 있고, 사용 또한 가능하다.
이는 스프링이 내부적으로 의존성을 주입해줬기 때문이다.
우리는 이를 스프링이 DI, 의존성 주입을 해줬다라고 한다.
DI란 외부에서 두 객체 간의 관계를 결정해주는 디자인 패턴으로,
인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않도록 하고
런타임 시에 관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮출 수 있게 해준다.
의존관계 주입은 크게 4가지 방법이 있다.
- 생성자 주입
- 수정자 주입(setter 주입)
- 필드 주입
- 일반 메서드 주입
🟢 @Autowired
스프링에는 여러 가지 의존성 주입이 있지만 가장 간편하게 쓰이는 방법으로는 @Autowired 어노테이션을 활용하는 방법이 있다.
“여기에 의존성을 주입해 줘”라는 뜻이다.
@Autowired를 붙이게 되면 Spring이 자동으로 해당 클래스의 객체를 찾아서 필요한 의존성을 주입해준다.
@Component
public class Pizza {
@Autowired
private Cheese cheese;
}
다만 여기서 주입받을 Cheese 도 스프링 Bean이어야 한다.
🟢 생성자 주입
생성자를 통해 의존 관계를 주입하는 방법이다.
생성자 주입을 사용하면 객체의 최초 생성 시점에 스프링이 의존성을 주입해준다.
그렇기 때문에 주입받은 객체가 변하지 않거나, 반드시 객체의 주입이 필요한 경우에 강제하기 위해 사용할 수 있다.
또한 Spring 프레임워크에서는 생성자 주입을 적극 지원하고 있기 때문에,
생성자가 1개만 있을 경우에 @Autowired 를 생략해도 주입이 가능하도록 편의성을 제공하고 있다.
만약 @Autowired 가 붙은 생성자가 여러 개 있을 경우 가장 많은 의존성을 주입할 수 있는 생성자를 사용해서 의존성 주입한다.
@Component
public class Pizza {
private Cheese cheese;
private Bread bread;
// @Autowired
public Pizza(Cheese cheese, Bread bread) {
this.cheese = cheese;
this.bread = bread;
}
}
🟢 setter 주입
setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.
선택, 변경 가능성이 있는 의존관계에 사용할 수 있다.
setter 메서드에 @Autowired 어노테이션을 붙이면 스프링이 setter를 사용해서 자동으로 의존성을 주입해준다.
이때 빈 객체를 만들고 setter로 의존성을 주입해주기 때문에 빈 생성자가 필요하다.
때문에 파이널 필드를 만들 수 없고 의존성의 불변을 보장할 수 없다는 특징이 있다.
@Component
public class Pizza {
private Cheese cheese;
private Bread bread;
public void setCheese(Cheese cheese) {
this.cheese = cheese;
}
public void setBread(Bread bread) {
this.bread = bread;
}
}
🟢 필드 주입
말 그대로 필드에 바로 주입하는 방법이다.
@Component
public class Pizza {
@Autowired
private Cheese cheese;
@Autowired
private Bread bread;
}
하지만 필드 주입은 더 이상 추천되는 방법이 아니며 심지어 인텔리제이가 경고도 해준다.
필드 주입을 사용하게 되면 테스트 등의 이유로 자동이 아닌 수동 의존성을 주입하고 싶어도,
생성자, setter가 없으므로 우리가 직접 의존성을 넣어줄 수가 없다.
때문에 필드 주입을 사용하게 되면 의존성이 프레임워크에 강하게 종속된다는 문제점이 있다.
애플리케이션의 실제 코드와 관계없는 테스트 코드, 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용해야 한다.
🟢 일반 메소드 주입
한 번에 여러 필드를 주입받을 수 있다.
하지만 일반적으로 잘 안 쓴다.
@Component
public class Pizza {
private Cheese cheese;
private Bread bread;
@Autowired
public void init(Cheese cheese, Bread bread) {
this.cheese = cheese;
this.bread = bread;
}
}
🟢 DI 주의점
1. NullPointerException 방지
필드 주입이나 setter 주입의 경우 스프링의 빈 관리 기능을 빌리지 않고 new 키워드로 객체를 생성해 줄 경우,
NullPointerException이 발생할 수 있다.
왜냐면 이들은 빈 생성자를 사용해 기본적으로 의존성이 없는 상태이기 때문이다.
하지만 생성자 주입은 (완전한 생성자라는 가정 하에) 객체 생성 시점에 모든 의존성을 주입해주므로
Null을 의도적으로 넣어주지 않는 한 NullPointerException이 발생할 수 없다.
2. 순환참조 문제 방지
필드 주입이나 setter 주입을 통해 의존성을 주입하게 되면,
A 객체가 B 객체를 의존하는데 B 객체 또한 A 객체를 의존할 때 생기는 순환참조가 발생할 수 있다.
그러나 생성자 주입을 사용하는 객체들끼리 의존성이 순환되면 스프링은 에러 메시지와 함께 프로그램을 종료한다.
@Component
public class CircuitA {
@Autowired
private CircuitB b;
public void doB() {
b.doA();
}
}
@Component
public class CircuitB {
@Autowired
private CircuitA a;
public void doA() {
a.doB();
}
}
code A와 code B 같은 코드가 있다면 A가 B의 doSomething을 호출하면 B가 다시 A의 doSomething을 호출하고 순환 호출이 반복되다가 결국 스택오버플로우 에러가 발생해서 프로그램이 멈추게 될 것이다.
만약 생성자 주입을 이용한다면 애플리케이션 시작 시에 위의 에러를 통해서 방지해줄 수 있다.
하지만 스프링 2.6 버전부터 setter 주입과 필드 주입도 위의 에러로 알려준다.
🟢 주입 대상이 여러 개일 때
@Component
public class Pizza {
private Cheese cheese;
private Bread bread;
public Pizza(Cheese cheese, Bread bread) {
this.cheese = cheese;
this.bread = bread;
}
}
public interface Cheese {
}
@Component
public class CheddarCheese implements Cheese{
}
@Component
public class MozzarellaCheese implements Cheese{
}
만약 Pizza 에서 Cheese 를 의존성 주입하고 싶을 때,
Bean에 MozzarellaCheese 와 CheddarCheese 가 둘 다 등록되어 있을 때 어떤 것을 선택해야 하는지 모른다.
이때는 아래 순서대로 기준을 정한다
- 타입
- 이름
타입이 1순위이다. 우선 정의되어 있는 타입을 기준으로 찾는다.
CheddarCheese와MozzarellaCheese 서비스 모두 Cheese 의 구현체임으로 Cheese 라는 타입으로 검색된다.
이렇게 타입을 기준으로 여러 Bean이 검색되었다면 스프링은 그다음으로 Bean의 이름을 기준으로 의존성을 주입한다.
이때 주입하는 데 사용하는 메서드의 매개변수명과 등록된 Bean의 이름이 일치하는지 체크한다.
@Component
public class Pizza {
private Cheese cheese;
private Bread bread;
public Pizza(Cheese cheddarCheese, Bread bread) {
this.cheese = cheddarCheese;
this.bread = bread;
}
}
그래서 생성자 매개변수명을 cheddarCheese 로 바꿔주면 자동으로 CheddarCheese Bean이 주입되고 생성에 성공하게 된다.
하지만 이런 식으로 매개변수명을 바꿔버리면 수동으로 CheddarCheese 를 넣어줘야 하는 경우에는 헷갈린다.
자동으로 주입해주려는 Bean을 바꿀 때도 귀찮아진다. 그렇다면 어떻게 해야 할까?
@Qualifier 또는 @Primary 를 이용하는 방법이 있다.
🟢 @Qualifier
@Component
@Qualifier("defaultCheese")
public class MozzarellaCheese implements Cheese {
}
@Component
public class Pizza {
private Cheese cheese;
private Bread bread;
public Pizza(@Qualifier("defaultCheese") Cheese cheese, Bread bread) {
this.cheese = cheese;
this.bread = bread;
}
}
위 코드처럼 @Qualifier 어노테이션 안에 해당 Bean의 구분자를 지정해줄 수 있다.
CheddarCheese에@Qualifier 어노테이션을 붙여서 defaultCheese 라고 수정해주고 의존성을 주입받을 부분에 같은 어노테이션을 작성하면 CheddarCheese 가 주입된다.
🟢 @Primary
@Component
@Primary
public class MozzarellaCheese implements Cheese {
}
@Component
public class Pizza {
private Cheese cheese;
private Bread bread;
public Pizza(Cheese cheese, Bread bread) {
this.cheese = cheese;
this.bread = bread;
}
}
@Primary 어노테이션이 붙은 Bean은 해당 타입으로 의존성 검색을 할 때 우선적으로 주입된다.
일종의 기본 Bean이 되는 것이다.
🟢 의존성 주입 기준
위 의존성 주입에 대한 우선순위는 다음과 같다.
타입 → @Qualifier → @Primary → 변수명
🟢 Lombok
Lombok이라는 Java 라이브러리가 있다.
반복적인 코드를 줄이는 데 도움을 주는 어노테이션 기반의 도구를 제공해준다.
getter, setter, equals, hashCode 및 toString 메서드 등을 Lombok의 어노테이션을 사용하여 해당 코드를 자동으로 생성할 수 있다.
위에서 말했던 생성자 또한 그러하다.
아래는 Lombok이 제공해주는 생성자이다.
DI 방법 중, 생성자 주입을 자동으로 설정해준다.
- @NoArgsConstructor : 파라미터가 없는 기본 생성자를 생성
- @RequiredArgsConstructor : final 또는 @NonNull로 표시된 필드만을 파라미터로 하는 생성자를 생성
- @AllArgsConstructor : 모든 필드 값을 파라미터로 받는 생성자를 생성
이는 아래와 같이 쓰일 수 있다.
@Component
public class Pizza {
private Cheese cheese;
private Bread bread;
public Pizza(Cheese cheese, Bread bread) {
this.cheese = cheese;
this.bread = bread;
}
}
원래 이런 방식으로 @Autowired 를 이용하여 DI를 진행하던 코드였지만,
Lombok을 이용한다면 아래와 같이 바꿀 수 있다.
@RequiredArgsConstructor
@Component
@RequiredArgsConstructor
public class Pizza {
private final Cheese cheese;
private final Bread bread;
}
@AllArgsConstructor
@Component
@AllArgsConstructor
public class Pizza {
private Cheese cheese;
private Bread bread;
}
'Backend' 카테고리의 다른 글
<Spring> Webflux + Coroutine + MDC (0) | 2024.05.10 |
---|---|
<Spring> WARN 레벨 이상 로그 Slack 알림 보내기 (0) | 2024.03.17 |
<Spring> Spring Bean, IoC/DI 정리 1 (0) | 2023.10.03 |
<Spring> Redis 기반 검색어 자동 완성 기능 구현하기 (1) | 2023.10.01 |
<Spring> AOP 기반 분산락 (0) | 2023.10.01 |