의존성 역전 원칙(Dependency Inversion Principle)

옹심이·2025년 1월 9일
0
post-thumbnail

이 원칙은 고수준/저수준 모듈이 추상화에 의존해야 한다는 원칙이다. 의존성 역전 원칙을 이해하기 위해서는 의존성이 무엇인지 먼저 이해해야 한다.

의존성

의존 : 다른 객체나 함수를 사용하는 상태, 어떤 객체가 다른 코드를 사용하고 있기만 해도 의존하고 있다고 볼 수 있다.

class Printer{ public void print(Book book){//do something} }

Printer 클래스는 print 메서드의 매개변수에서 Book 클래스를 사용하기 때문에 Printer 클래스는 Book 클래스에 의존한다.

class Book{private String content; private Writer writer; //do something}

위 코드와 마찬가지로 Book 클래스 또한 Writer 클래스를 사용하고 있어 Book 클래스가 Writer 클래스를 의존하고 있음을 알 수 있다.

class Car implements Vehicle{//do somthing }

Car 클래스는 Vehicle 인터페이스로 정의된 역할을 책임으로 사용한다. 따라서 상속이나 구현 관계도 의존 관계라는 사실을 알 수 있다.

이철럼 의존 관계는 단순하고 명확하다. 어떤 코드에서 다른 코드를 사용하기만 해도 의존하는 것이다.

의존성 equals 결합도?

컴퓨터 공학에서 사용되는 결합도라는 단어를 들어봤을 것이다. 결합은 우리가 공부중인 의존성과 같은 의미이며, 결합도란 얼마나 결합이 강하게 돼있는지 평가하는 지표이다.

이처럼 의존성은 말처럼 어려운 것이 아니지만 소프트웨어 설계에서 권장되는 약한 의존 상태로 만들고 유지하는 것은 어렵다.

그런데 여기서 내가 의존성을 약화 시키는 법을 잘 알고 있는지 의문이 든다. 이제 객체를 사용하면서 의존성을 약하게 유지하는 방법을 알아보자

의존성 주입

의존성을 약화 시키는 가장 대표적인 방법이다. 코드를 통해 외부에서 의존성을 넣어주는 의존성 주입에 대해 알아보자

class HambergerChef{
	public Food make(){
		Bread bread = new WheatBread();
		Meat meat = new Beef();
		Vegetable vegetable = new Lettuce();
		Sauce sauce = new TomatoSauce();
		
		return Hambergur.builder()
				.bread(bread)
				.mear(meat)
				.vegetable(vegetable)
				.sauce(sauce)
				.build();
	}
}

Hamberger를 표현하기 위해 Food로 반환하는 부분을 통해 이 클래스는 총 10개의 클래스와 추상에 의존하고 있음을 알 수 있다.

class HambergerChef{
	public Food make(
			Bread bread,
			Meat meat,
			Vegetable vegetable,
			Sauce sauce){
		return Hambergur.builder()
				.bread(bread)
				.mear(meat)
				.vegetable(vegetable)
				.sauce(sauce)
				.build();
	}
}

수정된 코드는 메서드가 필요한 협력 객체를 외부에서 전달 받아 사용하도록 바뀌었다. 이처럼 외부에서 협력 객체를 주입할 수 있게 된 상황을 보고 의존성 주입이 사용되었다고 한다. 위 코드처럼 의존성을 주입하는 방식은 매개변수 주입이라고 부르며 다양한 주입 방법이 존재한다. 그렇다면 의존성 주입이 왜 의존성을 약화 시키는 것일까?

두 번째 코드는에서는WheatBread, Lettuce, Beef, TomatoSauce같 ㅣ구체적인 클래스에 의존하지 않게 되었다. 덕분에 의존성은 10개에서 6개로 줄어들었다.

나머지 의존성이 남아있지만, 애초에 소프트웨어는 협력으로 만들어지기 때문에 의존성을 완전히 제거하려는 시도는 무의미한 것이다.

구현 객체가 인스턴스화되는 시점을 최대한 뒤로 미루는 것이 좋다. 만약 클래스에서 new를 통해 인스턴스를 미리 할당하면 추상 타입과 관계 없이 고정된 객체를 사용하겠다는 의미가 된다. 따라서 다른 객체가 사용될 여지가 사라지기 때문에 추상화 객체를 사용하는 의미가 없게 된다.

의존성 역전

대부분의 소프트웨어 문제는 의존성 역전으로 해결이 가능하다라는 말이 있다. 이는 SOLID 원칙 중 5 번째 원칙에 해당하는 가장 중요한 원칙이다.

의존성 역전 vs 의존성 주입

식당 클래스가 햄버거 셰프에게 햄버거를 만들어달라고 부탁한다. 화살표의 방향을 통해 레스토랑 클래스가 햄버거 셰프 클래스에 의존하고 있음을 알 수 있다.

Chef 인터페이스를 만들고 셰프 구현체와 식당 클래스가 인터페이스에 의존하도록 수정하였다. 이를 다른 말로 추상화를 이용한 간접 의존 형태로 바꿨다고 표현할 수 있다. 이게 어째서 의존성이 역전되었다는 것일까?

HambergerChef의 입장에서 두 관계도를 파악해보면, 첫 번째는 HambergerChef 클래스 입장에서 화살표가 들어오며, 두 번째는 앞의 상황과 반대로 화살표가 나가는 방향으로 수정되었다.

조금 전에 화살표가 의존성을 나타낸다고 했으니 의존성이 역전 되었다고 보는 것이다.

의존성을 역적 시키면 세부 사항에 의존하지 않고 정책에 의존하도록 변한다. 여기서 정책이란 인터페이스를 의미한다. 또한 두 화살표가 가리키는 Chef 인터페이스를 기준으로 경계가 발생한다. 이 경계를 통해 모듈의 상하 관계를 파악할 수 있다.

상위 모듈은 하위 모듈에 의존해서는 안되며, 의존성 역전을 통해 이를 실현할 수 있다.

restaurant 모듈과 hamburger모듈을 상 하 관계로 나누면 의존 방향이 하위 모듈이 상위 모듈을 바라보게 만듦으로써 restaurant 모듈 같은 상위 모듈의 재사용성을 높일 수 있다.

예를 들어, hamburger 모듈을 Koreandish모듈로 교체하고자 할 때 하위 모듈을 교체하기만 하면 된다. 상위 모듈은 하위 모듈에 의존하지 않기 때문에 하위 모듈을 쉽게 교체해 새로운 기능을 제공할 수 있는 것이다.

꼭 restaurant과 Chef인터페이스가 상위 모듈이 되어야할까?

앞서 의존 방향의 경계가 모듈을 나누는 기준점이 된다고 했기 때문에, Restaurant 클래스가 상위 모듈이 되고 Chef와 Hamburger Chef를 하위 모듈로 구성할 수 있다. 하지만 이러한 설계에는 문제가 있다. 왜냐하면 상위 모듈이 하위 모듈에 의존하는 꼴이 되기 때문이다. 이전에 Hamburger Chef를 Koreandish로 교체했던 것처럼 쉽게 설계를 변화시킬 수 없을 것이다.

만약 하위 모듈인 Chef 인터페이스에 변화가 생기거나 사라진다면 상위 모듈도 똑같이 영향을 받을 것이다.

  1. 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
  2. 추상화는 세부 사항에 의존해서는 안된다. 세부 사항이 추상화에 의존해야한다.

의존성 역전과 스프링

스프링은 의존성 주입을 지원하지만 의존성 역전 원칙을 지원하지는 않는다. 의존성 역전 원칙은 설계의 영역이기 때문에 설계 부문에서 개발자들이 능동적으로 신경써야한다.

우리가 해온 프로젝트의 컴포넌트 호출 관계를 되돌아보자.

이 호출 관계를 살펴보면 의존성 역전을 찾아볼 수 없다. 따라서 SOLID 관점에서 이는 좋은 설계라고 말하기 어렵다.

이러한 구조가 의존성 역전 원칙 관점에서 보면 더 좋은 설계이다. 하위 모듈이 교체 되어도 프로그램 전반에 영향을 주지 않는다. 이 부분은 나중에 더 깊이 다뤄보도록 하자

의존성이 강조되는 이유

설계 관점에서 유지보수성을 판단할 때는 크게 세 가지 맥락이 있다.

  1. 영향 범위 : 코드 변경으로 인한 영향 범위
  2. 의존성 : 소프트웨어에서 의존성 관리가 제대로 이뤄지고 있는가
  3. 확장성 : 쉽게 확장 가능한가

이를 해결하는 방법

  1. 영향 범위에 문제가 있으면 응집도를 높이고 적절히 모듈화해서 단일 책임 원칙을 준수하는 코드를 만든다.
  2. 의존성 문제가 있다면 의존성 주입과 의존성 역전 원칙 등을 적용해 의존 관계를 만든다
  3. 확장성에 문제가 있다면 의존성 역전 원칙을 이용해 개발 폐쇄 원칙을 준수하는 코드로 만든다

이처럼 설계 측면에서 강조되는 부분은 코드 변경이나 확장 시에 영향 범위를 최소화 시키는 것이다. 의존성이 이 부분에서 어떤 이점을 제공하는지 살펴보도록 하자.

이 컴포넌트 관계도에서는 컴포넌트들이 단방향으로 의존하고 있는 것을 볼 수 있다. 따라서 C 컴포넌트가 수정된다면 C 컴포넌트에 의존하는 A, B 컴포넌트에도 영향이 전파된다. 이러한 상황을 의존성 전이라고 한다.

하지만 컴포넌트 관계도를 조금만 수정해주면 이렇게 의존성이 전이되는 문제를 해결할 수 있다.

C컴포넌트 자리를 C 인터페이스로 교체해주고, C 컴포넌트가 C인터페이스에 의존하도록 수정하였다. 이러면 C컴포넌트에 변화가 생겨도 C인터페이스의 영향 범위에 있는 A, B 컴포넌트에 아무런 영향도 미치지 않는다.

같은 맥락으로 자바 인터페이스가 구현을 가져서 안되는 이유도 설명이 된다. 추상에 구현이 들어가 변경이 잦아서는 안되며, 추상이 자주 변경되면 전이되는 의존성으로 인해 영향을 받는 클래스가 너무 많아지게 된다.

의존성과 순환 참조

스파게티 코드는 더러운 코드, 변수 이름이 지저분한 코드, 메서드 분할이 일어나지 않은 코드가 아니다. 이는 의존 관계 관리가 제대로 안되고 있는 상태이다.

순환 참조는 이런 관점에서 아주 심각한 스파게티 코드이다. 순환 참조 시에는 의존에 의한 영향 범위가 확장되기 때문에 좋은 설계라고 할 수 없는것이다.

이 설계도를 보면 셋 중 어떤 컴포넌트를 변경해도 모든 컴포넌트에 영향을 미치는 것을 알 수 있다. 따라서 A, B, C는 같은 컴포넌트나 다름이 없다.

이처럼 순환 참조 관계에 있는 설계는 의존성 전이 범위를 확장시키고 이는 변경으로 인한 영향 범위를 축소하라는 목표에 반하는 것이다.

0개의 댓글