🌎 Web/Spring

[Spring] IoC(제어의 역전)와 DI(의존성 주입) 그리고 Spring

오늘 ONEUL 2023. 2. 23. 05:18

IoC와 DI 그리고 Spring

그동안 Spring을 사용하면서 너무도 당연하게 생각했던 개념들에 대해 다시 짚어보려 한다.
IoC가 뭔지? DI는 또 뭔지? Spring은 무슨 관련이 있는 거고, 어떤 점이 좋아서 Spring을 사용하는지?
궁금증을 파헤쳐보자!

 

 

IoC(Inversion of Control) : 제어의 역전

IoC(Inversion of Control)란?
코드 흐름이 제3자에게 위임되는 것

잘 와닿지 않을 수 있으니, 예제를 통해 살펴보자.

public class A {
    private B b;

    public A() {
        b = new B();
    }
}

A 클래스에서 B 필드를 가지고 있고, 생성자 내부에서 직접 생성해 필드를 초기화하고 있다.
즉, 객체 생명주기나 메서드의 호출을 개발자가 직접 ‘제어’ 하고 있다.
이러한 흐름을 직접 제어하는 것이 아니라 외부에서 관리한다면?

public class A {
    
    @Autowired
    private B b; // 필드 주입방식
}

B라는 객체가 Spring Container에게 관리되고 있는 Bean이라면 @Autowired를 통해 객체를 주입 받을 수 있다.
개발자가 직접 객체를 관리하지 않고, Spring Container에서 객체를 생성하여 해당 객체에 주입시켜 준 것이다.
이것이 바로 제어의 ‘역전’이다. 프로그램의 제어권이 역전 된 것이다.

IoC의 또 다른 예제는 템플릿 메서드 패턴이다.

템플릿 메서드 패턴(Template Method Pattern)이란?
상위 클래스의 견본 메서드에서 하위 클래스가 오버라이딩한 메서드를 호출하는 패턴

템플릿 메서드 패턴을 적용하게 되면 상위의 추상 클래스에 흐름을 제어하는 메서드를 정의해 두고,
해당 템플릿 메서드에서 사용할 하위 메서드들은 변경이 필요 없으면 공통 메서드로, 변경이 필요하면 추상 메서드로 정의해 둔다.
그 뒤 해당 클래스를 상속받는 클래스에서 추상 메서드를 구현하여 사용하게 된다.
이렇게 될 경우 하위 클래스에서 메서드의 실제 구현을 하기는 하지만,
해당 메서드의 호출 제어권은 상위의 추상 클래스에 있게 되어 제어의 역전이 일어나는 것이다.

 

 

IoC는 왜 필요할까?

  • 역할과 관심을 분리해 응집도를 높이고 결합도를 낮추며, 이에 따라 변경에 유연한 코드를 작성할 수 있다.
  • 프로그램의 진행 흐름과 구체적인 구현을 분리시킬 수 있다.
  • 개발자는 비즈니스 로직에 집중할 수 있다.
  • 객체 간 의존성이 낮아진다.

 

IoC를 구현하는 Pattern

  • Service Locator
  • Factory
  • Abstract Factory
  • Strategy
  • Template Method
  • Dependency Injection

IoC를 구현하는 Pattern 에는 여러 가지가 있는데, 놀랍게도 우리는 벌써 2가지를 살펴보았다.
오늘의 핵심 내용인 DI에 대해 더 자세히 알아보도록 하자.

 

 

DI(Dependency Injection) : 의존성 주입

DI(Dependency Injection)이란?
IoC를 구현하기 위해 사용하는 디자인 패턴 중 하나로, 이름 그대로 객체의 의존관계를 외부에서 주입시키는 패턴
(여기서 외부란, 객체 기준의 외부를 의미한다.)

토비의 스프링에서 말하는 DI(의존관계 주입)의 3가지 조건은 다음과 같다.

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야 한다.
  • 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
  • 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.

 

위에서 사용했던 예제를 다시 보도록 하자.

public class A {
    private B b;

    public A() {
        b = new B();
    }
}

생성자 내부에서 직접 생성해 필드를 초기화하고 있는 이 예시에는 다음과 같은 문제점이 있다.

  • 강한 결합
    • 만약 B 생성자에 변경이 생긴다면? 모든 A 클래스가 영향을 받는다. 즉, 유연성이 떨어진다.
  • 객체들 간의 관계가 아닌 클래스 간의 관계
    • 객체들 간에 관계가 맺어졌다면 다른 객체의 구체 클래스를 전혀 알지 못하더라도, (해당 클래스가 인터페이스를 구현했다면) 인터페이스의 타입으로 사용할 수 있다.

 

DI를 적용하지 않은 예시

public class Barista {
    private AmericanoRecipe americanoRecipe;

    public Barista() {
        this.americanoRecipe = new AmericanoRecipe();
    }
}

바리스타는 아메리카노 레시피에 의존하기 때문에 아메리카노 레시피 클래스에 변경이 생긴다면 바리스타 클래스 또한 변경되어야 한다.

DI를 통해 이 문제를 해결해 보자.

 

생성자 주입

public class Barista {
    private AmericanoRecipe americanoRecipe;

    public Barista(AmericanoRecipe americanoRecipe) {
        this.americanoRecipe = americanoRecipe;
    }
}

필요한 의존성을 모두 포함하는 생성자를 만들고, 그 생성자를 통해 의존성을 주입한다.

 

setter 주입

public class Barista {
    private AmericanoRecipe americanoRecipe;

    public void setAmericanoRecipe(AmericanoRecipe americanoRecipe) {
        this.americanoRecipe = americanoRecipe;
    }
}

의존성을 입력받는 setter 메서드를 만들고, 메서드를 호출해서 의존성을 주입한다.

 

Interface 주입

public class Barista implements RecipeInjection{
    private AmericanoRecipe americanoRecipe;

    @Override
    public void injection(AmericanoRecipe americanoRecipe) {
        this.americanoRecipe = americanoRecipe;
    }
}

interface RecipeInjection {
    void injection(AmericanoRecipe americanoRecipe);
}

의존성을 주입하는 메서드를 포함하는 인터페이스를 작성하고, 인터페이스를 구현하도록 함으로써 실행 시에 이를 통해 의존성을 주입한다.
setter 주입처럼 메서드를 외부에서 호출한다는 점은 비슷하지만, 의존성 주입 메서드를 빠뜨릴 수 있는 setter와 다르게 오버라이드를 통해 메서드 구현을 강제할 수 있다는 차이가 있다.

자, 여기서 문제가 하나 더 있다.
바리스타가 아메리카노만 만드는가?
아메리카노가 아닌 라떼를 만들려면?
결국 근본적으로 관심이 분리되지 않았다는 문제가 있다.
DIP에 따라 DI를 적용시켜 의존관계를 분리시켜 보자.

 

DIP(Dependency Inversion Principle) : 의존 역전 원칙

DIP(Dependency Inversion Principle)란?
- 상위 모듈은 하위 모듈에 의존해서는 안된다.
- 둘 다 추상화에 의존해야 한다.

 

Barista → AmericanoRecipe에 의존하는 방향이었다면,
추상화를 통해 이 방향 역전시킬 수 있다.

interface CoffeeRecipe {
}

class AmericanoRecipe implements CoffeeRecipe{
}
public class Barista{
    private CoffeeRecipe coffeeRecipe;

    public Barista(CoffeeRecipe coffeeRecipe) {
        this.coffeeRecipe = coffeeRecipe;
    }
}

Barista → CoffeeRecipe ← Americano Recipe, LatteRecipe

상위 계층인 Barista가 하위계층인 AmericanoRecipe에 의존하는 상황을 interface를 이용해 반전시켜 하위계층의 구현으로부터 독립시킨 것을 볼 수 있다.
기존의 생성자 주입 코드와 비교할 때 변화로부터 자유로워진 것을 알 수 있다.

 

 

IoC와 DI는 무엇이 다를까?

IoC는 객체의 흐름, 생명주기관리 등 독립적인 제 3자에게 역할과 책임을 위임하는 개념, 즉 원칙 중 하나이고,
DI는 IoC를 달성하는 디자인 패턴 중 하나로 좀 더 구체적인 행위라고 할 수 있다.

 

 

아직 Spring은 나오지도 않았다.
DI 없이도 IoC를 만족하는 프로그램을 만들 수 있고, DI는 IoC를 사용하지 않아도 된다.
물론 Spring을 사용하지 않고도 DI의 사용이 가능하다.
그렇지만 Spring을 사용하면? 더 간편하게 사용할 수 있다!

 

 

Spring IoC 컨테이너 사용하기

지금까지 제대로 글을 읽었다면 하나의 의문점이 들 것이다.
DI를 사용하기 위해서는 객체 생성이 우선 되어야 한다.
과연 어디서 객체 생성을 해야 할까?
바로 스프링프레임워크가 필요한 객체를 생성하여 관리하는 역할을 대신해 준다.

  • 빈 (Bean): 스프링이 관리하는 객체
  • 스프링 IoC 컨테이너: '빈'을 모아둔 통

 

Spring Bean 등록 방법

1. @Component

@Component
public class ProductService { ... }

이렇게 클래스 선언 위에 @Component 어노테이션을 붙여 설정하면 Spring 서버가 뜰 때 객체가 생성되고, Spring IoC에 Bean으로 저장된다.

// @Component 클래스에 대해서 스프링이 해 주는 일
// 1. ProductService 객체 생성
ProductService productService = new ProductService();

// 2. 스프링 IoC 컨테이너에 빈 (productService) 저장
// productService -> 스프링 IoC 컨테이너

@Controller, @Service, @Repository 어노테이션은 @Component 어노테이션을 포함하고 있다.
그럼 아무 클래스나 @Component 어노테이션만 붙이면 되는 걸까? 아니다. 적용 조건이 있다.

 

1-1. @Component 적용 조건

@Configuration
@ComponentScan(basePackages = "com.exapmle.demo")
class BeanConfig { ... }

@ComponentScan에 설정해 준 packages 위치와 하위 packages들에만 적용된다.
어라? 난 이런 걸 적용해 준 적이 없는데?

@SpringBootApplication에 의해 default 설정이 되어 있다.

 

2. @Bean

직접 객체를 생성하여 빈으로 등록 요청을 할 수 도 있다.

package com.example.springtest.config;

import com.example.springtest.repository.ProductRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BeanConfiguration {
    @Bean
    public ProductRepository productRepository() {
        String dbUrl = "jdbc:h2:mem:db";
        String username = "sa";
        String password = "";
        return new ProductRepository(dbUrl, username, password);
    }
}

설정 클래스에 @Configuration 어노테이션을 설정하고, 해당 메서드에 @Bean 어노테이션을 붙여준다.
그럼 Spring 서버가 뜰 때 @Bean으로 설정된 함수를 호출하고, Spring IoC에 Bean으로 저장된다.

// 1. @Bean 설정된 함수 호출
ProductRepository productRepository = beanConfiguration.productRepository();

// 2. 스프링 IoC 컨테이너에 빈 (productRepository) 저장
// productRepository -> 스프링 IoC 컨테이너

 

Spring Bean 사용 방법

1. @Autowired

Spring에서는 DI를 구현하기 위해 @Autowired 어노테이션을 사용한다.

필드 주입

@Component
public class ProductService {
		
    @Autowired
    private ProductRepository productRepository;
		
		// ...
}

원래는 불가능한 주입을 프레임워크의 힘을 빌려서 주입해 주는 방법이다.
주입받고자 하는 필드 위에 @Autowired 어노테이션을 붙여주기만 하면 Spring에 의해 의존성이 주입된다.
외부에서 접근이 불가능하기 때문에 프레임워크에 의존성이 강하게 종속된다는 단점이 있다. 또한 객체 생성 이후에 주입하기 때문에 NPE(Null Pointer Exception) 등의 문제가 발생할 수 있다.

 

setter 주입

@Component
public class ProductService {

    private final ProductRepository productRepository;

    @Autowired
    public void setProductRepository(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }
		
		// ...
}

setter 메서드 위에 @Autowired 어노테이션을 붙이면 Spring이 setter를 사용해서 자동으로 의존성을 주입해 준다.
setter 주입은 빈 생성자, 또는 빈 정적 팩토리 메서드가 필요하다.
따라서 fianl 필드를 만들 수 없고, 의존성의 불변을 보장할 수 있다는 특징이 됐다.
주로 런타임에 의존성을 수정해주어야 하거나, 의존성을 선택적으로 주입할 때 사용한다.

 

생성자 주입(Spring 추천 방식)

@Component
public class ProductService {

    private final ProductRepository productRepository;

    @Autowired
    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }
		
		// ...
}

객체의 최초 생성 시점에 Spring이 의존성을 주입해 준다.

 

Spring은 왜 생성자 주입 방식을 추천하는 걸까?

생성자 주입된 컴포넌트들은 완전히 초기화된 상태로 클라이언트에 반환된다.
생성자 주입을 사용하게 되면 필드를 final로 만들어줄 수 있고, 의존성 주입이 생성자 호출 시 최초 1회만 이루어지는 걸 보장할 수 있다. 따라서 의존관계를 불변으로 만들어줄 수 있다.
또한 final이 가능하기 때문에 NPE(Null Pointer Exception)을 방지할 수 있다.
생성자 주입을 사용하면 순환참조 문제도 방지할 수 있었으나 Spring boot 2.6 버전부터 기본 설정으로 순환참조가 방지된다.

 

1-1. @Autowired 적용 조건

스프링 IoC컨테이너에 의해 관리되는 클래스에서만 가능하다.

 

1-2. @Autowired 생략 조건

Spring 4.3 버전부터 생성자 선언이 1개일 때만 생략 가능하다.

public class A {
	@Autowired // 생략 불가
	public A(B b) { ... }

	@Autowired // 생략 불가
	public A(B b, C c) { ... }
}

Lombok의 @RequiredArgsConstructor를 사용하면 다음과 같이 사용할 수 있다.

@RestController
@RequiredArgsConstructor // final로 선언된 멤버 변수를 자동으로 생성합니다.
public class ProductController {

    private final ProductService productService;
    
    // 생략 가능
		// @Autowired
		// public ProductController(ProductService productService) {
		//     this.productService = productService;
		// }
}

 

2. AppliciationContext

Spring Ioc 컨테이너에서 빈을 수동으로 가져올 수도 있다.

@Component
public class ProductService {

    private final ProductRepository productRepository;

    @Autowired
    public ProductService(ApplicationContext context) {
        // 1.'빈' 이름으로 가져오기
        ProductRepository productRepository = (ProductRepository) context.getBean("productRepository");
        // 2.'빈' 클래스 형식으로 가져오기
        // ProductRepository productRepository = context.getBean(ProductRepository.class);
        this.productRepository = productRepository;
    }

		// ...		
}

 

 

3줄 요약

  • IoC와 DIP는 원칙
  • DI는 IoC를 구현하기 위해 사용되는 디자인 패턴
  • DI를 자동으로 해줌으로써 프로그램의 제어권을 가져가는 역할을 해주는 것이 스프링

 

 

 

 

📚 참고자료

의존성주입과 의존관계역전원칙 - 큰돌의  터전

[10분 테코톡] 오찌, 야호의 DI와 IoC - 우아한테크

 

[Spring DI/IoC] IoC? DI? 그게 뭔데?

스프링을 공부하다 보면 꼭 나오는 이야기가 있다. 스프링은 IoC 컨테이너로 빈을 관리한다. 스프링은 DI를 사용한다 .DI 방법에는 생성자 주입, setter 주입, 필드 주입 등이 있다.... 아니 근데 대체

velog.io

 

biggwang의 개발 블로그

백문이불여일견

biggwang.github.io

 

DI는 IoC를 사용하지 않아도 된다

Dependency Injection(DI)을 얘기할 때 빠지지 않는 글이 있다. Martin Folwer의 Inversion of Control Containers and the Dependency Injection pattern이다. 이 글에서 DI란 용어가 생겨나게된 배경을 볼 수 있다. 관련한 내용

jwchung.github.io

 

의존관계 주입(Dependency Injection) 쉽게 이해하기

이번 글에서는 DI(의존성 주입, 의존관계 주입)의 개념을 설명한다.

tecoble.techcourse.co.kr

 

[Spring] 의존성 주입(Dependency Injection, DI)이란? 및 Spring이 의존성 주입을 지원하는 이유

1. 의존성 주입(Dependency Injection)의 개념과 필요성 [ 의존성 주입(Dependency Injection) 이란? ] Spring 프레임워크는 3가지 핵심 프로그래밍 모델을 지원하고 있는데, 그 중 하나가 의존성 주입(Dependency Inj

mangkyu.tistory.com

 

우리는 왜 스프링을 사용하는가? - Java spring의 특징

앞선 글에서 왜 우리가 프레임워크를 사용해야 하는 지 알아보았다. 짧게 짚고 넘어가자보자. 결국 개발자 입장에서는 검증된 코드를 이용해서 최소한의 부분만 바꾸어 빠르게 개발하기 위해서

joychae.tistory.com