스프링 IoC/DI 찍먹

2023. 2. 24. 12:42BackEnd(Java)/Spring Boot

✅ 아래 내용들에 대해서 알아보자

- IoC
- IoC 컨테이너

- DI, DI 방식

 

IoC

 IoC(Inversion of Control, 제어의 역전)란 프로그램의 일부에 대한 제어를 프레임워크로 이전하는 소프트웨어 엔지니어링 원칙이다.

스프링에서는 IOC는 클래스 간의 느슨한 결합을 하기 위해 객체의 생성과 관리를 컨테이너를 통해 프레임워크가 제어하는 것을 의미한다.

스프링 컨테이너가 제어권을 가지고 있다고 해서 스프링 컨테이너를 IoC 컨테이너라고 부른다.

 

이전 개발방식에서는 개발자가 직접 객체를 생성하고 의존성을 관리해야 했으나, 스프링에서는 객체의 생성과 의존성 관리를 IoC 컨테이너가 담당함으로써 객체 간의 결합도를 낮추고, 유연하고 확장성 높은 애플리케이션을 개발할 수 있다.

 

IOC/DI/DIP/IOC Container 그림

 

IoC 컨테이너

 스프링에서 IoC를 담당하는 컨테이너를 Bean Factory, DI Container, Application Context라고 부른다.

Bean Factory는 스프링의 기본 IoC 컨테이너로서, 필요한 Bean을 호출할 때마다 생성하고 반환한다.

Application Context는 BeanFactory의 확장된 형태로, IOC/DI 그 이상의 기능을 가진 인터페이스이다.

실제 코드를 같이 보자

 

Bean Factory, ApplicationContext 관계

 

Bean Facotry

BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스이다.
  • 스프링 빈을 관리/조회하는 역할을 담당한다.
  • 대표적으로 getBean() 메서드를 제공한다.

 

 

Application Context

 

ApplicationContext

  • ApplicationContext는 빈 팩토리 기능을 모두 상속받아서 제공한다(extends ListableBeanFactory 내부 참고)
  • 코드를 보면 여러 가지 인터페이스를 extends 하는데 각 인터페이스들의 역할들(메시지 국제화, 이벤트, 리소스 조회 등) 범위가 방대해서 이글에서는 다루지 않겠다.
  • 빈에 대한 설정 메타 정보(빈을 어떻게 만들고 등록할지 관리하는 정보) 등록 가능(XMl, Java, Groovy 등...)

 

어노테이션 기반 자바 코드 설정 예시를 설명하겠다(나머지 설정들은 다른 글 참고 부탁드립니다)

 

@Configuration : 1개 이상의 빈을 제공하는 클래스의 경우 반드시 명시해야 하는 어노테이션
@Bean : 클래스를 IoC 컨테이너의 빈으로 등록할 때 사용한다

 

빈을 설정하는 코드를 짜다가 문득 궁금증이 생겼다.  @Bean, @Configuration, @Component 차이점은 뭘까? 언제 써야 할까? 클래스에는 @Bean이 써질까? 등이 궁금해서 자료를 찾게 되었다.(향로님 참고)

 

결론부터 이야기하자면 @Bean은 개발자가 컨트롤이 불가능한 외부 라이브러리들을 등록하고 싶은 경우에 사용한다.

반대로 @Component는 개발자가 직접 컨트롤이 가능한 Class들 경우에 사용한다.

 

궁금증이 생겨 @Bean과 @Component @Configuration을 내부 코드를 까보기 시작했다.

 

@Bean

 @Bean 어노테이션 코드를 보면 @Target이 Method, ANNOTATION_TYPE이고 @Retention은 RunTime인 것을 확인할 수 있다.

(메타 어노테이션에 대해 모르시면 해당 글 참조)

 

@Bean은 클래스의 메서드에 지정이 되며 런타임시에 ComponentScan을 통해 객체로 등록되는 것을 알 수 있다.

 

따라서 @Bean을 클래스 선언하려고 하면 에러가 발생하게 된다.

 

 

 

@Component

Component 어노테이션은 @Target = ElementType.TYPE, @Retention = RUNTIME 이란 것을 알 수 있다.

따라서 해당 어노테이션은 클래스의 어떤 요소에든 적용 가능하고, 런타임시에 ComponentScan을 통해 객체로 등록된다.

 

 

@Configuration

 Configuration 어노테이션을 보면 @Target = ElementType.TYPE, @Retention = RUNTIME 이란 것을 알 수 있다.

따라서 해당 어노테이션은 클래스의 어떤 요소에든 적용 가능하고, 런타임시에 ComponentScan을 통해 객체로 등록된다.

 

내부를 보면 @Component 어노테이션이 선언된 것을 확인할 수 있다

 

 

그렇다면 어떻게 메서드 레벨에 존재하는 @Bean을 Spring이 빈으로 등록 시킬까?  @Configuration을 이용하는 것이다!

@Configuration은 메소드 레벨을 등록하는 @Bean을  Spring에게 해당 class는 bean을 등록하고 있다는 것을 알리는 역할을 하며 내부적으로 @Component가 있기 때문에 스프링 IoC 컨테이너의 빈으로 등록된다!

 

 

DI

 스프링에서는 IOC는 DI(Dependency Injection, 의존성 주입) 방식으로 구현된다. DI는 객체 생성 시점에 IoC 컨테이너가 해당 객체(Bean)를 주입하는 방식을 의미한다. 이로 인해 개발자는 객체 간의 의존성을 직접 관리하지 않아도 되며, 코드 재사용성과 유지보수성이 높아짐

 

즉, 기존에 내부에서 관리하던 방식을 외부에서 관리를 하고, 외부에서 관리하는 것들 중에 필요한 것들을 넣어 주는 방식

 

*스프링 IoC 컨테이너는 Bean으로 등록되지 않은 객체에는 DI를 해주지 않는다!!

 

IoC 컨테이너의 DI

 

DI 구현 방식

Sring에서는 어떻게 DI를 해줄까? 기본적으로는 타입 매칭을 통해 빈을 주입해 주고 만약 동일한 타입을 반환하는 빈이 여러 개가 있다면, 빈 이름으로 매칭한다. 

 

동일한 Bean이 여러 개 있을 떄, @Qualifier를 통해 명시적으로 지정할 수 있다.

@Autowired
@Qualifier("specificBeanName")
private MyService myService;

 

 

 

di 구현 방식에는 3가지 방식이 있다. 각각 알아보자

 

1.  필드 주입

변수 선언부에 @AutoWired 어노테이션을 사용한다.

 

장점

  • @AutoWired 어노테이션 사용하면 자동으로 DI가 된다

단점

  • 의존성이 숨는다 -> 생성자 주입에 비해 의존 관계를 한눈에 파악하기 어렵다
  • DI 컨테이너와의 결합도가 커지고, 테스트하기 어렵다
  • 불변성을 보장할 수 없다
  • 순환 참조가 발생할 수 있다

 

 

2.  수정자 주입

 Setter를 사용한 주입 방식이다.

 

장점

  • 선택적인 의존성 사용 가능

단점

  • 의존성이 숨는다 -> 생성자 주입에 비해 의존 관계를 한눈에 파악하기 어렵다
  • 순환 참조 문제가 발생할 수 있다

 

 

3.  생성자 주입

 생성자에 @Autowired 어노테이션을 사용하여 DI를 받으며, 스프링에서 권장하는 주입방식이다.

 

장점

  • final 키워드를 사용으로 불변성을 보장할 수 있다
  • 의존 관계를 모두 주입해야만 객체 생성이 가능하므로 NPE를 방지할 수 있다.
  • 순환 참조를 컴파일 단계에서 찾아낼 수 있다.

 

순환 참조

 순환 참조란 서로 다른 빈들이 서로를 의존하여 참조하고 있는 현상이다. 순환 참조 현상이 나타나게 되면 서로가 서로를 호출하는 무한 루프가 발생하게 되어 StackOverFlow가 발생하고 죽게 된다.

 

필드 주입이나 세터 주입에서는 객체 생성 후 순환 참조가 일어나기 때문에 컴파일 단계에서 순환 참조를 잡아낼 수 없게 된다

 

 

MemberService.func() -> ItemService.func() -> MemberService.func() -> ItemService.func() -> 무한반복

public class MemberServiceImpl implements MemberService{

    private final ItemService itemService;

    @Override
    public void func() {
        itemService.func();

    }
}


public class ItemServiceImpl implements ItemService{

    private final MemberService memberService;

    @Override
    public void func() {
        memberService.func();

    }
}

 

 

생성자 주입에서는 아래와 같이 컴파일 단계에서 순환 참조를 잡아낼 수 있는 장점이 있다!

컴파일 단계에서 순환 참조 발견

 

정리

 정리하자면, IoC란 제어의 역전을 말하며 스프링에서는 IoC를  IoC 컨테이너를 통해 DI 방식으로 제공하고 있다.

 IOC/DI를 사용으로 객체 간의 결합도를 낮추고 유연하고 확장성 높은 애플리케이션을 구성할 수 있는 것이다!

 


참고자료

반응형