공부해봅시당

[쉽게 배우자! Spring Triangle 1] IoC(Inversion of Control, 제어의 역전)을 쉽게 이해해보자 (feat. Dependency Injection, 의존성 주입) 본문

STUDY/Spring

[쉽게 배우자! Spring Triangle 1] IoC(Inversion of Control, 제어의 역전)을 쉽게 이해해보자 (feat. Dependency Injection, 의존성 주입)

tngus 2024. 2. 23. 00:42

Spring은 Spring Triangle이라고 부르는 핵심 3대요소를 제공해준다

이는 각각 IoC, AOP, PSA를 일컫는다

 

 

오늘은 IoC에 대해 알아보자

 


 

아래 그림은 IoC를 이해하기 쉽게 정리한 이미지이다

포스팅을 끝까지 읽으면 아래 이미지가 이해될 것이다

 


IoC(Inversion of Control, 제어의 역전)을 [파티 주최하기]라는 비유로 이해해보자

파티를 내가 혼자 다 준비하는 경우: 제어의 역전이 없는 경우

내가 친구들을 초대해서 파티를 열기로 했다고 하자

그럼 파티 주최자인 내가 음식을 준비하고, 음악을 틀고, 장소를 꾸미고, 모든 것을 직접 관리해야 한다

이런 경우에 나는 파티의 모든 것을 제어하고 있다

많은 일이고, 너무 많아서 혼자서 다 하기 벅찰 수 있다

 

파티 플래너를 고용하는 경우: 제어의 역전이 있는 경우

지난번에 혼자서 파티 준비하기가 너무 힘들었던 나는 파티 플래너를 고용했다

이번에는 파티 플래너에게 모든 준비를 맡겼다

파티 플래너는 이전에 내가 관리했던 음식, 음악, 장식 등 모든 것을 준비하고 관리한다

이 경우, 나는 사소한 것들이 아니라 파티 플래너에게 미리 이야기할 파티의 주제 등 큰 그림에만 신경 쓰면 된다

 

정리하면

제어의 역전이라는 단어 자체가 익숙하지 않기 때문에 이해하기 힘들 수 있다

하지만 위 예시로 인해 이해가 쉬울 것이라 생각한다

 

파티를 내가 준비하는 것은 나에게 모든 제어권이 있는 상황이고,

파티 플래너를 고용하는 경우는 대부분의 제어권을 파티 플래너에게 양도했기 때문에 제어권이 파티 플래너에게 있는 상황이다

 

파티의 세부적인 준비와 관리를 하는 주체의 '제어'가 나에서 파티 플래너로 '역전'된 것

 

IoC는?

소프트웨어에서 IoC가 하는 일도 비슷하다

프로그래머가 모든 세부 사항에 신경 쓰는 대신, 프로그램의 일부 기능을 외부 라이브러리나 프레임워크에 맡긴다

 

이렇게 하면 프로그래머는 프로그램의 중요한 부분,

'무엇을' 할 것인가(=파티 주제 등)에 집중할 수 있고,

'어떻게' 할 것인가(파티 음식, 음악, 장식 등)는 프레임워크가 처리하게 된다

 

결과적으로, 개발 과정이 더 간단해지고, 유지보수도 더 쉬워진다

이런 방식으로, IoC는 개발자가 보다 중요한 결정에 집중할 수 있게 해주고, 반복적이거나 일반적인 작업은 프레임워크가 처리하도록 한다

 

마치 파티 준비를 전문가에게 맡기고, 즐기기만 하면 되는 것처럼!

 

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

IoC는 소프트웨어 개발에서 사용되는 설계 원칙 중 하나로, 프로그램의 제어 흐름을 사용자 코드가 아닌 외부 시스템(예: 프레임워크)에 위임하는 것을 말합니다. 전통적인 프로그래밍에서는 사용자 코드가 프로그램의 흐름을 제어하고 모든 결정을 내렸습니다. 그러나 IoC에서는 이러한 제어 권한이 프레임워크 같은 외부 시스템으로 이동합니다. 이를 통해 개발자는 비즈니스 로직에 더 집중할 수 있으며, 코드의 유연성과 재사용성이 향상됩니다.

 

 

Spring IoC는?

Spring 프레임워크에서 IoC는 주로 의존성 주입(Dependency Injection)을 통해 구현된다

따라서 의존성 주입에 대해 먼저 알아보자

 


DI(Dependency Injection, 의존성 주입)을 [파티 주최하기]라는 비유로 이해해보자

DI는 IoC와 밀접한 관계가 있다

따라서 위 IoC에서 살폈던 상황과 동일하게 가정하고 살펴보자

파티를 내가 혼자 다 준비하는 경우: 의존성 주입이 없는 경우

내가 친구들을 초대해서 파티를 열기로 했다고 하자

그럼 파티 주최자인 내가 음식을 준비하고, 음악을 틀고, 장소를 꾸미고, 모든 것을 직접 관리해야 한다

모든 준비 과정에서 각각의 작업을 위해 필요한 모든 정보와 자원을 직접 찾아서 결정해야 한다

음식을 준비한다면, 무엇을 만들지 결정하고, 재료를 사고, 요리를 해야 한다

이 과정은 매우 시간이 많이 걸리고 복잡할 수 있다

 

파티 플래너를 고용하는 경우: 의존성 주입이 있는 경우

지난번에 혼자서 파티 준비하기가 너무 힘들었던 나는 파티 플래너를 고용했다

이번에는 파티 플래너에게 모든 준비를 맡겼다

파티 플래너는 이전에 내가 관리했던 음식, 음악, 장식 등 모든 것을 준비하고 관리한다

이 경우, 나는 파티에 대한 전반적인 아이디어만 제공한다(예: 테마, 선호하는 음식 종류 등)

그 후, 모든 세부 사항은 파티 플래너가 처리한다

파티 플래너는 의존성 주입을 하는 '주입기'와 같은 역할을 한다

내가 필요로 하는 서비스(장소, 음식, 장식, 음악)를 외부에서 가져와서 준비해주기 때문이다

 

정리하면

제어의 역전에서도 이해한 것과 비슷하게 이해할 수 있다

 

파티를 내가 준비하는 것은 내가 모든 것을 준비해야 하기 때문에 준비 과정에서 어려울 수 있고,

파티 플래너를 고용하는 경우는 파티 플래너(주입기)가 모든 서비스를 제공하기 때문에 준비 과정이 훨씬 간단해진다

 

DI는?

프로그래밍에서의 의존성 주입도 제어의 역전과 비슷하다

 

프로그램이 필요로 하는 컴포넌트나 서비스를 직접 만들고 관리하는 대신,

외부 시스템(스프링 프레임워크와 같은 의존성 주입 컨테이너)이 필요한 것들을 제공해 준다

 

이렇게 함으로써 개발자는 더 중요한 작업에 집중할 수 있고, 코드의 유지보수와 확장성도 개선된다

 

DI (Dependency Injection, 의존성 주입)

DI는 IoC의 한 형태로, 객체가 자신의 의존성(즉, 다른 객체와의 관계)을 직접 생성하거나 관리하지 않고, 외부로부터 주입받는 방식입니다. 이 의존성 주입은 생성자 주입, 세터 주입 또는 인터페이스 주입 등 다양한 방식으로 이루어질 수 있습니다. DI를 통해 객체는 필요한 의존성을 외부로부터 받기 때문에, 객체 간의 결합도가 낮아지고, 코드의 유연성 및 테스트 용이성이 증가합니다.

의존성이란 한 객체가 다른 객체를 사용할 때 의존성이 있다고 합니다.

 

코드로 확인해보자

의존성이 있다는 것

Store 객체가 Pencil 객체를 사용하고 있는 경우에

Store 객체가 Pencil 객체에 의존성이 있다고 표현한다

public class Store {

    private Pencil pencil;

}

 

그리고 두 객체 간의 관계(의존성)를 맺어주는 것을 의존성 주입이라고 하며 생성자 주입, 필드 주입, 수정자(Setter) 주입 등 다양한 주입 방법이 있다

 

의존성이 있지만 의존성 주입을 하지 않은 경우의 문제점

연필이라는 상품과 1개의 연필을 판매하는 Store 클래스가 있다고 하자

public class Store {

    private Pencil pencil;

    public Store() {
        this.pencil = new Pencil();
    }

}

 

위 예시 코드의 문제점은 아래와 같다

 

1. 두 클래스가 강하게 결합되어 있음

두 클래스가 강하게 결합되어 있어서 만약 Store에서 Pencil이 아닌 Food와 같은 다른 상품을 판매하고자 한다면 Store 클래스의 생성자에 변경이 필요하다

즉, 유연성이 떨어진다

각각의 다른 상품들을 판매하기 위해 생성자만 다르고 나머지는 중복되는 Store 클래스들이 파생되는 것은 좋지 못하다

이에 대한 해결책으로 상속을 떠올릴 수 있지만, 상속은 제약이 많고 확장성이 떨어지므로 피하는 것이 좋다

2. 객체들 간의 관계가 아니라 클래스 간의 관계가 맺어짐

Store와 Pencil는 객체들 간의 관계가 아니라 클래스들 간의 관계가 맺어져 있다는 문제가 있다

올바른 객체지향적 설계라면 객체들 간에 관계가 맺어져야 한다

객체들 간에 관계가 맺어졌다면 다른 객체의 구체 클래스(Pencil인지 Food 인지 등)를 전혀 알지 못하더라도, (해당 클래스가 인터페이스를 구현했다면) 인터페이스의 타입(Product)으로 사용할 수 있다

 

위와 같은 문제점이 발생하는 근본적인 이유는 Store에서 불필요하게 어떤 제품을 판매할 지에 대한 관심이 분리되지 않았기 때문

의존성 주입을 통한 문제 해결

여러가지 해결방법이 있겠지만, 여기서는 interface를 통해 의존성을 주입하여 문제를 해결해보자

 

위와 같은 문제를 해결하기 위해서는 우선 다형성이 필요하다

 

Pencil, Food 등 여러 가지 제품을 하나로 표현하기 위해서는 Product 라는 Interface가 필요하다

그리고 Pencil에서 Product 인터페이스를 우선 구현해주도록 하자

public interface Product {

}

public class Pencil implements Product {

}

 

 

이제 우리는 Store와 Pencil이 강하게 결합되어 있는 부분을 제거해주어야 한다

이를 제거하기 위해서는 다음과 같이 외부에서 상품을 주입(Injection)받아야 한다

그래야 Store에서 구체 클래스에 의존하지 않게 된다

public class Store {

    private Product product;

    public Store(Product product) {
        this.product = product;
    }

}

 

이러한 이유로 우리는 Spring이라는 DI 컨테이너를 필요로 하는 것이다

Store에서 Product 객체를 주입하기 위해서는 애플리케이션 실행 시점에 필요한 객체(빈)를 생성해야 하며, 의존성이 있는 두 객체를 연결하기 위해 한 객체를 다른 객체로 주입시켜야 한다
예를 들어 다음과 같이 Pencil 이라는 객체를 만들고, 그 객체를 Store로 주입시켜주는 역할을 위해 DI 컨테이너가 필요한 것이다

public class BeanFactory {

    public void store() {
        // Bean의 생성
        Product pencil = new Pencil();
    
        // 의존성 주입
        Store store = new Store(pencil);
    }
    
}

 

 

이러한 부분은 스프링 프레임워크가 완벽하게 지원을 해준다

스프링은 특정 위치부터 클래스를 탐색하고, 객체를 만들며 객체들의 관계까지 설정해준다

이러한 이유로 스프링은 DI 컨테이너라고도 불린다.

 

의존성 주입(Dependency Injection) 정리 

한 객체가 어떤 객체(구체 클래스)에 의존할 것인지는 별도의 관심사이다

Spring은 의존성 주입을 도와주는 DI 컨테이너로써, 강하게 결합된 클래스들을 분리하고, 애플리케이션 실행 시점에 객체 간의 관계를 결해 줌으로써 결합도를 낮추고 유연성을 확보해준다

이러한 방법은 상속보다 훨씬 유연하다

단, 한 객체가 다른 객체를 주입받으려면 반드시 DI 컨테이너에 의해 관리되어야 한다는 것이다

  • 두 객체 간의 관계라는 관심사의 분리
  • 두 객체 간의 결합도를 낮춤
  • 객체의 유연성을 높임
  • 테스트 작성을 용이하게 함

 

하지만 의존 관계를 주입할 객체를 계속해서 생성하고 소멸한다면, 아무리 GC가 성능이 좋아졌다고 하더라도 부담이 된다

그래서 Spring에서는 Bean들을 기본적으로 싱글톤(Singleton)으로 관리한다

 


 

Spring 프레임워크에서의 IoC

위에서도 언급했다시피 Spring 프레임워크에서의 IoC(Inversion of Control)는 주로 의존성 주입(Dependency Injection)을 통해 구현된다

 

이는 객체가 자신의 의존성, 즉 다른 객체와의 관계나 협력을 직접 생성하거나 관리하지 않고, 외부(예: Spring 컨테이너)로부터 필요한 의존성을 주입받는 방식이다

이를 통해 코드는 더 유연하고, 테스트하기 쉬우며, 확장성이 높아진다

 

 

음악 플레이어 애플리케이션으로 IoC를 이해해보자

IoC를 DI로 구현해보자
DI 코드를 위해서 한 번 확인했기 때문에 아래 예시는 복습하는 작업이 될 것이다

 

이번에는 애플리케이션에서 MusicService 인터페이스가 있고, 이를 구현한 ClassicalMusicService 및 PopMusicService가 있다고 해보자

 

MusicPlayer 클래스는 MusicService를 사용해 음악을 재생한다

 

Spring을 사용하면, MusicPlayer가 특정 MusicService 구현체를 직접 생성하지 않고 Spring 컨테이너로부터 주입받을 수 있다

 

먼저, 인터페이스와 그 구현체를 정의해보자

public interface MusicService {
    String playMusic();
}

@Component
public class ClassicalMusicService implements MusicService {
    public String playMusic() {
        return "Playing classical music";
    }
}

@Component
public class PopMusicService implements MusicService {
    public String playMusic() {
        return "Playing pop music";
    }
}

 

그리고 MusicPlayer 클래스에서 의존성 주입을 사용해 MusicService를 주입받는다

@Component
public class MusicPlayer {
    private MusicService musicService;

    @Autowired
    public MusicPlayer(MusicService musicService) {
        this.musicService = musicService;
    }

    public void playMusic() {
        System.out.println(musicService.playMusic());
    }
}

 

여기서 @Component 어노테이션은 해당 클래스가 Spring 컨테이너에 의해 관리되는 컴포넌트임을 나타낸다

 

@Autowired 어노테이션은 Spring에게 이 생성자를 통해 MusicService 타입의 적절한 빈(bean)을 자동으로 주입하도록 지시한다

 

이렇게 되면, MusicPlayer는 더 이상 MusicService의 구현체를 직접 생성할 필요가 없게 된다

대신, Spring 컨테이너가 실행 시점에 적절한 MusicService 구현체를 MusicPlayer에 제공한다

이 예제에서는 ClassicalMusicService와 PopMusicService 중 어떤 것이 주입될지 명시적으로 지정하지 않았다

실제 애플리케이션에서는 @Qualifier 어노테이션을 사용하거나, 구성 클래스 또는 XML 구성을 통해 특정 구현체를 주입받도록 설정할 수 있다

Spring의 IoC를 사용함으로써, 우리는 MusicPlayer와 MusicService 간의 결합도를 낮출 수 있다

 

이는 MusicPlayer가 구체적인 MusicService 구현에 의존하지 않고 인터페이스에만 의존하기 때문이다

 

결과적으로, 새로운 종류의 음악 서비스를 추가하거나 기존 서비스를 변경할 때 MusicPlayer 클래스를 수정할 필요가 없게 된다

 

 

결론적으로

IoC는 더 넓은 개념의 설계 원칙이며, DI는 그 원칙을 실제로 적용하는 한 가지 방법입니다. DI를 통해 IoC의 원칙을 코드에 적용함으로써, 개발자는 더 유지보수가 용이하고, 확장성이 높으며, 테스트하기 쉬운 소프트웨어를 개발할 수 있습니다.

 

아래 그림이 이해되는지 점검해보자

DL에 대한 설명은 링크를 통해 확인하도록 하자

 


참고

https://mangkyu.tistory.com/150

 

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

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

mangkyu.tistory.com