카테고리 없음

1. 의존과 의존성 주입(DI) - 의존성에 대해서

일반정보 2021. 12. 19. 16:17

○ 목차

  1. 개요 : 의존성 주입(DI)는 스프링의 꽃이다.  
  2. 의존성
  3. 의존성의 문제점 
  4. DI(의존성 주입)
  5. DI 가 문제를 해결하는 가?
  6. SRP
  7. DI의 대해 좀 더 

 

 

 

○ 의존성 주입(DI)은 스프링의 꽃이다.  

학교를 다니면서 스프링의 대해서 이야기가 나올 때에면, 가장 많이 나오던 말이었습니다. DI, 그놈의 DI가 무엇이길래 이렇게 말이 나오는 것인지 많이 궁금하기도 했습니다. 사실 이런 말이 나왔다는 것은 그만큼 중요하다는 말이고 실제로도 스프링의 핵심 요소이기도합니다. 만약 스프링에 DI가 없었더라면 이렇게 범용적으로 사용되는 프레임워크는 아녔을 것입니다. 

이번 시간에는 스프링의 대해서 배우기 전 의존성과 DI(의존성 주입 - Dependency Injection )의 대해서 이것이 무엇이고, 왜 필요한 지, 왜 필요하게 되었는 지의 대해서 알아보려고 합니다. 

 

 

 

○ 의존성

의존적이라는 것은 무엇일까요. 우리가 자주 사용하는 말로써 설명하려면 너무나 추상적입니다. 말만 들어서는 원리를 제대로 이해하기 힘들 것입니다. 때문에 이번 글을 이해하기 쉽고, 또 제대로 이해할 수 있도록 원리와 그림 중심으로 설명해 보도록 하겠습니다. 

예를 들어 공장에서 A씨와 B 씨가  존재한다고 해 봅시다. 공장에서 A는 물건을 만들고, B는 A가 만든 물건을 포장하도록 각각의 역할이 있다고 해 봅시다. 

만약 A와 B중 한 명이라도 없으면 어떻게 될까요? 만약 B가 일을 그만두었다고 해 봅시다. B가 없어서 포장을 할 수는 없겠지만 적어도 어느 정도 공장 가동에는 문제가 없다고 할 수 있습니다. 포장할 사람은 없지만 당분간은 물건 생산은 가능하니까요. ( 이렇게 되면 B를 대신할 인원을 빨리 구해야 합니다. )

만약 A가 일을 그만두었다고 해 봅시다. 이렇게 되면 공장 가동에 큰 차질이 생깁니다. 비록 B는 포장 할 수 있지만, A가 없어 물건을 만들 수 없고, B는 더 이상 할 일이 없습니다.  때문에 더 이상 일을 할 수 없고, 공장 가동은 중단되게 됩니다.

보통 이러한 관계일 때 B는 A에 의존적이라고 합니다. 누가 의존했는지의 대한 기준은 추상적이지만 적어도 A가 수정 되었을 때 B가 영향을 받는다면 의존적이라고 할 수 있습니다. 반대로 영향을 받지 않는다면 의존적 관계가 아니지요.

 

A는 의존적이지 않기 때문에 상관없지만, B는 A에 의존적이여서 A 없이는 일할 수 없습니다.

 

이해를 돕기 위해서 위와 같은 예시를 들었지만, 위 내용도 추상적인 내용처럼 보일 수 있습니다.  한번 코드를 예로 들어 보겠습니다. 2개의 클래스를 생성해 보겠습니다. 위처럼 물건을 만드는 Item, 포장을 하는 Packaging 클래스로 간단하게 코드를 짜 보겠습니다. 

public class Item {
   String helloWorld = "Hello, World!";
}
public class Packaging {
  private Item item = new Item();
  public String starPack(){
  	return "*"+item.helloWorld+"*";
  }   	
}

물건은 간단하게 우리들에게 익숙한 "Hello World!"로 생각해 봅시다. 위 코드에서 각각 Item, Packaging 클래스를 작성하였습니다. 전체적인 흐름은 Item클래스에 helloWorld( == Hello World!)라는 필드가 존재하고, Packaging클래스에서는 Item 인스턴스의  "Hello World!"를 가지고 포장해서 *Hello World!*라는 문자열로 반환하는 코드입니다. 

 

위의 Packaging 클래스의 2번 라인을 보면 item 인스턴스를 생성하고 있는 것을 알 수 있습니다.

 private Item item = new Item();

Packaging 클래스에서 New 키워드를 통해 item객체를 생성했습니다. 그리고 이 item인스턴스의 helloWorld를 가지고 starPack()가 동작합니다. 이러한 상황일 때, 의존관계는 Packaging이 Item에 의존적입니다.

Item입장에서는 Packaging이 어떻게 되든지 간에 영향을 받지 않습니다. 반대로 Packaging은 Item 클래스의 코드가 수정됨에 따라 영향을 받습니다. 당장에 helloWorld의 자료형만( 예를 들면 Int형이라던가 ) 바뀌더라고 코드는 동작하지 않습니다. 

이렇게 한 쪽이 수정 되었을 때, 다른 한쪽이 영향을 받는것을 의존적이라고 합니다. 

 

 

 

○ 의존성의 문제점 

위에서 보았듯이 A가 수정 되었을 때 B가 영향을 받는다면  B는 A에 의존적입니다. 그리고 이렇게 될 경우 B는 영향을 받았기 때문에 수정해야 할 것입니다. 이것이 의존성의 문제점입니다. 

한 번 그림으로 이해해 봅시다. 객체 A는 ○를 가지고 있고, 객체B는 내부에서 객체A 를 생성해서 A의 ○를 가지고 동작한다고 합시다.

위 그림에서 B의 내용을 확인할 수 있습니다. B에서 A의 ○을 받습니다. 

만약 클래스의 내용이 변경되었다고 가정해 봅시다.  객체 A의 ○가 △로 변경되었다고 해보겠습니다. 이렇게 되면 다음과 같은 문제가 발생합니다. 

B는 A에 의존적인 상태입니다. 따라서 A가 변경되면, B도 변경되어야 합니다.

위 그림에서 B는 지금까지 ○으로만 알고 있습니다. 따라서 변경된 △를 모릅니다. 그렇게 된다면 전체는 동작하지 않을 것입니다. 만약 이러한 문제를 수정하려면 내부를 다음과 같이 바꿔야만 합니다. 

A가 변경되면 의존적인 모든 것은 바꾸어줘야 합니다.

여기서 가장 문제가 되는 것은 내부를 수정해야 한다는 것입니다. 코드 관점으로, 이미 다 만들어진 코드를 수정하는다는 것은 결코 쉬운 일이 아닙니다.  물론 코드의 수정은 가능한 이야기이지만, 현재의 시스템은 점점 복잡해지고 있고, 약간의 수정으로 서비스에 장애가 생길 수 있는 만큼 보통은 내부의 코드를 변경하는 것을 피하는 게 가장 좋습니다.  

다만 그렇다고 수정이 없을 수는 없기 때문에, 옛날 사람들은 이러한 상황을 많이 마주쳤을 것입니다. 아마 내부 코드의 변경점이 있는 부분을 하나하나 수정하기에 진절머리가 났었을 것입니다. 때문에 하나의 지혜를 생각해 내게 됩니다. 

만약 이러한 의존성을 외부로 넘길 수는 없을까?  외부에서 인스턴스를 생성시키고 받는 방법은 어떨까? 내부 코드를 수정할 일을 줄을 수 있는 방법은 무엇일까? 하고 말입니다. 

 

 

 

○ DI(의존성 주입)

의존성 주입이라는 것은 위처럼 이러한 의존성의 대한 문제 때문에, 이를 해결하고자 내부에서 이루어졌던 의존성을 외부에서 해결하고자 하는 것입니다. 

DI( Dependency Injection - 의존성 주입 )은 내부적으로만 설정 가능했던 의존성(의존관계)을 외부에서 확인, 결정, 수행하는 것입니다. 이가 어렵다면 위에서 했던 예시를 연장해서 설명해 보겠습니다. 

아까와 같은 그림입니다. 아까 전에는 A의 내용이 달라지면 B의 내용에도 영향을 끼쳐, 내부 코드 수정이 필요했습니다. 이렇게 의존관계가 생기는 이유는 A가 B안에 있기 때문입니다. 이를 한번 밖으로 꺼내봅시다.

객체 A와 B를 독립적으로 꺼내보았습니다.

 

하나의 방을 만들고, 여기에 각각 독립적으로 객체 A와 B를 보관하겠습니다. 그리고 B를 사용하기 위해 B를 밖으로 꺼냈습니다. 

B는 보니 동작하기 위해서 A가 필요합니다. A도 꺼내어 필요한 B안에 넣어주었습니다. 이렇게 되면 전체 동작에는 문제가 없습니다. DI는 이렇게 동작합니다. 

만약 중간에 A의 내용이 ○가 △로 변경되었다면 어떻게 할까요? 그대로 동작하지 않는 것은 같습니다. 다만 아까 전과는 다르게 외부에서 조치가 가능할 것입니다.

도중에 변경하는 로직을 추가할 수 있습니다. 그렇다면 굳이 클래스 내부까지 접근하지 않아도 됩니다.

객체 A의 ○가 △로 변경되었기 때문에 우리는 객체 B안에 넣어주는 과정에서 중간에 변경을 할 수 있습니다.  △를 ○로 바꾸어 주는 코드를 B 외부에 추가했습니다. 이렇게 되면 A가 어떻게 변경되든지 간에 외부에서 코드만 수정하면 됩니다. 

즉 내부 코드를 건드리지 않고, 외부에서 의존의 관한 관리, 통제를 할 수 있다는 것입니다. 

 

 

○ DI 가 문제를 해결하는가?

위 설명을 들으면 의문점이 생길 수 있습니다. 첫 번째, 위 내용에서도 결국 근본적인 문제가 해결된 것은 아닙니다. 상황에 따라서는 내부 코드를 수정해야 하는 상황이 생길 수 있습니다. 두 번째, 위와 같은 예시처럼 조치를 내부에서 해도 (즉 의존성 있는 코드여도 조치를 내부에서 해결하면) DI와 동일한 효과라고 할 수 있을 것 같습니다.  정리하면 다음과 같습니다. 

1.  DI를 사용해도 상황에 따라서는 내부코드를 수정해야 하는 상황이 생길 수 있습니다.
2.  DI를 사용하지 않아도 내부코드를 수정하는 방법으로 해결해도 동일한 결과인 것 같습니다.

 우선 2번 의문점부터 해결해 봅시다. 

 

2.  DI를 사용하지 않아도 내부 코드를 수정하는 방법으로 해결해도 동일한 것 같습니다.

우선 위의 답변의 관한 내용은 우선 틀리다는 것을 말씀드리고 싶습니다. DI를 사용하는 것과 사용하지 않는 것은 결과가 다릅니다. 

한 번 그림으로 표현해 봅시다.

왼쪽은 ID를 구현한 경우, 오른쪽은 구현하지 않은 경우

 

왼쪽은 ID를 사용한 경우, 오른쪽은 사용하지 않고 내부적인 코드로 해결했을 경우입니다. 위와 같은 경우 사실 동일한 효과를 내는 것처럼 보이고, 실제로도 그런 편입니다. 하지만 다음과 같은 경우는 달라집니다. 

 

의존하는 객체가 많아질 수록 ID를 구현하지 않은쪽은 더 많은 문제가 생깁니다.

좀 더 확장해서 봅시다. 의존성은 여러 개에 걸쳐 있는 경우가 있습니다. A에 의존적인 객체 C, D를 추가했습니다. 이러한 경우에는 결과적으로도 차이가 나게 됩니다. 왼쪽 ID로 구현된 경우에는 외부에서 문제를 해결했기 때문에 문제가 없지만, 내부에서 해결하는 방법은 종속성을 모두 찾아 해결해주지 않으면 동작하지 않을 것입니다. 

내부 코드를 수정한다는 것을 지양하는 이유가 바로 이것입니다. 너무나 비용적인 측면에서, 신뢰 성적인 측면으로도 효율적이지 않습니다. 이러한 문제를 해결하는 것은 바로 각각 캡슐화를 시키는 것입니다. 각각의 코드가 독립적이라면 이렇게 수정할 필요도, 또 신뢰성 측면으로도 높은 시스템을 구축할 수 있습니다. 

이러한 캡슐화를 수행하기 위해서는 의존성이 큰 장애물이었던 것입니다. 그리고 이를 해결하기 위해서 DI가 등장했던 것입니다. 

 

1. DI를 사용해도 상황에 따라서는 내부 코드를 수정해야 하는 상황이 생길 수 있습니다.

결과적으로는 맞습니다.  어쩔 수 없죠. 사실 실제 개발하는 과정에서 ID를 구현하다고 해서 클래스의 캡슐화를 완벽하게 구현하기는 힘들 것입니다. 따라서 굳이 DI를 구현할 이유는 없는 것 같습니다. 

하지만 반대로 DI를 구현하지 않을 이유도 없습니다. 아니, 해야 합니다. 시스템은 점점 복잡해지고 있습니다. 하나의 문제가 생겼을 때, 그 부분만 수정한다고 해서 고쳐지는 안일한 시스템이 아닙니다. 이러한 상황에서 안정성 있는 시스템을 구현하고자 한다면 DI가 필요합니다. 

여기에 대해서 좀 더 복잡하게 글을 쓸 수도 있을 것 같은데, 굳이 쓰지 않더라도 알게 되겠죠. 바로 다음으로 넘어갑시다.

 

 

 

○ SRP(Single Responsibility Principle - 단일 책임 원칙) 

위 내용을 좀 더 이야기해봅시다. 아까 전에 우리는 캡슐화의 대한 이야기를 해보았습니다. 이러한 캡슐화가 필요한 이유는 근본적으로 SRP(단일 책임 원칙) 때문입니다. 

위키백과를  SRP의 정의가 나와있습니다. 

객체 지향 프로그래밍에서 단일 책임 원칙(single responsibility principle)이란 모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화해야 함을 일컫는다. 클래스가 제공하는 모든 기능은 이 책임과 주의 깊게 부합해야 한다.
출처 : https://ko.wikipedia.org/wiki/%EB%8B%A8%EC%9D%BC_%EC%B1%85%EC%9E%84_%EC%9B%90%EC%B9%99

객체 지향 프로그래밍에서 각 클래스는 하나의 책임 원칙을 가지고 있어야 합니다. 하나의 클래스가 여러 개의 기능을 책임지게 되면 관리하기 힘들어 집니다. 시스템 애플리케이션은 점점 복잡해져가고 있고, 이러한 여러개의 책임을 가지는 프로그래밍은 관리하기 힘들기 때문에 지양해야 합니다. 

 

 

○ DI대해 좀 더

우선 여기까지만 해보려고 합니다. 개념과 필요성은 원리 중심으로 설명할 필요가 있었기 때문에 추상적으로 설명했기 때문입니다. 따라서 다음에는 코드를 가지고 좀 더 객관적으로 DI의 대해 구현하는 방법을 다뤄볼 것입니다. 

스프링 프래임워크를 다루지는 않았지만, 스프링의 꽃인 DI를 이해했다면 어느 정도는 이해한 셈입니다. 저의 경험이지만 DI를 정확히 이해하지 못하고 스프링을 다루었을 때에는 정말 왜 스프링 프래임워크를 써야 했는지 의문을 가지며 사용법만 터득했던 것 같습니다. 이렇게 원리를 모르는 기술은 자신의 것이 아닙니다. 따라서 뭐든지 원리를 제대로 이해해야 합니다. 더 많이 아는 것보다, 정확히 아는 것이 더 중요한 것 같습니다. 

 

○ 마치는 글 

정말 오랜만입니다. 한 2달 정도 블로그를 쉬었던 것 같습니다.... 결국 변명이지만 다른 더 중요한 일이 있었고, 병행할 수는 있었지만 그쪽에 좀 더 투자하고 싶었던 욕심이 있었습니다. 잠깐 멈추더라도 제대로 하고 싶었던 마음이 있었던 것입니다. 이때 가장 중요한 것은 멈추더라도 때가 되면 다시 시작하는 것이지만, 지금 이제 다시 때가 된 것 같습니다. 

현재 DI와 스프링 프레임워크의 대해서 알아보았지만, 아쉽게도 다음 글은 스프링이 아니라 알고리즘의 관한 글일 것입니다. 이쪽을 빠르게 끝내고 완성시키고자 합니다. 스프링은 가끔씩 이어가는 코너로 할까 합니다. 

결국 다시 돌아왔습니다. 지금까지 이 글을 읽어주신 많은 사람들께 감사드립니다.