거북이-https://velog.io/@violet_evgadn 이전완료

IoC와 DI 본문

웹 개발/Spring(이론)

IoC와 DI

VioletEvgadn 2022. 8. 3. 23:10

IoC

◎ IoC란?

Spring Container와 Spring Bean을 제대로 공부했다면 이제는 IoC가 무엇인지 바로 설명할 수 있을 것이다.

 

IoC는 Inversion of Control(제어의 역전)의 약자로써 메소드나 객체 호출 및 생성 작업을 개발자가 직접 하지 않고, 외부(특별한 객체)가 수행하도록 하는 것이다.

즉, 기존에는 개발자가 객체를 생성했다면 IoC에서는 개발자는 어떤 객체를 활용할지만 (Spring에게) 알려주면 제어권을 가진 주체(Spring Container)가 알아서 의존성 객체(Spring Bean)을 주입해주는 것을 의미한다.

 

IoC를 통해 사용자가 제어의 흐름을 컨트롤하지 않고 스프링이 대신 작업을 처리함으로써 개발자는 로직에만 신경쓰면 되기 때문에 효율적인 개발이 가능해진다.

 

IoC는 DI와 DL에 의해 구현되는데, IoC를 제대로 이해하기 위해서는 DI와 DL을 알 필요가 있다.


DI

◎ DI란?

DI는 Dependency Injection(의존성 주입)의 약자이다.

DI는 Spring이 다른 프레임워크와 차별화되어 제공하는 기능으로써 Spring이 IoC가 잘 되는 핵심 로직이라고 할 수 있다.

 

DI는 객체를 직접 생성하지 않고, 외부에서 생성한 후 필요한 객체를 주입해주는 방식을 말한다.

 

먼저 "의존성"이라는 단어부터 알고 가자. A라는 클래스가 존재하는데 이 때 A라는 클래스 내에 B 클래스에 대한 메서드 C가 존재한다고 가정하자.

이 때 A 클래스의 C 메서드를 수행하기 위해서는, B 클래스가 먼저 선언되어 있어야 한다.

이 경우 A 클래스는 B 클래스 선언 여부에 영향을 받기 때문에 "A 클래스는 B 클래스에 의존성을 가진다"라고 말한다.

 

원래라면 우리는 A 클래스 내 B에 대해서 `B b = new B()` 명령어를 통해 B Instance를 선언해주고 프로그래밍을 진행할 것이다. 하지만, DI를 활용하면 다르다.

DI를 활용해선 외부에서 이미 선언되어 존재하는 B Instance를 A 클래스에 전달해주어 A 클래스 내부에서는 B 클래스에 대한 선언 없이 B 클래스를 활용할 수 있게 되는 것이다.

 

이미 Spring Container와 Spring Bean에 대해 주구장창 배웠으니 이 개념을 이용해보자.

우리는 Spring Container 내에 존재하는 Spring Bean 저장소에 수많은 Bean들을 저장했다. 그리고 위 예시처럼 프로그램 상에서 A라는 클래스를 실행시키고 싶다고 가정하자.

이 때 개발자는 A 클래스 내에 B Instance를 직접 선언하지 않고 컨테이너에 등록되어 있는 Bean을 꺼낸다. 이후 A 클래스 내에 등록되어 있던 Bean을 주입해줌으로써 A 클래스는 B 클래스에 대한 선언 없이 바로 로직을 수행할 수 있게 되는 것이다.

즉, DI를 통해 Spring Container(A 클래스 입장에서 외부) 내에 존재하는 Spring Bean(B 객체)을 활용하여 A 클래스 로직을 수행할 수 있게 되는 것이다.

 

◎ DI 장점

<의존성이 줄어듬>

DI는 의존성을 "Runtime 시" 주입하기 때문에 의존성이 많이 감소한다.

즉 "Compile 시" 의존성을 크게 고려하지 않아도 되기 때문에 코드를 짤 때부터 의존성에 대해 고민하거나 이에 대해 코드를 수정할 피로도가 많이 감소된다는 말이다.

 

이 의존성이 줄어든다는 것이 매우 중요한 장점임과 동시에 이해가 안되는 부분이기도 했다. 코드를 통해 알아보자.

class Wine extends Alcohol {
	...
}

class Soju extends Alcohol {
	...
}

class Drinking{
    private Alcohol alcohol;
    
    public Drinking() {
    	this.alcohol = new Wine();
    }
}

위 예시는 DI를 활용하지 않은 우리가 일반적으로 활용하는 코딩 방식이다.

여기에서 내가 Soju를 먹고 싶을 경우 `this.alcohol = new Soju()`로 코드를 수정한 이후 다시 프로그램을 수행시켜야 한다.

즉 외부의 변화에 Drinking이라는 의존을 받는 클래스가 너무나 많은 영향을 받게 된다.

 

DI를 활용하여 외부에서 객체를 주입받아 보자.

class Wine extends Alcohol {
	...
}

class Soju extends Alcohol {
	...
}

class Drinking{
    private Alcohol alcohol;
    
    public Drinking(Alcohol alcohol) {
    	this.alcohol = alcohol;
    }
}

우리는 Drinking 객체를 선언할 때 의존성을 가진 객체인 Alcohol에 대한 Instance를 입력해줘야 한다

만약 입력값으로 Wine Instance를 준다면 Wine 클래스에 대한 메서드가 수행될 것이며, Soju Instance를 준다면 Soju 클래스에 대한 메서드가 수행될 것이다.

 

따라서 무엇을 마시고 싶든 객체 선언 시 매개변수 값만 바꿔주면 되고, Alcohol이라는 변수가 변경되었다 하더라도 Drinking 클래스 자체는 큰 영향을 받지 않게 되는 것이다.

 

이런 현상을 "의존성이 줄어든다"라고 표현하며, 다른 말로 객체 간 결합도를 줄인다고도 표현할 수 있다.

객체 간 결합도를 줄임으로써 한 개의 클래스를 수정하더라도 다른 클래스까지 수정해야 하는 상황을 막거나 매우 줄일 수 있기 때문에 DI의 가장 큰 장점이라고 말할 수 있다.

 

<단위 테스트의 편의성을 높여줌>

위 코드 예시에서 Drinking 클래스와 Alcohol 클래스에 대한 테스트를 진행해야 한다고 가정하자.

 

이 때 DI를 활용했다면 Alcohol에 대한 테스트와 Drinking에 대한 테스트를 분리하여 진행할 수 있기 때문에 편의성이 매우 높아진다.

 

<코드 재사용성을 높임>

위 코드 예시를 보면 재사용성이 높아짐을 단번에 알 수 있을 것이다.

만약 Alcohol 클래스를 상속받은 클래스가 매우 많더라도 외부에서 의존성을 주입해주므로 굳이 코드를 수정하지 않아도 주입하는 Instance 값만 바꿔주면 코드를 그대로 재사용하여 다른 로직이 수행되도록 만들 수 있다.

 

◎ DI 방법

<Setter 주입>

Setter를 통해 의존성을 주입하는 방법으로 편하지도 않으면서 장점이 뚜렷하지도 않은 가장 애매한 주입법이다. 단위 테스트에서든 실제 프로젝트에서든 거의 활용하지 않는다고 보는 것이 편하다.

 

Setter 주입의 가장 큰 단점은 의존성 객체가 Null값이여도 프로그램이 수행될 수 있다는 점이다. 아래 코드를 살펴보자.

public class Controller {
    private MemberService memberService;

    @Autowired
    public void setMemberService(MemberService memberService) {
        this.memberService = memberService;
    }
}

만약 Controller 객체가 생성되어 수행된다면 MemberService 클래스의 메서드도 수행될 수 있으므로 memberService 객체는 Null이여서는 안된다.

하지만 Setter 주입을 활용하면 new Controller()로 객체만 생성시킨 뒤 Setter를 수행시키지 않아 memberService 객체가 Null 값을 가질수도 있게 된다.

이 경우 Controller라는 서비스는 실행되는데 실질적은 로직을 담당해야하는 MemberService 객체가 Null값이기 때문에 NullPointerException이 발생할 수 있는 위험성이 생긴다

 

<Field 주입>

@Autowired 어노테이션만 활용하면 바로 주입이 가능하므로 매우 편리한 주입법이다. 단위 테스트 때는 많이 활용하지만 메인 코드에서는 사용을 권장하지 않는데 몇 가지 이유가 존재한다.

 

1. SRP(단일 책임 원칙)을 위반할 수 있다.

@Autowired 선언만 하면 필드 주입은 쉽고 빠르게 가능하다. 그래서 필드 주입을 활용하다보면 너무나 많은 Bean들을 1개 클래스에서 활용 할 수 있다. 그런데 이렇게 Bean들이 많아질 경우 1개의 클래스가 너무나 많은 책임을 떠안게 된다.

 

1개의 클래스가 너무나 많은 역할을 하는 것은 Refactoring 신호가 되는데, 생성자 주입을 하면 `this.X = X` 명령어를 수없이 입력해야 하므로 Refactoring 신호를 인지할 수 있지만 필드 주입은 이 신호를 인지하는 것을 어렵게 하므로 추천하지 않는다.

 

2. Field 주입은 숨은 의존성을 제공한다.

만약 어떤 Class가 특정 의존성에 대해 책임을 지지 않을 경우 생성자나 Setter 주입은 책임 여부를 확실히 할 수 있다.

하지만 필드 주입은 어떤 클래스가 어느 정도의 책임을 져야하는지 표현이 모호하므로 의존성이 숨게 되며, 이를 숨은 의존성을 제공한다고 말한다.

 

즉 A가 B에 의존성을 가지고 B가 C에 의존성을 가지는 상황에서 에러가 발생했다면 필드 주입은 A와 B 사이에 문제가 발생한 것인지 B와 C 사이에 문제가 발생한 것인지 모호해진다는 것이다.

 

3. DI Container와 너무나 강한 결합을 가져 단위 테스트가 어려워진다.

필드 주입을 활용하면 결국 DI Container에 존재하는 Bean만 활용할 수 있다. 그런데 단위 테스트를 할 때는 Container에 올라간 Instance가 아닌 임의의 Sample Instance로 실험해보고 싶을 수도 있을 것이다.

생성자 주입이나 Setter 주입은 내가 원하는 Instance를 매개변수로 전달해줄 수 있으므로 편리하게 단위 테스트를 진행할 수 있지만 필드 주입은 Container에 올라가 있는 Bean만 연결시켜주므로 단위 테스트가 까다로워진다.

 

4. 불변성(Immutability)를 가지지 못한다.

생성자 주입을 활용하면 final을 통해 객체를 선언할 수 있어 변하지 않는 객체 형태로 의존성 주입을 받을 수 있다.

하지만 필드 주입은 final 선언이 불가하므로 주입받은 객체가 변할 수 있다는 위험성이 존재한다.

 

5. 순환 의존성이 발생할 수 있다.

순환 의존성이란 A가 B를 참조하는 상황에서 B가 A를 참조하여 서로가 서로를 참조하는 경우를 말한다.

생성자 주입을 활용하면 BeanCurrentlyCreationException을 발생시키므로 순환 의존성을 바로 알아챌 수 있지만 필드 주입은 이를 알 수가 없다.

 

이렇게 많은 단점이 존재하므로 예전에는 간편함 때문에 필드 주입이 유명했으나 최근에는 거의 활용하지 않는 추세이다.

 

☆☆☆☆<생성자 주입>☆☆☆☆

가장 권장하는 방법으로, Lombok에서 @RequiredArgsConstructor를 활용하면 쉬운 주입이 가능해진다

생성자 주입을 권장하는 이유는 아래와 같다.

 

 

1. 객체의 불변성이 확보된다.

개발을 하면 의존 관계의 변경이 필요한 상황이 거의 없다.

Setter 주입이나 필드 주입을 활용하면 final 접근제어자를 활용하지 못하므로 동작 과정에서 객체가 변동될 가능성이 발생한다.

 

하지만 생성자는 final 활용이 가능하여 객체가 변동할 가능성이 없으므로 불변성을 보장할 수 있다는 장점이 있다.

 

2. 테스트 코드 작성이 쉬워진다는 점이다.

Setter 주입과 마찬가지로 의존성 주입을 내가 원하는 Instance로 수행할 수 있으므로 단위 테스트 때 내가 원하는 Sample Instance를 매개변수로 넣어 실험을 진행할 수 있게 되는 것이다.

 

3. 순환 의존성을 효율적으로 찾을 수 있다는 것이다.

순환 의존성은 위에서 설명했었다.

Setter 주입이나 Field 주입의 경우 A와 B 사이에 순환 의존성이 생길 경우 두 객체가 모두 생성되어 애플리케이션이 구동되기는 한다. 하지만 A나 B를 호출할 경우 서로가 서로를 무한 로딩하여 StackOverFlow 에러가 발생할 때 까지 계속해서 서로를 호출한다

즉, 에러를 감지하는데 어느 정도의 시간이 필요하며 에러 내용으로 어떤 부분에서 문제가 발생했는지도 파악하기 어려워진다.

 

그에 비해 생성자주입은 A가 B에 의존해서 B로 갔는데, B가 A를 의존할 경우 바로 BeanCurrentlyCreationException을 발생시킨다. 또한 어떤 부분에서 주입이 잘못되었는지도 알려주기 때문에 로그를 확인하면 어떤 부분에서 문제가 발생했는지 파악하기도 쉬워진다.

 

4. Lombok과의 결합이 가능하여 쉬운 코딩이 가능해진다는 점이다

위에서도 가볍게 말했지만, Lombok의 @RequiredArgsConstructor를 활용하면 바로 생성자 주입을 활용한 DI가 가능해진다.

필드 주입은 간편하긴 하지만 클래스 1개당 @Autowired를 계속해서 붙여줘야하며 Setter 주입은 Setter 함수 만드는 것 자체가 어렵다. 이런 점에서 Lombok을 활용해 한 번에 완료시킬 수 있다는 장점을 가진다.

 

◎ DL이란?

DL은 Dependency Lookup(의존성 검색)의 약어이다.

DI를 통해 의존성을 주입하기 위해선 "어떤 객체"를 주입할지 찾아야 한다.

DL은 Spring Container에 존재하는 수많은 Bean 중 필요한 Bean을 검색하는 방법이다.


요약

IoC란 제어의 역전이라는 의미로 개발자가 객체를 직접 관리하는 것이 아닌 개발자는 어떤 객체를 활용할지만 프레임워크 상에 알리면 프레임워크에서 알아서 요청한 객체에 적절한 Instance를 찾아 의존성 주입을 통해 객체를 관리해주는 방법을 말한다.

이 때 적절한 Instance를 찾는 것을 DL, 그리고 찾은 Instance를 주입해주는 것을 DI라고 한다.

 

DI를 활용하면 의존성이 줄어들며 단위 테스트의 편의성을 높여주고 코드 재사용성을 높일 수 있다는 장점을 지닌다.

 

DI 방법에는 Setter 주입, 필드 주입, 생성자 주입이 존재한다.

이 중 객체의 불변성을 확보할 수 있고 쉬운 단위 테스트를 가능케 하며 순환 의존성이 존재하는 부분을 효율적으로 찾을 수 있는 생성자 주입 활용을 강력히 추천한다.

또한 생성자 주입은 Lombok과 같이 활용하면 쉬운 코딩이 가능해진다는 장점을 가지며 숨은 의존성을 제공하는 필드 주입의 단점을 가지지 않는다는 것도 큰 장점이다.

'웹 개발 > Spring(이론)' 카테고리의 다른 글

Spring의 데이터 처리 방법  (0) 2022.09.19
AOP  (0) 2022.08.04
스프링 컨테이너와 스프링 빈  (0) 2022.08.03
Spring  (0) 2022.08.02
IntelliJ 단축키 + 추천 코딩 방법  (0) 2022.08.02
Comments