본문 바로가기

개발/안드로이드

DI (Dependency Injection)는 왜 써야 하는가?

반응형

 각 클래스간의 관계를 유연하게 해서 OOP에 효율적인 코드를 만들자!

 

 

DI란? 객체와 객체의 관계를 주입을 통해서 연결해 주어 객체간의 관계를 유연하게 하는 것.
이라고 말할 수 있다.
그렇다면 객체와 객체간의 관계를 유연하게 하는 것은 어떤 것일까?
아래 내용으로 천천히 설명해 보겠다.


사설.

 

언젠가 이런 질문을 받은적이 있다.
DI에 대해서 아시나요? DI는 왜 사용해야 하나요??

 

 

나의 답은 너무나 간단하게 
"OOP를 잘하려면 DI를 사용해야 합니다. " 였는데

 

생각해보면 이 이야기가 무엇인지 알아들을수 있는 사람이 있고 혹여는 오해 할 수 있겠다라는 생각이 들었다.

또 누군가는 좋다고 하는건 알겠는데 굳이?? DI 쓰지 않고도 개발은 가능하잖아?? 라고 생각하는 사람도 있을 것이다.

게다가 OOP를 잘하려면 알고 실천해야 할것들이 많이 있다. 

 

(하지만 당신은 이미 사용자!! 이미 쓰고 있으면서도 모르는 사람들이 대부분일것이다.)

 

 

그렇다면 DI는 어떻게 좋고 왜 사용해야 하는지 한번 알아보도록 하자.
사실 아는것과 실제 적용하는건 다른 이야기 이긴 한데 알아두고 적용하려고 노력하면 언젠가는 내것이 되지 않을까? 관심이 간다면 천천히 한번 따라가 보자.

 

모든 프로그램에는 성격에 맞는 프로그램 방식이 있다.
블로그에서 좋다고 했다고 유투브에서 좋다고 했다고 모두가 답은 아니니까 각자의 상황에 맞게 우선 이해하고 필요하다면 프로젝트에 적용해 보자.

 


DI (Dependency Injection)는 객관적인 사실만 놓고 본다면 의존관계 주입을 이야기 하는 단어이다.
말그대로 의존관계에 있는 오브젝트를 주입해서 사용하게 만드는 것이다.

 

의존관계 주입을 사용하는 예를 들면 다음과 같다.
의존관계 주입을 통해서 로봇에 팔과 다리를 장착한다고 생각해 보자. 다음과 같을 것이다.

val superRobot = SuperRobot(SuperArm(), SuperLeg())

 

 

이게 DI라고?? DI 뭐 별거 없네?? 내가 코딩하는 코딩과 다를게 없는데?? 라고 생각할 것이다.
맞다 알고 보면 별게 없다.

 

 

아래와 같이 요로케도 가능하지만 요로케 하면 프로그램 실행 중간에 객체의 값을 바꿀수 있으므로 프로그래머가 의도하지 않았지만 내용이 변경될수도 있다. 생성자에 객체를 넣고 따로 변경할수 있는 기능을 제공하지 않는다면 객체가 가지고 있는 외부에서 주입된 객체들의 값의 무결성을 보장 할 수 있을 것이다.

val superRobot = SuperRobot()
superRobot.arm = SuperArm()
superRobot.leg = SuperLeg()

 


 

다음내용을 보면서 천천히 다시 한번 생각해보자.

 

일반적으로 로봇클래스를 만들어 보자라고 하면 다음과 같이 팔과 다리를 만들어 줄 것이다.
이미 DI를 이용하는 사람들도 있겠지만 말이다.

이렇게 클래스간의 관계가 밀접해 지면 테스트를 할때도 곤란해 진다.

로봇을 테스트 해야 하는데 팔과 다리를 테스트하게 될지도 모른다. 

로봇의 팔과 다리가 바뀌면 테스트도 바뀌어야 하고 테스트의 결과도 달라질 수도 있기 때문이다.

 

 

class SuperRobot : Robot {
    var arm = SuperArm()
    var leg = SuperLeg()
}

 

 

 

만약에 요구사항이 바뀌어서 다른 팔과 다리를 사용해야 한다면?
로봇을 만들수 있는 설계도인 SuperRobot클래스를 수정해야 한다.
다음과 같이 말이다.

class SuperRobot : Robot {
    var arm = PowerArm()
    var leg = PowerLeg()
}

 

프로그램을 개발하면서 요구사항이 주기적으로 변경되고 Robot및 Arm의 기능이 변경되다 보면 Robot클래스와 Arm 클래스는 끈끈하게 엮이는 관계를 가지게 될 가능성이 높아진다. 마치 두 클래스를 분리하세요. 라고 하면 "그게 가능한가요?? 너무 많이 고쳐야 하는데요~!?" 라는 말이 저절로 나오게 말이다.

 

 

설계도를 5년에 한번이나 10년에 한번 바꿔야 한다면 뭐.. 그래 그때마다 설계도를 다시 만들면 되지.
자주 있는 일도 아니잖아?? 라고 생각할 수 있다.

 

하지만 같은 한해에만 여러 종류의 로봇을 만들어야 하고 요구사항이 쉴세 없이 바뀐다면? 어떨까?
고객이 또는 결정권을 가진 누군가가 갑자기 요구사항을 바꾼다면 어떻게 대응해야 할까?

 

 

미리 적정하지 말자!! 그래 우리에겐 Ctrl+C, Ctrl+V라는 엄청난 무기가 있다.
"여러 로봇 여러 종류의 팔다리 그래!! Ctrl+C, Ctrl+V로 만들면 되지!! 아무것도 아니야!! "
그래 어떻게 생각하면 아무것도 아니다 그냥 Ctrl+C, Ctrl+V만 열심히 눌러대고 이름을 바꿔주고 필요한 내용들을 적용하면 되니까 말이다.

 

 

여기까지는 너무 아름답다. 생각만 해도 너무 내 자신이 자랑스럽고 정해진 시간안에 마무리한 내가 너무 대견하다.


문제 발생 1

그런데 Robot클래스에서 Bug가 발견되었다. 그것도 Ctrl+C, Ctrl+V의 기본이 된 클래스가 말이다.
사업확장을 통해서 각 현장의 요구사항에 맞게 지금까지 복사해서 만들어온 Robot클래스가 80개가 넘어가는데 말이다.

 

 

너무 말도 안되는 예인가?? 이 글을 읽는 사람중에는 "나는 아직 클래스 20개를 만들기도 버거운데 비슷한 성질을 가지고 있는 Class를 80개를 만들일이 있을까?? 팔, 다리 라면 모를까?" 라고 생각하는 사람도 분명 있을 것이다.

 

위 이야기는 실제 업무를 하면서 쉽게 발생할 수 있는 일이다. 업무에 따라 다를 수도 있지만 말이다.

 


(참고로 지인은 현재 1년짜리 프로젝트에서 매일 같이 CRUD만 만들어 내고 있다고 한다. 한달에 약 5~60개 몇개월 작업해야 하는 양을 말이다. 유연함을 준비하지 않고 작업 했는데 갑자기 요구사항이 바뀐다면? 그것도 프로젝트가 마무리 되어가는 시점에 말이다) 그 많은 클래스를 수정해야 한다면 생각만 해도 앞이 깜깜하지 않은가?

 

 

위내용을 가정했을때 Bug수정은 어떻게 해야할까? IDE 툴의 찾기 기능을 이용하면 아무것도 아닌일이 될까?
이런 문제를 방지하려면??

 

 

말도 안되는 비약이라고 할 수 있겠지만 위 내용을 가정하고 DI를 이용했다면 어떨까??
팔, 다리만 바뀌었던 Robot 클래스라면 DI를 이용했다면 문제해결이 아마 쉬웠을 것이다.

 

 

다음 코드를 보자 .
팔과 다리만 바뀌었지 Robot의 기본틀이 되는 SuperRobot은 바뀌지 않았다.
당연히 Arm, Leg는 interface화 되어 있다.

val superRobot = SuperRobot(SuperArm(), SuperLeg())

val powerRobot = SuperRobot(PowerArm(), PowerLeg())

val hyperRobot = SuperRobot(HyperArm(), HyperLeg())

 

팔과 다리만 바뀌는 로봇이긴 하지만
위와 같은 코드 였다면 우리는 SuperRobot() 클래스의 오류만 수정해 주면 된다.

위의 이야기는 단지 비약적인 예 중 하나임을 기억하자.
너무 심플한 문제 이기도 하지만 말이다.


문제 발생 2

로봇 클래스의 Bug이외에도 특정 Arm과 Leg 에서 Bug가 발생했다면 어떻게 해야할까?
만약 DI를 이용하지 않고 만들었다면 특정 Arm과 Leg도 수정해야 하지만 끈끈하게 묶여 있는 Robot Class를 수정해야 할지도 모른다.

 


우리는 너무나도 쉽게 function을 추가하고 내용을 바꾸면서 프로그램을 하고 있는지도 모른다.
지금까지 우리가 해왔던 코딩이 얼마나 큰문제를 가지고 있었는지 확인 할 수 있다.

 

 

이미 이런문제에 대해서 고민하고 DI에 대해서 알고 있거나 몸이 배워서 나도 모르게 이미 DI를 이용하고 있다면 다행이지만 말이다.

극단적인 예이기는 하지만 실제 업무를 하면서 일어나지 않으리라는 법은 없다.

 

 

갑자기 요구사항이 바뀌면 짜증나는가? 아마 유연하게 프로그램하고 있지 못해서 그럴지도 모른다.

 


프로그램의 요구사항은 수시로 변한다. 그것도 마감일이 다가오면 요구사항을 꼭 한번씩은 바꿔야 직성이 풀리는 사람들이 있다.

이런사람들과는 대화가 되지 않는다. 미리 미리 유연하게 대처할 수 있도록 프로그램적으로 유연성을 갖춰 두는것이 좋다.

 

우리는 인성 좋은 사람들이니까 마감일 가까워 바뀌는 요구사항쯤이야~!! 훗!! 


 

그렇다면 위와 같은 조립(주입)은 어디서 해야 하는것일까?

 

로봇을 만들었으니 로봇을 판다(납품한다)라고 생각해보자.

우리는 로봇을 만들어서 파는 SRCompany(Super Robot Company)이다.

SRCompany는 로봇을 대리점에서 판매한다.
로봇은 공장에서 만들어서 각 지역 대리점에 납품한다.

 

그렇다 조립은 공장에서 하는것이다.

val chain001 = SRChainStore()

chain001.addRobots(SRFactory.getRobots())

 

 

로봇을 제공해야 하는 Factory Class에서 위와 같이 조립하면 된다.

프로그램을 하면서는 실제 사용해야 하는 클래스에서 DI를 구현하거나 DI를 담당하는 클래스를 두면 된다.

 

우리는 DI를 담당하는 클래스를 개념상 Container 혹은 Factory라고 부른다.

 

 

위내용을 약간 변경하여 공장에 주문할 로봇의 모델명을 전달하고 모델명에 맞게 제작된 로봇을 납품 받는다면

다음과 같이 수정이 가능할 것이다.

 

팩토리에서는 모델명을 입력받아서 해당객체를 제작하여 돌려 줄것이다.

val chain001 = SRChainStore()

chain001.addRobots(SRFactory.getRobots("Super"))
chain001.addRobots(SRFactory.getRobots("Power"))
class SRFactory : RobotFactory {

    fun getRobots(modelName:String) {
        when(modelName) {
            "Super" -> {
                return SuperRobot(SuperArm(), SuperLeg())
            }
            "Power" ->{
                return SuperRobot(PowerArm(), PowerLeg())
            }
        }
    }
}

 

바로 Robot 객체를 만드는것이 아니라 Factory라고 하는 중간다리를 거쳐야 하는것이 귀찮고 불편해 보일 수 있다. 하지만 DI로 인해서 얻는 장점이 많기 때문에 DI를 권장하고 있다. 특히 변경이 잦은 큰 프로젝트를 진행하고 있다면 말이다.

 

파일 3개로 끝날수 있는 임시 프로그램에는 당연히 DI를 사용하지 않는 편이 훨씬 효율적일것이다~! 

 


이런 DI를 편하게 해주는 것이 Annotation이다 반복되는 코드들을 숨겨주고 우리가 로직에 집중할 수 있게 해주기 위해서 말이다.

 

 

스프링에서 @ (annotation)을 사용하여 Controller와 Service를 만들어 보았는가?

 

생성자에 사용되는 객체들이 프로그래머가 따로 만들지 않아도 자동으로 만들어 져서 주입되는 것을 경험했는가?

 

안드로이드의 DI툴 Dagger, Koin, Hilt등을 들어보았는가??

 

 

DI가 가지는 장점의 필요에 의하여 반복되는 코드를 숨겨주고 실제 로직에 집중하게 해주는 Tool들이다.

 

DI를 공부하게 되면 자연스럽게 interface와도 친해 져야 한다.
여러분은 조금씩 진정한 프로그래머가 되어가고 있는것이다.

 

안드로이드의 DI 공부하고 있다면 다음 포스팅을 참고 해봐도 좋겠다.


Anroid Hilt에 대해서 알아보자! 


 

반응형