본문 바로가기
공부 자료/Java Spring

[Java Spring] IoC, DI, Bean, 컨테이너

by 미노킴 2022. 12. 3.

*이 글의 내용은 제가 이해한 것을 바탕으로 작성되었습니다. 글의 내용 중 잘못된 내용이 있다면 댓글로 피드백 해주시면 감사하겠습니다.

0. 개요

항해에서 Spring을 공부하면서 IoC, DI, Bean, 컨테이너의 개념들을 간단하게 배웠다. 대략적인 흐름은 이해할 수 있었지만, 누군가에게 설명할 정도로 이해하진 못했다.

 

개인적으로 개념을 어설프게 알고 있는 건 아예 모르는 것 보다 위험하다 생각한다. 내가 그 개념을 알고 있다고 착각하고 그냥 넘겨버릴 수 있기 때문이다.

 

그래서 위 4가지 개념을 제대로 이해하고자 이렇게 글로 정리하게 되었다. 이 글에서는 IoC, DI, Bean, 스프링 컨테이너에 대한 '개념'만 다룰 예정이다. Bean의 자세한 사용 방법이나 스프링 컨테이너의 내부 구현 과정 등은 여기서 다루지 않는다.

 

1. 제어의 역전, IoC (Inversion of Control)

제어의 역전은 하나의 설계 원칙이다. (디자인 패턴이라고도 한다.)

 

예시를 먼저 들어보자.

 

PostsApiController 라는 클래스에서 PostsService라는 클래스를 사용한다고 생각해보자. 일반적인 자바 프로그래밍이라면 이 과정은 다음과 같이 이루어져야 한다.

 

public class PostsApiController {
	
    //PostsService라는 타입의 객체를 사용하기 위해 new PostsService()로 새로운 인스턴스를 만들었다.
    private final PostsService postsService = new PostsService();

    //위에서 new PostsService()로 만든 인스턴스를 참조하는 postsService 변수로 PostsService의 메소드를 사용한다.
    public List<PostsResponseDto> showPosts(){
        return postsService.findAll();
    }

 

new PostsService()로 직접 PostsService 인스턴스를 만들었고, = 연산자를 통해 새로 만든 인스턴스의 주소를 postsService 변수에 넣어주었다.

 

이때 이 과정이 PostsApiController 클래스 안에서 '직접' 이루어졌다. 이건 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다고 볼 수 있다.

 

그런데 제어의 역전을 사용하면 프로그램의 제어 흐름을 '외부'에 맡길 수 있다.

 

마찬가지로 PostsApiController 클래스에서 PostsService 클래스를 사용하되, 이번엔 Spring을 통해 제어 흐름을 '외부'에 맡긴 코드를 확인해 보자.

 

= new PostsService(); 가 없다!

 

위 사진을 보면 postsService의 인스턴스를 생성하지도 않고, postsService 변수에 인스턴스 주소를 연결해 주지도 않았다. 그런데 'postsService.findAll()' 에선 postService가 아무렇지도 않게 'PostsService'의 메소드를 쓰고 있다!!

 

이것은 Spring, 즉 '외부'에서 PostsService의 인스턴스를 만들어주고 그 주소를 postsService에 직접 연결을 해주었기 때문에 가능한 일이다. (구체적으로 어떤 과정으로 만들고 연결하는지는 글 후반부의 Bean과 Spring 컨테이너 부분에서 다룬다.)

 

이처럼 객체의 생성, 연결과 같은 프로그램의 제어 흐름을 직접 제어하는 것이 아니라, 외부에서 관리해 주는 것을 제어의 역전(IoC)이라 한다.

 

제어의 역전은 주로 프레임워크를 통해 이루어진다. 우리가 사용하는 모든 프레임워크에는 IoC 개념이 적용되어 있다.

 

프레임워크 vs 라이브러리

프레임워크와 라이브러리의 핵심적인 차이가 '제어의 역전' 이다. '토비의 스프링' 책에서 이 둘의 차이를 좀 더 자세하게 설명해 두었다.

 

프레임워크는 단지 미리 만들어준 반제품이나, 확장해서 사용할 수 있도록 준비된 추상 라이브러리의 집합이 아니다. 프레임워크가 어떤 것인지 이해하려면 라이브러리와 프레임워크가 어떻게 다른지 알아야 한다.

라이브러리를 사용하는 애플리케이션 코드는 애플리케이션 흐름을 직접 제어한다.
단지 동작하는 중에 필요한 기능이 있을 때 능동적으로 라이브러리를 사용할 뿐이다.

반면에 프레임워크는 거꾸로 애플리케이션 코드가 프레임워크에 의해 사용된다.
프레임워크에는 분명한 [제어의 역전] 개념이 적용되어 있어야 한다.

애플리케이션 코드는 프레임워크가 짜 놓은 틀에서 수동적으로 동작해야 한다.

- 토비의 스프링中

 

2. 의존관계 주입, DI (Dependency Injection)

DI를 이해하려면 의존 관계가 무엇인지 먼저 이해해야 한다.

 

의존 대상 B가 변할 때, 그것이 A에 영향을 미치면 A는 B에 의존한다고 하고 이 관계를 의존 관계라고 한다.

 

예를 들어 다음과 같은 상황을 가정해보자.

피자 가게의 요리사는 피자 레시피에 의존한다. 만약 피자 레시피가 변경된다면, 요리사는 피자를 새로운 방법으로 만들게 된다. 레시피의 변화가 요리사에 미쳤기 때문에 요리사는 레시피에 의존한다라고 할 수 있다.

이를 코드로 나타내면 다음과 같다.

public class PizzaChef{
	
	private PizzaRecipe pizzaRecipe;
	
	public PizzaChef() {
		this.pizzaRecipe = new PizzaRecipe();
	}
	
}

 

여기서 PizzaChef는 PizzaRecipe에 의존한다. 

 

한편, 의존 관계는 '정적인 클래스 의존 관계'와 애플리케이션 실행 시점에 결정되는 '동적인 객체(인스턴스) 의존 관계' 를 구분해서 생각해야 한다.

 

정적인 클래스 의존 관계

애플리케이션을 실행하기 전에 확인할 수 있는 의존 관계이다. 클래스가 사용하는 import 코드만 보고 의존 관계를 쉽게 판단할 수 있다.

 

예시를 보자.

출처: 스프링 핵심 원리 - 기본편 (김영한님의 인프런 강의)

위 코드에서 import를 보면 OrderServiceImpl은 MemberRepository, DiscountPolicy, Member에 의존하고 있음을 알 수 있다. 위의 코드를 클래스 다이어그램으로 보면 아래와 같다.

출처: 스프링 핵심 원리 - 기본편 (김영한님의 인프런 강의)

(위 사진의 출처이자 이 글에서 참고하고 있는 김영한님의 강의에선 검은 실선을 의존 관계로, 점선을 상속 관계로 말하고 있다.)

 

위의 클래스 다이어그램을 보면 OrderServiceImplOrderService를 상속받고 있고, MemberRepositoryDiscountPolicy에 의존하고 있다.

 

DiscountPolicy와 MemberRepository에는 각각 자신을 상속 받고 있는 어떠한 구현체든 올 수 있다. 그런데 이러한 클래스 의존관계 만으로는 실제로 어떤 객체가 OrderServiceImpl에 주입될 지 알 수가 없다!!

 

동적인 객체 인스턴스 의존 관계 + DI (의존 관계 주입)

애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계다.

 

앞에 나왔던 클래스 다이어 그램을 객체 다이어그램으로 표현해보자.

출처: 스프링 핵심 원리 - 기본편 (김영한님의 인프런 강의)

위 객체 다이어그램을 앞에 나왔던 클래스 다이어그램을 통해 설명해보자. 주문 서비스 구현체는 OrderServiceImpl , 메모리 회원 저장소는 MemoryMemberRepository, 정액 할인 정책은 RateDiscountPolicy 타입의 객체이다.

 

의존 관계 주입(DI)은 애플리케이션 '실행 시점(런타임)'에 '외부'에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존 관계가 연결 되는 것을 의미한다.

 

위 객체 다이어그램에서는 '메모리 회원 저장소' 와 '정액 할인 정책'을 외부에서 생성하고 그 참조값을 클라이언트를 통해 전달하여 주문 서비스 구현체와 다른 객체들을 연결한 것이다.

 

Spring 프레임워크를 사용하면 애플리케이션 실행 시 이 DI가 일어난다.이때 이 DI 작업에서 객체를 생성하고 관리하면서 연결해 주는 클래스를 'IoC 컨테이너' 또는 'DI 컨테이너' 라고 한다.

 

IoC 컨테이너, DI 컨테이너 말고도 어셈블러, 오브젝트 팩토리 등으로도 불린다. 다만 최근에는 의존 관계 주입에 초점을 맞추어 주로 DI 컨테이너라고 한다.

 

3. 스프링 컨테이너와 Bean

IoC와 DI에서 공통적으로 계속 나왔던 얘기가 있었다. 바로 '외부'에서 객체를 생성하고 관리해준다는 이야기이다.

 

Spring에서 이 '외부'에 해당하는 곳이 스프링 컨테이너이고, 이때 컨테이너 안에서 관리하는 객체들을 Bean이라고 한다.

 

스프링 컨테이너는 다음과 같은 구조로 이루어져 있다.

 

출처: 스프링 핵심 원리 - 기본편 (김영한님의 인프런 강의)

정확히 스프링 컨테이너를 부를 때 'BeanFactory', 'ApplicationContext' 로 구분해서 이야기하지만, 보통 BeanFactory를 직접 사용하는 경우는 거의 없으므로 일반적으로 'ApplicationContext'를 스프링 컨테이너라 한다.

 

스프링 컨테이너는 XML을 기반으로 만들 수도 있고, 어노테이션을 기반으로 만들 수도 있다. XML과 어노테이션이 무엇인지는 아래의 링크에서 확인할 수 있다.

https://kimdirector1090.tistory.com/manage/posts/

 

TISTORY

나를 표현하는 블로그를 만들어보세요.

www.tistory.com

(스프링 부트가 나온 후엔 대부분의 작업을 XML 대신 어노테이션을 이용하여 진행한다고 한다. 어노테이션이 상대적으로 장점이 많으니 특별한 경우가 아닌 이상 XML 대신 어노테이션을 사용하자.)

 

어노테이션 기반의 스프링 컨테이너가 생성되는 과정은 다음과 같다.

 

1. 스프링 컨테이너 생성

//스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

출처: 스프링 핵심 원리 - 기본편 (김영한님의 인프런 강의)

스프링 컨테이너를 생성할 때는 구성 정보를 지정해 주어야 한다. 위 코드에선 매개변수로 들어간 AppConfig.class를 구성 정보로 지정했다.

 

2. 스프링 Bean 등록

스프링 컨테이너는 구성 정보를 토대로 스프링 빈 저장소에 Bean을 등록한다.

 

이때 저장되는 객체 인스턴스인 Bean은 싱글톤(인스턴스를 1개만 생성)으로 관리된다.

 

스프링 컨테이너는 싱클톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리할 수 있기 때문에 기존 싱글톤 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.

 

Bean은 싱글톤으로 관리되기 때문에 같은 이름의 Bean을 호출하면 동일한 참조값이 나오게 된다. 

 

Bean의 이름은 구성 정보인 AppConfig.class의 메소드 이름을 사용한다. 원한다면 빈 이름을 직접 부여할 수도 있다.

 

단, 빈 이름은 항상 다른 이름을 부여해야 한다. 같은 이름을 부여하면 다른 빈이 무시되거나, 기존 빈을 덮어버리는 등의 오류가 발생한다.

 

출처: 스프링 핵심 원리 - 기본편 (김영한님의 인프런 강의)

스프링 컨테이너는 구성 정보를 토대로 Bean을 등록한다고 했다. 그런데 @Component 와 @ComponentScan을 이용하면 설정 정보를 따로 제공해주지 않아도 된다!!

 

메인 메소드 위에 @ComponentScan이 있으면 @Component가 있는 모든 클래스를 Bean으로 자동 등록해준다. 우리가 주로 사용하는 @Service, @Controller안에도 @Component가 포함되어 있으며, 스프링 부트를 사용할 때 붙는 어노테이션인 @SpringBootApplication 안에 @ComponentScan도 들어있다.

 

3. 스프링 Bean 의존 관계 설정

출처: 스프링 핵심 원리 - 기본편 (김영한님의 인프런 강의)
출처: 스프링 핵심 원리 - 기본편 (김영한님의 인프런 강의)

스프링 컨테이너는 설정 정보를 참고해서 의존 관계를 주입(DI) 한다.

 

그런데 @ComponentScan을 사용하여 설정 정보가 없는 경우는 어떻게 의존 관계를 주입할까??

 

원래 스프링은 Bean을 생성하고, 의존 관계를 주입하는 단계가 나뉘어져 있다. 그런데 이렇게 자바 코드로 스프링 Bean을 등록하면 생성자를 호출하면서 의존 관계 주입도 한번에 처리된다

 

이때 사용되는 것이 @Autowired이다. 필드나 생성자 위에 @Autowired를 붙이면 Bean을 생성할 때 해당 내용을 토대로 의존 관계를 주입한다.

출처: 스프링 핵심 원리 - 기본편 (김영한님의 인프런 강의)

단, 필드 위에 @Autowired를 붙이는 건 스프링 측에서 지양하라고 하고 있다. 순환 참조 문제가 발생할 수 있고, 해당 필드에 final을 넣어줄 수 없기 때문이다. 그래서 @Autowired는 생성자 위에만 붙이는 것이 좋다.

 

Bean을 생성할 때 객체의 생성자를 호출해야 하므로 @Autowired가 붙은 생성자가 있다면 Bean 등록과 동시에 의존 관계도 주입이 된다.

 

참고로 Lombok의 @RequiredArgsConstructor는 final 이 붙은 필드들을 매개변수로 하는 생성자를 만들고 그 위에 @Autowired를 자동으로 붙여주어, 주로 생성자를 직접 입력하기 보단 @RequiredArgsConstructor를 주로 사용한다.

 

레퍼런스