Spring (1) - DI

Bruce Han·2022년 3월 20일
1

Spring 튜토리얼

목록 보기
1/3
post-thumbnail

Prolog

최근 들어 스프링을 공부하고자 무료 강의부터 수강했었다. 강사분들은 친절하게 스프링을 설명해주셨지만, 스프링을 설명할 때 나오는 의존성 주입은 소스만 보면서 이해하기엔 스프링을 제대로 공부해보지 않은 나에게는 막연한 단어일 뿐이었다.
이 단어를 먼저 익히지 않으면, 아무리 스프링을 공부한다고 한들 밑 빠진 독에 물 붓기가 될 것 같아 먼저 개념에 대해 정리해보기로 했다.

DI란?

Dependency Injection - 의존성(의존 관계) 주입

이 DI라는 개념은 어쩌다가 나오게 된 것일까요?

Java로 프로그래밍을 하면서 객체를 생성할 때 직접 클래스에 new 연산자를 이용하여 생성했었습니다. 하지만 개발자가 코드를 추가하여 객체를 생성하는 것이 아니라 컨테이너가 이를 생성시켜 주게 되면, 코드에서 직접적인 연관 관계가 발생하지 않아 각 클래스들의 변경이 자유로워집니다. 이를 느슨한 결합이라고 하는데요.
이러한 느슨한 결합을 수월하게 해줄 수 있도록 DI라는 기능이 도입됐다고 볼 수 있습니다.

정의

의존성 주입은 말 그대로 의존성이 있는 객체를 만들어서 넣거나 제어하는 것이 아니라, 특정 객체에 필요한 객체를 외부에서 결정해서 연결하는 것을 의미합니다.
다시 말해, 구성요소 간의 종속성을 소스코드에서 설정하지 않고 외부의 설정파일 등을 통해 주입하도록 하는 디자인 패턴입니다.

그러면, 의존 관계란 무엇일까요?

"A가 B를 의존한다."라는 표현을 토비의 스프링에서는 다음과 같이 정의되어있습니다.

의존대상 B가 변하면, 그것이 A에 영향을 미친다.
-- 이일민, 토비의 스프링 3.1, 에이콘(2012), p113

즉, B의 기능이 추가 또는 변경되거나 형식이 바뀌면 그 영향이 A에 미친다는 것입니다.

다음의 바리스타 예시를 보며 설명하겠습니다.

class Barista {
	private CoffeeManual coffeeManual;
    
    public Barista() {
    	coffeeManual = new CoffeeManual();
	}
}

위 코드에 나온 것처럼, 바리스타는 커피 메뉴얼에 의존합니다. 메뉴얼이 바뀌었을 때, 바뀐 레시피에 따라서 바리스타는 제조 공정을 수정해야 합니다. 메뉴얼의 변화가 바리스타의 행위(제조 공정)에 영향을 줬기 때문에, "바리스타는 메뉴얼에 의존한다"고 말할 수 있습니다.
메뉴얼과 제조 공정은 다른 의미를 지닌 단어입니다. 혼동 주의


의존 관계를 인터페이스로 추상화하기

위에서 살펴본 Barista 예시를 다시 봅시다. 지금까지 구현한 것으로는 CoffeeManual만을 의존할 수 있는 구조로 되어있다는 것을 알 수 있습니다. 더 다양한 CoffeeManual을 의존 받을 수 있게 구현하려면 인터페이스로 추상화해야합니다.

다양한 커피 제조 공정법이 있는 메뉴얼에 의존할 수 있는 Barista를 밑에 나올 코드로 알아보겠습니다.

class Barista {
	private CoffeeManual coffeeManual;
    
    public Barista() {
    	coffeeManual = new AmericanoManual();
		// coffeeManual = new CafeLatteManual();
        // coffeeManual = new CappuccinoManual();
    }
}

interface CoffeeManual {
	newCoffee();
    // 이외의 다양한 메서드
}

class AmericanoManual implements CoffeeManual {
	public Coffee newCoffee() {
    	return new Americano();
    }
    // ...
}

의존 관계를 인터페이스로 추상화하게 되면,
1. 더 다양한 의존 관계를 맺을 수가 있고,
2. 실제 구현 클래스와의 관계가 느슨해지며,
3. 결합도가 낮아집니다.

잠깐, 결합도가 뭔가요?

먼저 모듈의 뜻을 알아야 합니다. 모듈이란, 기능상 성격이 비슷한 또는 연관성 있는 부분들이 조립된 덩어리, 어떤 기능을 처리하는 코드가 모여있는 덩어리라는 추상적인 의미를 지닌 단어입니다.
다시 돌아와서 결합도는, coupling, 의역하면 의존도라고 보시면 되겠습니다.
의존도란? 어떤 모듈이 다른 모듈에 의존하는 정도를 나타내는 것입니다.

이 의존도가 높아지면
1. Unit Test가 어려워집니다. 내부에서 직접 생성하는 객체에 대해서, mocking을 할 방법이 없어집니다. 따라서 그만큼 단위테스트를 하기가 까다로워지고, 후에 서비스 장애에 대한 유연하고 안정적인 대응이 어려워질 수 있습니다.
그렇게 해서 장애 생기면 야근 angle
2. Code의 변경이 어려워집니다. 맨 처음에 나온 예시 코드처럼 Barista 클래스는 생성자에서 CoffeeManual 객체를 직접 생성하여 사용하고 있습니다. 만약 나중에 CoffeeManual라는 클래스가 CafeLatteManual로 바뀐다면, 지금처럼 CoffeeManual 클래스에 의존하고 있는 Barista의 클래스도 직접 같이 변경해줘야 합니다. 즉, 서로 뭔가 없으면 허전해지는 사이가 되죠. 객체 간의 강한 결합력이 생기게 된다는 겁니다. 이는 소프트웨어 모듈화의 목적인 낮은 결합력과 높은 응집도에 적절치 않은 행위가 됩니다.


그렇다면, Dependency Injection은?

의존 관계가 무엇인지에 대해, 그리고 다양한 의존 관계를 위해 인터페이스로 추상화한다는 것을 알아봤습니다. Dependency Injection은 무엇일까요?

지금까지의 구현을 보면, Barista 내부적으로 의존관계인 CoffeeManual이 어떤 값을 가질지 직접 정하고 있습니다.
만약 어떤 CoffeeManual을 만들지 회사가 정하는 상황을 상상해봅시다.투썸이나 스타벅스 생각해봅시다
즉, Barista가 의존하고 있는 CoffeeManual을 외부(회사)에서 결정하고 주입하는 것입니다.

이처럼 그 의존 관계를 외부에서 결정하고 주입하는 것이 DI(의존 관계 주입)입니다.
토비의 스프링에서는 다음의 세 가지 조건을 충족하는 작업을 의존 관계 주입이라 합니다.

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

DI 구현 방법

DI는 의존 관계를 외부에서 결정하는 것이기 때문에, 클래스 변수를 결정하는 방법들이 곧 DI를 구현하는 방법입니다. 런타임 시점의 의존 관계를 외부에서 주입하여 DI 구현이 완성됩니다.

카페 회사가 어떤 레시피를 주입하는지 결정하는 예시 두 가지로 설명하고자 합니다.

  • 생성자를 이용
class Barista {
	private CoffeeManual coffeeManual;
    
    public Barista(CoffeeManual coffeeManual) {
    	this.coffeeManual = coffeeManual;
    }
}

class CafeCompany {
	private Barista barista = new Barista(new AmericanoManual());
    
    public void changeManual() {
    	barista = new Barista(new CafeLatteManual()); 
    }
}
  • 메서드를 이용(대표적으로 Setter 메서드를 씁니다)
class Barista {
	private CoffeeManual coffeeManual = new AmericanoManual();
    
    public void setCoffeeManual(CoffeeManual coffeeManual) {
		this.coffeeManual = coffeeManual;
	}
}

class CafeCompany {
	private Barista barista = new Barista();
    
    public void changeManual() {
    	barista.setCoffeeManual(new CafeLatteManual());
    }
}

DI를 통한 구현의 이점

1. 의존성이 줄어듭니다.

의존한다는 것은 대상이 변화하였을 때, 이에 맞게 수정해야 합니다.
즉, 의존 대상의 변화에 취약하다는 것입니다.
DI를 통하여 구현하게 되었을 때, 주입받는 대상이 변하더라도 그 구현 자체를 수정할 일이 없거나 줄어들게 됩니다.

2. 재사용성이 높은 코드가 됩니다.

기존에 Barista 내부에서만 사용되던 CoffeeManual을 별도로 구현하면, 다른 클래스에서 재사용할 수 있습니다.

3. 테스트하기 좋은 코드가 됩니다.

CoffeeManual의 테스트를 Barista 테스트와 분리하여 진행할 수 있습니다.

4. 가독성이 높아집니다.

CoffeeManual의 기능들을 별도로 분리하게 되어 자연스레 가독성이 높아집니다.


Epilogue

막연하게 DI라고만 알고있어서 이를 어떻게 적용해야 되는지, 왜 적용해야되는지 잘 모르고 있었다. 이렇게 알아보고 나서 DI의 원리도 알게됨에 따라 DI 라는 것을 적용했을 때 개발함에 있어 더 좋은 코드로 만들 수 있다는 것이 JJIN으로 흥미롭게 느껴졌다.
러닝커브가 높다고 막연하게만 느껴졌던 스프링에 한 발 짝 더 다가갈 수 있게 된 것 같아 조금씩 자신감이 쌓여간다.


References

만약 혼동되거나 틀린 내용이 있다면 지적 해주시면 감사드리겠습니다 🙇‍♂️

profile
만 가지 발차기를 한 번씩 연습하는 사람은 두렵지 않다. 내가 두려워 하는 사람은 한 가지 발차기를 만 번씩 연습하는 사람이다. - Bruce Lee

0개의 댓글